Errors¶
The User API uses conventional HTTP status codes and returns a consistent, machine-readable error body so you can handle failures programmatically.
Error envelope¶
Failed requests return a JSON body with a stable shape:
{
"error": "Insufficient scope or role for this resource",
"code": "insufficient_scope",
"request_id": "abc123xyz"
}
| Field | Description |
|---|---|
error | Human-readable description of what went wrong. For display and logging. |
code | Stable, machine-readable identifier. Branch on this, never on error. |
request_id | Correlates the failure with server-side logs. Include it in support requests. |
message | Deprecated. Mirrors error for backward compatibility; will be removed in a future major version. Prefer error. |
Always match on code, not error
The error/message text is meant for humans and may be reworded at any time. Only code and the HTTP status are contractual.
Rate-limit responses use a compact envelope
The rate limiter returns 429 with a slightly different body — {"code": "rate_limit_exceeded", "message": ..., "request_id": ...} (no error field). Everywhere else the envelope above applies. In both cases, code and request_id are present and reliable.
Common status codes¶
| Status | Typical code | Meaning |
|---|---|---|
400 | bad_request, validation_error | The request was malformed or failed validation. |
401 | unauthorized | No valid credential was resolved. Authenticate and retry. |
401 | invalid_token | The bearer token is missing, malformed, expired, or revoked. |
403 | insufficient_scope | The credential authenticated but lacks the required scope or role. |
404 | not_found | The resource does not exist, or you cannot see it. |
409 | conflict | The request conflicts with the current state of the resource. |
429 | rate_limit_exceeded | You exceeded your rate limit. Honor Retry-After. |
429 | too_many_requests | Too many concurrent event-stream connections. |
500 | internal_server_error | An unexpected error occurred on our side. Safe to retry with backoff. |
Authentication vs. authorization¶
Two failure modes are easy to confuse — the code tells them apart:
401 invalid_token/401 unauthorized— Authentication failed. Your token was not accepted (missing, expired, revoked, or not a recognized first-party credential). The browser session cookie is not a valid credential on the User API; only first-party tokens (cto_at_v1_,cto_pat_v1_,cto_wk_v1_) authenticate here.403 insufficient_scope— Authorization failed. Your token is valid, but it does not carry the scope required for the operation. Mint a new credential with the needed scope (within the limits of your own grants).
Handling errors¶
resp = session.get("https://api.contio.ai/v1/meetings", headers=headers)
if not resp.ok:
body = resp.json()
code = body.get("code")
if code == "invalid_token":
refresh_or_reauthenticate()
elif code == "insufficient_scope":
raise PermissionError(f"Missing scope (request_id={body.get('request_id')})")
elif resp.status_code == 429:
backoff(resp.headers.get("Retry-After"))
else:
log.error("API error %s: %s (request_id=%s)",
code, body.get("error"), body.get("request_id"))
Always capture request_id
Logging request_id on every failure makes support investigations dramatically faster — it points our team straight to the corresponding server-side log entry.
Retries & idempotency¶
Retrying a failed request is often the right thing to do — but be aware of what is safe to repeat:
GET,PUT,PATCH, andDELETEare safe to retry. Repeating them with the same input produces the same end state (aDELETEof an already-deleted resource is a harmless no-op).POSTrequests that create a resource support de-duplication withIdempotency-Key. Include a unique, client-generated key in theIdempotency-Keyheader on eligiblePOSTendpoints. The first successful (2xx) response is cached; laterPOSTrequests with the same key, the same body, and the same route replay that response instead of re-executing the operation.
Eligible POST endpoints¶
The Idempotency-Key header is honored on:
POST /v1/meetingsPOST /v1/action-itemsPOST /v1/agenda-itemsPOST /v1/meetings/{id}/participantsPOST /v1/workspace/webhooks/{id}/re-enable
It is not honored on POST /v1/meetings/{id}/context (multipart uploads) or on PUT, PATCH, and DELETE requests.
Key format¶
Keys must be between 1 and 255 printable ASCII characters. Empty, whitespace-only, or non-ASCII keys return 400 with code idempotency_key_invalid.
Idempotency-specific responses¶
| Status | code | Meaning |
|---|---|---|
201 / 200 | (success) | First request completed. Repeat requests with the same key and body return the same response and include Idempotent-Replayed: true. |
400 | idempotency_key_invalid | The supplied key is empty, whitespace-only, too long, or contains non-printable characters. |
409 | idempotency_conflict | Another request with the same key is currently being processed. Retry after the interval in the Retry-After response header. |
422 | idempotency_key_reuse | The key has already been used, but the request body (or route) differs from the original. Keys cannot be reused with different payloads. |
Replaying a request
To replay a cached response, send an identical Idempotency-Key header and an identical request body. Any change to the body, route, or HTTP method is treated as a different request and returns 422 idempotency_key_reuse.
For throttling-driven retries (429), always honor Retry-After — see Rate Limits.
Timestamps & time zones¶
All timestamps in User API responses are RFC 3339 strings with an explicit UTC offset (Z / +00:00) — for example 2023-01-01T10:00:00Z. Because every timestamp carries its offset, the instant is unambiguous and you can convert it to any local time zone on the client.
- Request-body timestamps you send (e.g. a meeting
start_time) should likewise be RFC 3339 with an explicit offset. Values without an offset are interpreted as UTC. - Date-range query parameters (e.g.
start_date/end_dateon calendar endpoints) expect RFC 3339 as well. - The API does not read an
X-Client-Timezoneheader; localize for display in your own client.
Next steps¶
- Rate Limits — budgets, headers, and backoff
- Pagination — the list-response convention
- Getting Started — credentials and scopes