Error codes
Every error response from the Tenant REST API uses the same shape: ``json { "error": "ERRORCODE", "message": "Human-readable description" } ` There are no code…
Every error response from the Tenant REST API uses the same shape:
{ "error": "ERROR_CODE", "message": "Human-readable description" }
There are no code, field, or trace_id sub-fields. The HTTP status code carries the category; the error string narrows it.
Error codes by status
400 — Bad Request
| Code | Meaning |
|---|---|
BAD_REQUEST |
Validation failed — message names the specific field or rule |
CONFLICTING_ALIASES |
Two field aliases provided with different values (e.g. procedure_count + procedures_count) |
DISABLED |
Resource exists but is inactive — e.g. an inactive skill on api-tenant-skills-execute |
401 — Unauthorized
| Code | Meaning |
|---|---|
MISSING_AUTH |
No Authorization header or empty Bearer token |
INVALID_KEY |
Bearer token didn't match any active API key |
API keys don't expire on a schedule — they're valid until revoked. If you see INVALID_KEY for a key you know is yours, it's probably been revoked.
403 — Forbidden
| Code | Meaning |
|---|---|
INSUFFICIENT_PERMISSIONS |
Key is valid but lacks the scope needed for the endpoint |
NOT_ENTITLED |
Skill exists but isn't in your tenant's bundle allocations (api-tenant-skills-execute only) |
Cross-tenant access also returns 403 — every endpoint enforces that the key's tenant matches the resource's tenant.
404 — Not Found
| Code | Meaning |
|---|---|
NOT_FOUND |
Record doesn't exist, was deleted, or belongs to a different tenant (we don't leak existence across tenants) |
405 — Method Not Allowed
| Code | Meaning |
|---|---|
METHOD_NOT_ALLOWED |
Wrong HTTP method — message lists what's accepted |
409 — Conflict
| Code | Meaning |
|---|---|
CONFLICT |
Duplicate record. Most common on POST /api-tenant-users when the email already exists. |
5xx — Server errors
| Code | Meaning |
|---|---|
INTERNAL_ERROR |
Server-side error in the edge function — retry with backoff |
5xx responses can also come from the Supabase / edge-runtime infrastructure layer (502 / 503 / 504) without a JSON body — treat all 5xx as transient and retry with exponential backoff.
How to handle errors in code
- 400 — bug in your client. Don't retry, fix the request.
- 401 / 403 — credentials or scope problem. Don't retry; investigate the key.
- 404 — the resource isn't there. Don't retry blindly.
- 409 — duplicate. Look up the existing record instead of retrying create.
- 5xx — retry with exponential backoff (e.g. 1s → 5s → 30s, max ~3 attempts).
What we don't return
To keep error handling simple, the API deliberately doesn't return:
- An
expired_token/token_expiredcode (keys don't expire) - A
version_conflict/ optimistic-concurrency code (no ETag / If-Match handling today) - A
still_processingcode on sessions (session uploads are out-of-band; poll the list endpoint for state) - A 422 class of business-rule violations (validation errors land as 400)
If you need any of these semantics for an integration, get in touch (team@trainar.ai).
Webhook delivery errors (outbound)
If you're receiving webhook deliveries, your endpoint must return 2xx within 15 seconds. Anything else triggers TrainAR's retry policy — see Webhooks → Retries for the schedule.
Where to next
- Tenant REST API reference — per-endpoint error tables
- Webhooks — separate retry model for outbound deliveries
- API keys — scopes and key lifecycle