DOCS · REST API

API reference

Base URL: https://app.tillpulse.io (production) or http://localhost:3000 (local). Auth via httpOnly cookies (tp_access JWT, tp_refresh) or Bearer API key.

Authentication

Every request requires a valid session — either via cookies set by POST /api/auth/login, or via a Bearer token from a personal API key.

# Cookie session (web app default)
curl https://app.tillpulse.io/api/projects \
  --cookie 'tp_access=<jwt>; tp_refresh=<token>'

# API key (CLI / CI)
curl https://app.tillpulse.io/api/projects \
  -H 'Authorization: Bearer tp_sk_af_ab12cdef...'

Auth endpoints

POST /api/auth/signup — create user + org.

{ "email": "ada@example.com", "password": "min8chars", "name": "Ada Okafor", "org": "example" }
→ { "ok": true, "org": "example" }

POST /api/auth/login — email + password (+ TOTP).

{ "email": "...", "password": "...", "totpToken": "123456" }
→ { "ok": true, "org": "example", "role": "owner" }
→ { "totp_required": true }   // if 2FA enabled

POST /api/auth/logout — revoke refresh, clear cookies.

POST /api/auth/refresh — rotate refresh, mint new access.

POST /api/auth/forgot-password — body { email }; always returns 200 (no enumeration).

POST /api/auth/reset-password — body { token, password }.

POST /api/auth/change-password — body { currentPassword, newPassword }.

POST /api/auth/accept-invite — body { token, password }.

GET /api/auth/sso/start?org=example — redirect to OIDC IdP.

GET /api/auth/sso/callback — IdP redirects here; sets cookies.

GET / PUT /api/auth/sso/config — owner-only org SSO config.

Two-factor (TOTP)

POST /api/auth/2fa/setup{ otpauthUrl, backupCodes }

POST /api/auth/2fa/verify — body { token }

POST /api/auth/2fa/disable — body { password }

Projects

GET /api/projects — list projects in org.

POST /api/projects — body { name, platform, region } (region: af-south-1 | eu-central-1 | us-east-1; platform: react-native | flutter | ios | android | web).

GET / PATCH /api/projects/:projectId/settings

POST /api/projects/:projectId/rotate-dsn — invalidates the old key in the Redis cache within 5 min.

GET /api/projects/:projectId/issues — project-scoped issue list.

POST /api/projects/:projectId/source-maps — multipart/form-data file + release + filename + symbolication_type (js_source_map | proguard).

GET /api/projects/:projectId/source-maps — list uploads.

DELETE /api/projects/:projectId/source-maps/:mapId

POST / GET /api/projects/:projectId/dsyms — iOS dSYM upload (UUID auto-extracted from Mach-O LC_UUID).

GET / POST /api/projects/:projectId/integrations/github — repo URL + webhook secret (regenerate on POST).

GET / POST / DELETE /api/projects/:projectId/integrations/tracker — Linear or Jira credentials (encrypted at rest).

Issues

GET /api/issues — query: status, severity, projectId, limit, offset, sort.

GET /api/issues/:issueId — full detail (breadcrumbs, frames, AI analysis).

PATCH /api/issues/:issueId — body { status } (unresolved | resolved | ignored | regressed | escalated) or { assignee_id }.

DELETE /api/issues/:issueId

GET / POST /api/issues/:issueId/comments

DELETE /api/issues/:issueId/comments/:commentId (author or admin).

POST /api/issues/:issueId/tracker-ticket → creates Linear / Jira issue, links it.

GET /api/issues/search?q=...&projectId=... — Meilisearch full-text.

POST /api/issues/nlq — body { query } → Claude-translated SELECT (server-side validators block all non-SELECT statements; project filter injected server-side).

Alerts

GET / POST /api/alerts?projectId=...

{
  "projectId": "uuid",
  "name": "High severity spike",
  "trigger_type": "issue_spike",
  "conditions": { "threshold": 50, "window_minutes": 15, "severity": "high" },
  "actions": [{ "type": "slack", "webhook_url": "https://hooks.slack.com/..." }]
}

Trigger types: new_issue, issue_regression, issue_spike, security_event, pre_crash_state, battery_drain_threshold, performance_threshold, critical_path.

Action types: slack, webhook, email, pagerduty, msteams.

PATCH / DELETE /api/alerts/:ruleId

POST /api/alerts/:alertId/ack — acknowledge a fired alert.

GET /api/alerts/recent?limit=8 — used by the dashboard's notification bell.

Metrics & performance

GET /api/metrics?projectId=&range=1h|24h|7d|30d&metric=crash_rate|anr_rate|event_count|user_count

GET /api/performance?projectId=&range=7d — P50/P75/P95 cold-start, ANR rate, screen-render times.

Team

GET /api/team — list members.

POST /api/team — body { email, role }; sends invite email.

PATCH /api/team/:memberId — body { role }.

DELETE /api/team/:memberId

POST /api/invites — bulk invite. Body { emails: [...], role }.

API keys

GET /api/api-keys — list (no raw key — only prefix).

POST /api/api-keys — body { name, scopes }. Raw key returned once.

DELETE /api/api-keys/:keyId — immediate revoke.

Ask Pulse (AI)

POST /api/ask — body { messages: [{ role, content }] }; returns Server-Sent Events.

data: {"delta":"Based on the ClickHouse data..."}
data: {"delta":"```sql\nSELECT ...```"}
data: [DONE]

The model never executes SQL. The dashboard renders highlighted SQL with a sparkline; users copy-paste into ClickHouse if they want to run it.

Audit log

GET /api/audit-log?limit=50&offset=0&action=user.password_changed — admin/owner only.

Onboarding

GET /api/onboarding/status{ steps: { project_created, sdk_installed, first_event, team_invited }, completed }

Releases

GET / POST /api/releases?projectId=...

Notifications

POST /api/notifications/devices — register Expo / FCM push token. Body { token, platform }.

Webhooks (incoming)

POST /api/integrations/slack/interactive — Slack button payloads. Verifies X-Slack-Signature + 5-minute replay window.

POST /api/integrations/github/webhook?project=... — verifies X-Hub-Signature-256 against the per-project secret.

POST /api/integrations/tracker/webhook?provider=linear|jira — Linear via HMAC-SHA256, Jira via ?token=.

Error envelope

{ "error": "Human-readable message" }
StatusMeaning
400Validation error — check error
401Not authenticated — redirect to /auth/login
403Authenticated but insufficient role
404Resource not found
409Conflict (duplicate slug, already invited, etc.)
429Rate limited (per-DSN sliding window or per-IP)
500Internal server error