Skip to content

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 unauthorizedAuthentication 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_scopeAuthorization 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, and DELETE are safe to retry. Repeating them with the same input produces the same end state (a DELETE of an already-deleted resource is a harmless no-op).
  • POST requests that create a resource support de-duplication with Idempotency-Key. Include a unique, client-generated key in the Idempotency-Key header on eligible POST endpoints. The first successful (2xx) response is cached; later POST requests 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/meetings
  • POST /v1/action-items
  • POST /v1/agenda-items
  • POST /v1/meetings/{id}/participants
  • POST /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_date on calendar endpoints) expect RFC 3339 as well.
  • The API does not read an X-Client-Timezone header; localize for display in your own client.

Next steps