Handling errors
Problem details responses, stable codes, and what to retry.
Every error response is RFC 9457 problem details, with media type application/problem+json. There are no plain-text or empty error bodies.
{
"type": "https://docs.<platform-domain>/errors/api_client.product_not_allowed",
"title": "The API client may not read this product.",
"status": 403,
"code": "api_client.product_not_allowed",
"args": { "product": "planner" },
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"instance": "/api/v1/entitlements/72f988bf-86f1-41af-91ab-2d7cd011db47"
}| Member | Use it for |
|---|---|
code | Branching. Stable, dotted, lower snake case ({area}.{reason}) and part of the contract. Each has a page. |
type | The documentation page of the code on this site. |
status | The HTTP status, the fallback for a code you do not know yet. |
args | Named values about the failure (always present, possibly {}). Never secrets. |
title | English, fixed per code, for your logs. Do not show it to users or parse it. |
traceId | Log it, and quote it when you contact support. |
errors | Only on request.validation_failed: field paths mapped to lists of { code, args }. |
Validation errors
{
"code": "request.validation_failed",
"status": 400,
"errors": {
"email": [{ "code": "validation.email", "args": {} }],
"product": [{ "code": "validation.required", "args": {} }]
}
}Metered usage is the exception: each record is checked on its own, and a record's validation failure is reported in its result (outcome: "Refused" with error.code), not in errors.
Field codes include validation.required, validation.min_length, validation.max_length, validation.range, validation.min_items, validation.max_items, validation.email, validation.guid, validation.enum (with allowed), validation.timestamp, validation.one_of_required (with fields) and validation.unknown_parameter. Unknown JSON properties are refused (request.malformed_body), so mistakes surface early.
Statuses and what to do
| Status | Meaning | Retry? |
|---|---|---|
| 400 | The request is malformed or invalid. | No; fix the request. |
| 401 | Credentials missing, invalid, expired or revoked (auth.credentials_invalid). | After getting a new token or key. |
| 403 | Identified, but not allowed: scope, product, tenant. | No; check the API client's scopes and products. |
| 404 | Not found, or not in your workspace. | No. |
| 409 | Conflicts with the current state. | Possibly, after re-reading the state. |
| 412, 428 | Concurrency preconditions (If-Match). | After re-reading. |
| 422 | Breaks a business rule that does not depend on timing. | No. |
| 429 | Rate limited (request.rate_limited). | Yes, after Retry-After seconds. |
| 500 | Unexpected failure (server.error). | Yes, with backoff. |
| 502, 503 | A dependency (for example Microsoft) failed, or the service is unavailable. | Yes, with backoff. |
A 401 never says which part of the credential failed. Not-found and forbidden answers never reveal whether a resource exists in another partner's workspace.
Writing a robust client
- Branch on
code; fall back onstatusfor codes you do not know. New codes and newargsare additive changes and can appear at any time. - Retry
429,5xxand network failures with exponential backoff and jitter; honourRetry-After. - Send an
Idempotency-Key(a UUID per intent, reused on retries) where an endpoint honours one; a replay returns the stored answer withIdempotent-Replayed: true. - Log
traceIdwith every failure.