API reference
Base URL: https://tilldev.dev (production) or http://localhost:8455 (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://tilldev.dev/api/projects \
--cookie 'tp_access=<jwt>; tp_refresh=<token>'
# API key (CLI / CI)
curl https://tilldev.dev/api/projects \
-H 'Authorization: Bearer tp_9f3a...'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 }.
POST /api/auth/sso/start — form org=<slug>; redirects to the workspace’s identity provider. Add link=1, from a signed-in page on this site, to connect your own account instead.
GET /api/auth/sso/callback — the provider redirects here. Signs in only an account connected to that provider’s subject; otherwise emails the account holder a link to connect it.
GET / PUT /api/auth/sso/config — the workspace’s provider. Owner or admin, signed in (not an API key). Endpoints must be https and public; a new issuer disconnects everyone.
GET / POST / PATCH / DELETE /api/auth/sso/domains — domains proven by a TXT record at _tilldev.<domain>; company sign-in creates accounts only on these.
GET / DELETE /api/auth/sso/identities — your own connected company sign-ins.
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 — the old DSN stops accepting events within ~5 minutes.
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
GET /api/projects/:projectId/dsyms — list dSYMs, one row per build UUID.
POST /api/projects/:projectId/dsyms — multipart/form-data file (the DWARF file inside a .dSYM, up to 32 MB) + release + optional dist. Answers with the UUIDs found and any warnings.
POST /api/projects/:projectId/dsyms/uploads — start an upload of a larger file (up to 1 GB): JSON release, filename, size, optional dist. Answers upload_id, part_size and parts.
PUT /api/projects/:projectId/dsyms/uploads/:uploadId/parts/:n — raw bytes of part n (1-based, part_size bytes; the last is the rest). Answers its etag.
POST /api/projects/:projectId/dsyms/uploads/:uploadId/complete — JSON parts: every { part_number, etag }. Registers the file like a direct upload; repeating it is safe.
DELETE /api/projects/:projectId/dsyms/uploads/:uploadId — abandon an upload.
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=... — full-text search across your issues.
POST /api/issues/nlq — body { project_id, query } → your question is translated to a safe, read-only query over your telemetry (only read-only queries are ever run; 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, secrets_anomaly (a TillSecrets agent spending unlike itself; conditions min_severity and agents), ark_signal (a TillArk signal; conditions kinds, sources and min_severity). Updates are checked the same way as creates.
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":"Computed from your telemetry..."}
data: {"delta":"```sql\nSELECT ...```"}
data: [DONE]The model never executes queries. The dashboard renders the highlighted read-only query with a sparkline; results are computed from your telemetry.
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. Slack requests are signature-verified.
POST /api/integrations/github/webhook?project=... — GitHub requests are signature-verified against your per-project secret.
POST /api/integrations/tracker/webhook?provider=linear|jira — tracker webhooks are verified per provider.
Actions that need a person
With approvals on (the default), revealing a secret, running a command with secrets, allowing a destination, consenting to a grant, presigning a call, minting a service or agent token, syncing out, lowering a tier and turning approvals off answer 428 with code: STEP_UP_REQUIRED and an approval object. Nothing is done. The person approves at approval.url with a passkey or a confirmed authenticator code; then repeat the same request with x-tilldev-approval: <approval.id>.<approval.claim>. An API key gets 403: it cannot approve.
GET /api/step-up/:id — poll until status is approved, denied or expired.
GET /api/step-up — your requests from the last 30 days (?status=pending for waiting ones).
POST /api/step-up/:id/deny — refuse a request, or stop one allowed to repeat.
GET | PUT /api/step-up/policy — { step_up: "on" | "off" }; owner or admin, and off needs an approval.
The full contract is in /openapi.json under the Approvals tag; the flow is explained in TillSecrets approvals.
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.) |
423 | The workspace's customer key refused, so the data this call needs is offline (code: KEY_LOCKED) |
428 | Needs a person's approval — see Approvals |
429 | Too Many Requests — you exceeded your rate limit |
500 | Internal server error |
Product APIs on their own hosts
Two products serve their own end-user API on a dedicated host, with their own credential and their own OpenAPI document. They are documented here so you know where to look — the reference above covers the cross-product dashboard API only.
| API | Credential | Specification |
|---|---|---|
| TillAuth — end-user identity (sign-in, sessions, MFA, OIDC) | publishable + secret app keys | auth.tilldev.dev/openapi.json |
| TillNotary — transparency log (submit, proofs, checkpoints) | tnot_… bearer | notary.tilldev.dev/v1/openapi.json |
TillNotary reads on a public log need no credential at all — the log is public by construction, so proofs and checkpoints are fetchable by anyone who wants to verify them. See the TillNotary docs for the full surface, and note that its error envelope is the richer { error, code, request_id } form rather than the{ error } shape above.