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 enabledPOST /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" }| Status | Meaning |
|---|---|
400 | Validation error — check error |
401 | Not authenticated — redirect to /auth/login |
403 | Authenticated but insufficient role |
404 | Resource not found |
409 | Conflict (duplicate slug, already invited, etc.) |
429 | Rate limited (per-DSN sliding window or per-IP) |
500 | Internal server error |