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
| Type | Typical HTTP | Retryable? | Meaning |
|---|---|---|---|
| authentication_error | 401 | No | Bearer missing/expired/revoked. |
| invalid_request_error | 400, 404 | No | Request is malformed or refers to a missing resource. |
| permission_error | 403 | No | Token lacks required scope. |
| rate_limit_error | 429 | Yes — after Retry-After | API key exceeded rate limit. |
| idempotency_error | 409 | No | Idempotency-Key reused with different body. |
| api_error | 5xx | Yes — exponential backoff | Unexpected server error. |
Error codes
UNAUTHORIZED
Bearer token missing, malformed, expired, or revoked.
| Type | authentication_error |
| HTTP status | 401 |
| 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).
| Type | permission_error |
| HTTP status | 403 |
| 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.
| Type | invalid_request_error |
| HTTP status | 400 |
| 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.
| Type | invalid_request_error |
| HTTP status | 400 |
| 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.
| Type | invalid_request_error |
| HTTP status | 404 |
| 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.
| Type | invalid_request_error |
| HTTP status | 400 |
| 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).
| Type | invalid_request_error |
| HTTP status | 400 |
| 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.
| Type | invalid_request_error |
| HTTP status | 400 |
| 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.
| Type | invalid_request_error |
| HTTP status | 400 |
| 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.
| Type | idempotency_error |
| HTTP status | 409 |
| 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.
| Type | rate_limit_error |
| HTTP status | 429 |
| 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.
| Type | api_error |
| HTTP status | 500 |
| 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"
}
}