API reference
TracePath has two HTTP surfaces, on two hosts, with two different kinds of credential:
| Ingest | Application API | |
|---|---|---|
| Host | https://ingest.tracepath.dev | https://app.tracepath.dev |
| Base path | /api/otel | /api |
| Credential | Project token | Personal access token or dashboard session |
| Purpose | Writing telemetry | Everything the dashboard does |
The application API is the same API the dashboard calls. There is no version
segment: routes are flat under /api, so it is /api/projects, not
/api/v1/projects.
This page documents the endpoints that are stable enough to build on: authentication, personal access tokens, projects, billing and the main read endpoints. The API is not frozen, and the dashboard is its first consumer — if you automate something not listed here, pin your expectations and watch for changes.
Authentication
Every authenticated request carries an Authorization: Bearer <token> header, where
the token is either a personal access token (prefix tpp_) or a dashboard
session JWT. A missing, malformed or unknown credential returns 401 with an empty
body — the header value must literally begin with Bearer .
A personal access token acts as you: it inherits your role in every organization you belong to and nothing more. There are no separate token scopes.
Personal access tokens
Create them from the Account page in the dashboard, or over the API with an existing credential.
curl -X POST https://app.tracepath.dev/api/personal-access-tokens \
-H "Authorization: Bearer $TRACEPATH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "ci-pipeline", "expiresInDays": 90}'201 Created:
{
"id": "0d6f0f1a-6a0e-4b4d-9a3f-2f1f8a4c5d21",
"name": "ci-pipeline",
"prefix": "tpp_AbCdEfGh",
"token": "tpp_AbCdEfGh...",
"createdAt": "2026-09-21T09:14:03Z",
"expiresAt": "2026-12-20T09:14:03Z"
}token is returned once and never again — only a SHA-256 hash and the
12-character prefix are stored. name is required and capped at 100 characters;
expiresInDays is optional and must be between 1 and 3650. A violation of either
returns 422 with an error string.
List and revoke:
curl https://app.tracepath.dev/api/personal-access-tokens \
-H "Authorization: Bearer $TRACEPATH_TOKEN"
curl -X DELETE https://app.tracepath.dev/api/personal-access-tokens/<token-id> \
-H "Authorization: Bearer $TRACEPATH_TOKEN"The list returns id, prefix, name, lastUsedAt, expiresAt and createdAt
for your own tokens — never the secret. Revoking answers {"status":"revoked"}, or
404 if the id is not one of yours.
Device authorization flow
Headless clients — the CLI, an MCP server, anything without a browser — authenticate
with the OAuth 2.0 device grant (RFC 8628) instead of a pasted token. Both JSON and
application/x-www-form-urlencoded bodies are accepted.
1. Ask for a code. The client_id must be a known first-party client; omitting it
defaults to the CLI.
curl -X POST https://app.tracepath.dev/api/auth/device/authorize \
-H "Content-Type: application/json" \
-d '{"client_id": "tracepath-cli"}'{
"device_code": "…",
"user_code": "BDWJ-KQXT",
"verification_uri": "https://app.tracepath.dev/device",
"verification_uri_complete": "https://app.tracepath.dev/device?user_code=BDWJ-KQXT",
"expires_in": 600,
"interval": 5
}2. Send the person to verification_uri_complete. Signed in, they see which
client is asking and approve or deny it. That screen is backed by
GET /api/device?user_code=…, POST /api/device/approve and
POST /api/device/deny, all of which require a dashboard session.
3. Poll for the token, no faster than interval seconds:
curl -X POST https://app.tracepath.dev/api/auth/device/token \
-H "Content-Type: application/json" \
-d '{"grant_type": "device_code", "device_code": "<device_code>"}'While the request is outstanding you get 400 with {"error":"authorization_pending"},
or slow_down if you are polling too fast. After approval you get 200 with an
access token, a refresh token, token_type and expires_in. A denial returns
access_denied; an unapproved code past its 10-minute life returns expired_token.
Refresh with {"grant_type": "refresh_token", "refresh_token": "…"} against the same
endpoint, and revoke the whole token family with
POST /api/auth/logout carrying the refresh token. Both token routes share a 60
requests/minute per-IP budget, which is sized for device polling behind NAT.
The CLI wraps all of this — see CLI Authentication.
Project scope
Most endpoints operate on one project, identified by a projectId query
parameter, not a path segment:
GET /api/dashboard?projectId=<uuid>A request without a parseable projectId on a project-scoped route returns 400. A
projectId you have no access to returns 403 with
{"error":"Access denied"}.
Read endpoints tend to be POST with a JSON filter body even though they read
nothing — the filters are too large for a query string. The projectId still travels
in the query string.
Projects
curl https://app.tracepath.dev/api/projects \
-H "Authorization: Bearer $TRACEPATH_TOKEN"Returns every project you can reach, each with its id, name, framework,
organizationId, backendUrl, your role on it, and its ingest token. Members
whose effective role is read-only receive the literal string
read-only-hidden-token in place of the real token.
Creating a project is project-scoped like the rest: pass an existing project's
projectId to establish the organization context, and optionally an
organizationId in the body to create it in a different organization you can write
to.
curl -X POST "https://app.tracepath.dev/api/projects?projectId=<existing-project-uuid>" \
-H "Authorization: Bearer $TRACEPATH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "checkout-api", "framework": "opentelemetry"}'framework is required and must be one of the values the dashboard's project picker
offers; opentelemetry is the one to use for any server-side system. name is 1–100
characters of letters, numbers, spaces, hyphens and underscores. 201 Created
returns the project with its new token.
Crossing the plan's project limit returns 422:
{"error":"You have reached the projects limit on the free plan. Upgrade to add more."}PUT /api/projects?projectId=… updates a project and DELETE /api/projects?projectId=…
deletes one; both need write access.
POST /api/projects/source-map-token?projectId=… issues a new source-map upload
token for the project and returns {"sourceMapToken":"…"}. The previous one stops
working immediately.
Reading telemetry
All of these are POST, authenticated, and scoped with ?projectId=:
| Endpoint | Returns |
|---|---|
/api/logs | Log records, filtered and paginated |
/api/exception-stack-traces | Grouped, ranked exceptions |
/api/exception-stack-traces/:hash | One exception group's occurrences |
/api/endpoints | Inbound HTTP endpoints |
/api/endpoints/grouped | Endpoints rolled up by route |
/api/tasks | Background tasks and scheduled jobs |
/api/metrics/query | A metric query over a time range |
/api/stats | The homepage summary figures |
Plus a few GET routes: /api/dashboard, /api/dashboard/overview,
/api/metrics/discover and /api/dashboards.
A log search, showing the filter shape:
curl -X POST "https://app.tracepath.dev/api/logs?projectId=<project-uuid>" \
-H "Authorization: Bearer $TRACEPATH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"fromDate": "2026-09-20T00:00:00Z",
"toDate": "2026-09-21T00:00:00Z",
"serviceName": "checkout-api",
"minSeverity": 17,
"orderBy": "timestamp",
"sortDirection": "desc",
"pagination": {"page": 1, "pageSize": 50}
}'pagination is not optional: page must be at least 1 and pageSize between 1 and
500, or the request is rejected with 400. minSeverity uses OpenTelemetry severity
numbers (9 = INFO, 13 = WARN, 17 = ERROR).
A free-text search over log bodies with no other selector is capped at a 24-hour
range and answers 422 beyond it. Add a serviceName, minSeverity, traceId or
an attribute filter to search wider.
Organizations and members
Organization routes are the one family that does use a path parameter,
:organizationId, and most of them require the admin role:
GET /api/organizations/:organizationId/members
PUT /api/organizations/:organizationId/members/:userId
DELETE /api/organizations/:organizationId/members/:userId
GET /api/organizations/:organizationId/invitations
POST /api/organizations/:organizationId/invitations
DELETE /api/organizations/:organizationId/invitations/:id
GET /api/organizations/:organizationId/settings
PUT /api/organizations/:organizationId/settingsInviting past the plan's seat limit returns 422 with the same limit message shape
as projects.
Billing
GET /api/billing/plans # public: the plan catalogue and its limits
GET /api/billing/usage # this period's metered usage for your organization
GET /api/billing/subscription # plan code, status and period
POST /api/billing/checkout # {"planCode":"pro","interval":"monthly"} -> Stripe Checkout URL
POST /api/billing/portal # -> Stripe customer portal URLcurl https://app.tracepath.dev/api/billing/usage \
-H "Authorization: Bearer $TRACEPATH_TOKEN"{
"usage": {
"planCode": "pro",
"periodStart": "2026-09",
"counters": {"ingest_bytes": 18253611008},
"limits": {
"projects": 10,
"seats": 10,
"ingest_gb_monthly": 50,
"retention_days_hot": 30,
"retention_days_warm": 0,
"synthetics_checks": 250,
"session_recordings": 0
}
}
}counters.ingest_bytes is decompressed bytes for the current calendar month across
every project in the organization — the number the quota is judged against. See
Billing.
Ingest endpoints
Telemetry goes to the ingest host and authenticates with a project token, never a personal access token:
| Signal | Endpoint |
|---|---|
| Traces | POST https://ingest.tracepath.dev/api/otel/v1/traces |
| Metrics | POST https://ingest.tracepath.dev/api/otel/v1/metrics |
| Logs | POST https://ingest.tracepath.dev/api/otel/v1/logs |
Authorization: Bearer <project token>
Content-Type: application/x-protobuf # or anything else, which is read as OTLP/JSON
Content-Encoding: gzip # optionalEach path also answers OPTIONS for CORS preflight, so a browser-side exporter can
post to it directly. A successful export returns 200 with an empty OTLP response
body, encoded the same way you sent the request.
POST /api/otel/v1development/profiles accepts OTLP profiles. That signal is
experimental upstream in OpenTelemetry, and so is this endpoint — the path will change
when the specification stabilises.
For the full protocol reference — limits, compression, content types and the per-language wiring — see OpenTelemetry Integration.
Status codes
| Code | Meaning |
|---|---|
200 / 201 | Success |
400 | Malformed body, or a project-scoped route without a parseable projectId |
401 | Missing, malformed, expired or unknown bearer token. Empty body |
403 | Authenticated but not permitted: no access to that project or organization, insufficient role, or a suspended organization |
404 | No such resource, or not one of yours |
408 | The request body arrived too slowly |
413 | Body over the cap — 8 MB on the application API, 10 MB on ingest |
422 | Validation failure, or a plan limit reached. The error string names the reason |
429 | Rate limit exceeded on an authentication endpoint |
500 | Our fault. Report it with the time and the endpoint |
503 | Ingest paused: quota exhausted, organization suspended, or the admission gate is saturated. Always with Retry-After |
Errors carry {"error": "<message>"}, except 401, which has no body.
Rate limits
Authentication endpoints are rate-limited per IP and answer 429 with
{"error":"Too many requests, try again in Ns"} when you cross one: password sign-in
at 10 requests/minute, OAuth start and callback at 20/minute, device authorization at
10/minute, the OAuth token endpoints at a shared 60/minute, password-reset requests at
5/minute. Ordinary authenticated API calls are not rate-limited by request count
today; the ingest admission gate bounds concurrency instead.
Getting help
Something here wrong, or an endpoint you need that is not listed? Write to [email protected]. Security issues go to [email protected].