Docs

Errors

Every error code that can be returned by the Tendral Partners API. Each entry carries the error.type bucket, HTTP status, retry guidance, and a sample response. The doc_url field on every error response deep-links into this page.

Partners endpoints emit a Stripe-style error envelope:

{
  "error": {
    "type": "invalid_request_error",
    "code": "TOKEN_NOT_FOUND",
    "message": "No record exists for the provided token.",
    "details": { "token": "Ab3kQ9" },
    "doc_url": "https://developer.tendralhealth.com/docs/errors#TOKEN_NOT_FOUND",
    "request_id": "req_018f5a2e7c127c8da123cafed00dfeed"
  }
}

Branch retry logic on error.type (the coarse machine-routable bucket), not error.code (the specific reason). New codes are additive within an existing type; new types are not. The request_id is also surfaced on the X-Request-Id response header — quote it when reporting issues.

Error types

TypeTypical HTTPRetryable?Meaning
authentication_error401NoBearer missing/expired/revoked.
invalid_request_error400, 404NoRequest is malformed or refers to a missing resource.
permission_error403NoToken lacks required scope.
rate_limit_error429Yes — after Retry-AfterAPI key exceeded rate limit.
idempotency_error409NoIdempotency-Key reused with different body.
api_error5xxYes — exponential backoffUnexpected server error.

Error codes

UNAUTHORIZED

Bearer token missing, malformed, expired, or revoked.

Typeauthentication_error
HTTP status401
Retryable?No

The Authorization header is missing or does not match a valid token. Tokens may be revoked at any time, and they may carry an optional expires_at — once past, requests fail with this code. Re-issue a token from your Tendral dashboard.

Example

Request

GET /v1/partners/stitched/tokens/Ab3kQ9
(no Authorization header)

Response

{
  "error": {
    "type": "authentication_error",
    "code": "UNAUTHORIZED",
    "message": "Bearer token required.",
    "doc_url": "https://developer.tendralhealth.com/docs/errors#UNAUTHORIZED",
    "request_id": "req_018f5a2e7c127c8da123cafed00dfeed"
  }
}

INSUFFICIENT_SCOPE

Token does not include the required scope (e.g. tokens:read).

Typepermission_error
HTTP status403
Retryable?No

The bearer is valid but lacks the scope this endpoint requires. Tokens are scoped to one or more of tokens:read, events:read, recipients:read. Re-issue with the appropriate scopes.

Example

Request

GET /v1/partners/stitched/events?since=2026-04-01T00:00:00Z
Authorization: Bearer <token with only tokens:read>

Response

{
  "error": {
    "type": "permission_error",
    "code": "INSUFFICIENT_SCOPE",
    "message": "Token lacks events:read scope.",
    "doc_url": "https://developer.tendralhealth.com/docs/errors#INSUFFICIENT_SCOPE",
    "request_id": "req_018f5a2e7c127c8da123cafed00dfeee"
  }
}

INVALID_TOKEN

The 6-character URL token is malformed.

Typeinvalid_request_error
HTTP status400
Retryable?No

Tendral short-link tokens are exactly 6 characters in the base62 alphabet [0-9A-Za-z]. Anything outside that pattern is rejected at the validation layer before a database lookup happens.

Example

Request

GET /v1/partners/stitched/tokens/abc-123

Response

{
  "error": {
    "type": "invalid_request_error",
    "code": "INVALID_TOKEN",
    "message": "Token must match [0-9A-Za-z]{6}.",
    "details": { "token": "abc-123" },
    "doc_url": "https://developer.tendralhealth.com/docs/errors#INVALID_TOKEN",
    "request_id": "req_018f5a2e7c127c8da123cafed00dfeef"
  }
}

INVALID_CAMPAIGN_ID

Campaign id must be the prefixed cmp_<hex> form.

Typeinvalid_request_error
HTTP status400
Retryable?No

Path parameters that take a campaign id expect the prefixed form, e.g. cmp_018f5a2e7c127c8da123cafed00dfeef. Bare UUIDs, names, or other shapes are rejected.

Example

Request

GET /v1/partners/stitched/campaigns/not-a-real-id/recipients

Response

{
  "error": {
    "type": "invalid_request_error",
    "code": "INVALID_CAMPAIGN_ID",
    "message": "Campaign id must match cmp_<hex>.",
    "doc_url": "https://developer.tendralhealth.com/docs/errors#INVALID_CAMPAIGN_ID",
    "request_id": "req_018f5a2e7c127c8da123cafed00dffaa"
  }
}

TOKEN_NOT_FOUND

No record exists for the provided token.

Typeinvalid_request_error
HTTP status404
Retryable?No

The token is well-formed but no recipient record matches. Most often: the token has not been published to your platform yet (pre-arrival batch not delivered), or the recipient was withdrawn.

Example

Request

GET /v1/partners/stitched/tokens/Zz9zZ9

Response

{
  "error": {
    "type": "invalid_request_error",
    "code": "TOKEN_NOT_FOUND",
    "message": "No record exists for the provided token.",
    "details": { "token": "Zz9zZ9" },
    "doc_url": "https://developer.tendralhealth.com/docs/errors#TOKEN_NOT_FOUND",
    "request_id": "req_018f5a2e7c127c8da123cafed00dffbb"
  }
}

MISSING_PARAMETER

A required query or body parameter is missing.

Typeinvalid_request_error
HTTP status400
Retryable?No

