API reference

TracePath has two HTTP surfaces, on two hosts, with two different kinds of credential:

IngestApplication API
Hosthttps://ingest.tracepath.devhttps://app.tracepath.dev
Base path/api/otel/api
CredentialProject tokenPersonal access token or dashboard session
PurposeWriting telemetryEverything 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=:

EndpointReturns
/api/logsLog records, filtered and paginated
/api/exception-stack-tracesGrouped, ranked exceptions
/api/exception-stack-traces/:hashOne exception group's occurrences
/api/endpointsInbound HTTP endpoints
/api/endpoints/groupedEndpoints rolled up by route
/api/tasksBackground tasks and scheduled jobs
/api/metrics/queryA metric query over a time range
/api/statsThe 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/settings

Inviting 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 URL
curl 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:

SignalEndpoint
TracesPOST https://ingest.tracepath.dev/api/otel/v1/traces
MetricsPOST https://ingest.tracepath.dev/api/otel/v1/metrics
LogsPOST 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                 # optional

Each 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

CodeMeaning
200 / 201Success
400Malformed body, or a project-scoped route without a parseable projectId
401Missing, malformed, expired or unknown bearer token. Empty body
403Authenticated but not permitted: no access to that project or organization, insufficient role, or a suspended organization
404No such resource, or not one of yours
408The request body arrived too slowly
413Body over the cap — 8 MB on the application API, 10 MB on ingest
422Validation failure, or a plan limit reached. The error string names the reason
429Rate limit exceeded on an authentication endpoint
500Our fault. Report it with the time and the endpoint
503Ingest 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].