See error.details.parameter for the field name. Most commonly fired by list endpoints when ?since= is omitted.

Example

Request

GET /v1/partners/stitched/events

Response

{
  "error": {
    "type": "invalid_request_error",
    "code": "MISSING_PARAMETER",
    "message": "`since` query parameter is required.",
    "details": { "parameter": "since" },
    "doc_url": "https://developer.tendralhealth.com/docs/errors#MISSING_PARAMETER",
    "request_id": "req_018f5a2e7c127c8da123cafed00dffcc"
  }
}

INVALID_PARAMETER

A parameter is present but malformed (wrong type, format, or value).

Typeinvalid_request_error
HTTP status400
Retryable?No

Look at error.details for the parameter name and the offending value. Common cases: limit > 500, since not ISO-8601, cursor not base64url-decodable.

Example

Request

GET /v1/partners/stitched/events?since=yesterday

Response

{
  "error": {
    "type": "invalid_request_error",
    "code": "INVALID_PARAMETER",
    "message": "`since` must be an ISO-8601 timestamp.",
    "details": { "parameter": "since", "value": "yesterday" },
    "doc_url": "https://developer.tendralhealth.com/docs/errors#INVALID_PARAMETER",
    "request_id": "req_018f5a2e7c127c8da123cafed00dffdd"
  }
}

INVALID_EXPAND

A value passed in expand[] is not allowed for this endpoint.

Typeinvalid_request_error
HTTP status400
Retryable?No

Each list endpoint defines its own allowlist for expand[] values. Check the OpenAPI spec for the operation; common allowed values are campaign, program, recipient.

Example

Request

GET /v1/partners/stitched/tokens?since=2026-04-01T00:00:00Z&expand[]=lead

Response

{
  "error": {
    "type": "invalid_request_error",
    "code": "INVALID_EXPAND",
    "message": "Unknown expand value `lead`.",
    "details": { "unknown": "lead", "allowed": ["campaign", "program"] },
    "doc_url": "https://developer.tendralhealth.com/docs/errors#INVALID_EXPAND",
    "request_id": "req_018f5a2e7c127c8da123cafed00dffee"
  }
}

INVALID_TYPE_FILTER

A value passed in type or types[] is not in the allowed enum.

Typeinvalid_request_error
HTTP status400
Retryable?No

Only outreach.* event types are exposed via the /events endpoint. Filtering by clinician.* events returns INVALID_TYPE_FILTER.

Example

Request

GET /v1/partners/stitched/events?since=2026-04-01T00:00:00Z&type=clinician.account_created

Response

{
  "error": {
    "type": "invalid_request_error",
    "code": "INVALID_TYPE_FILTER",
    "message": "Unknown event type `clinician.account_created`. Allowed: outreach.email_sent, outreach.email_bounced.",
    "details": { "unknown": "clinician.account_created", "allowed": ["outreach.email_sent", "outreach.email_bounced"] },
    "doc_url": "https://developer.tendralhealth.com/docs/errors#INVALID_TYPE_FILTER",
    "request_id": "req_018f5a2e7c127c8da123cafed00dffff"
  }
}

IDEMPOTENCY_KEY_REUSED

Same Idempotency-Key + different request body.

Typeidempotency_error
HTTP status409
Retryable?No

Once an Idempotency-Key has been associated with a request body (within the 24-hour replay window), the body cannot change. Use a fresh key for any new operation.

Example

Request

POST /v1/partners/stitched/...
Idempotency-Key: 6f8e2c44-a13e-4c8f-9d8b-1c2d3e4f5a6b
(body differs from earlier request that used the same key)

Response

{
  "error": {
    "type": "idempotency_error",
    "code": "IDEMPOTENCY_KEY_REUSED",
    "message": "Idempotency-Key was previously used with a different request body.",
    "doc_url": "https://developer.tendralhealth.com/docs/errors#IDEMPOTENCY_KEY_REUSED",
    "request_id": "req_018f5a2e7c127c8da123cafed010000"
  }
}

RATE_LIMITED

API key has exceeded its rate limit.

Typerate_limit_error
HTTP status429
Retryable?Yes (after Retry-After)

Partners endpoints default to 100 requests/minute per key. The 429 response carries Retry-After (seconds) in addition to the standard X-RateLimit-* headers. Back off accordingly; do not retry tighter.

Example

Request

(any partner endpoint, after exceeding window quota)

Response

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1715040060
Retry-After: 42

{
  "error": {
    "type": "rate_limit_error",
    "code": "RATE_LIMITED",
    "message": "Rate limit exceeded for this API key.",
    "doc_url": "https://developer.tendralhealth.com/docs/errors#RATE_LIMITED",
    "request_id": "req_018f5a2e7c127c8da123cafed010001"
  }
}

INTERNAL_ERROR

Unexpected server error.

Typeapi_error
HTTP status500
Retryable?Yes (exponential backoff)

Indicates a bug or transient infrastructure issue on the Tendral side. Retry with exponential backoff (start at 1s, cap at 60s, jitter ±20%). If the failure persists for >5 minutes, surface to your operations channel — Tendral monitors INTERNAL_ERROR rates.

Example

Request

(any endpoint)

Response

{
  "error": {
    "type": "api_error",
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred. The team has been notified.",
    "doc_url": "https://developer.tendralhealth.com/docs/errors#INTERNAL_ERROR",
    "request_id": "req_018f5a2e7c127c8da123cafed010002"
  }
}