Responses and Errors
How every v1 REST response is shaped — success envelope, pagination cursors, and machine-readable errors.
Success envelope
Every successful JSON response on /v1/... includes "ok": true as the first
field of the top-level object:
{
"ok": true,
"chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
"timestamp": 1765602474760032
}
You can branch on body.ok before reading the rest of the payload. Non-JSON
bodies (for example meeting.transcript when requested as WebVTT) and 204
responses are not wrapped.
Request correlation uses the Request-Id response header (not a body field).
Error envelope
Error responses use the inverse envelope. The error field is a machine-readable
catalog code (not a free-text sentence):
{
"ok": false,
"error": "invalid_token"
}
HTTP status codes still apply (400, 401, 403, 404, 429, 500, …).
Branch on error rather than parsing human language.
missing_scope details
When the token lacks a required scope, the body includes structured fields so you can drive a re-auth / re-consent flow without parsing prose:
{
"ok": false,
"error": "missing_scope",
"needed": ["chat:send_message", "chat:write"],
"provided": ["chat:read", "chat:history", "group:read"]
}
| Field | Meaning |
|---|---|
needed | Scopes that would satisfy the check. Any-of (OR): holding any one element is enough. |
provided | The token's granted scopes after alias normalization (e.g. legacy groups:read appears as group:read). For personal tokens this is the expanded OAuth set, not the pat:* group names. |
When an endpoint requires several scopes together (AND), the API still uses
any-of needed and reports one missing scope at a time. Grant that scope,
retry, and the next response names the next missing scope. Do not assume
needed is a complete all-of checklist.
Slack porting note: Slack returns needed / provided as comma-separated
strings on missing_scope. Roam returns string arrays, and needed is
explicitly any-of (matching endpoints that accept alternate scopes such as
chat:send_message or chat:write). Capability parity — not wire compatibility.
Auth failures
| Code | Meaning | Client action |
|---|---|---|
not_authed | No bearer token | Attach a token |
invalid_token | Unknown, malformed, or expired | Obtain a new token |
token_revoked | Permanently unusable (e.g. owner archived) | Discard token; re-authenticate — do not retry |
invalid_token and token_revoked also send:
WWW-Authenticate: Bearer error="invalid_token"
Common codes
| Code | Typical status | Meaning |
|---|---|---|
invalid_arguments / invalid_parameter / missing_parameter / invalid_json | 400 | Bad request |
invalid_cursor | 400 | Pagination cursor invalid or expired — restart without a cursor |
invalid_expand | 400 | Unrecognized expand= field |
missing_scope | 403 | Token lacks a required scope — see needed / provided |
access_mode_not_supported | 403 | Personal vs organization access cannot use this endpoint |
not_in_chat / not_in_group | 403 | Caller is not a member |
*_not_found (e.g. chat_not_found, user_not_found) | 404 | Resource missing or invisible |
msg_too_long | 413 | Message text too long |
ratelimited | 429 | Rate limit; honor Retry-After |
transcript_pending | 404 | Transcript not ready; retry later |
transcript_unavailable | 404 | Meeting was not transcribed; stop retrying |
upstream_timeout | 504 | Upstream timeout; retry |
internal_error | 500 | Unexpected server failure |
Rate-limit headers
Every response carries burst-bucket headers. Retry-After is sent only on
429. See Rate Limiting for the full table.
HTTP/1.1 200 OK
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1776556801
RateLimit-Policy: "burst";q=10;w=1
RateLimit: "burst";r=7
HTTP/1.1 429 Too Many Requests
Retry-After: 10
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1776556810
RateLimit-Policy: "burst";q=10;w=1
RateLimit: "burst";r=0;t=10
{"ok":false,"error":"ratelimited"}
X-RateLimit-Limit— burst size (10)X-RateLimit-Remaining— tokens left (0–10)X-RateLimit-Reset— Unix epoch seconds (UTC) when another burst token is availableRetry-After— seconds to wait after a429; honor this overResetRateLimit-Policy/RateLimit— IETF draft equivalents (q/wandr;tonly whenris 0)
PATs add a "day" policy on RateLimit-Policy / RateLimit for the
1000/day quota. X-RateLimit-* is always the burst bucket.
Remaining is a per-process hint (appserver replicas do not share counters).
Pagination cursors
List endpoints that support cursor pagination expose:
| Field | Where | Meaning |
|---|---|---|
cursor | request query/body | Resume token from a prior page |
nextCursor | response | Pass as cursor on the next request; omitted on the last page |
Cursors are opaque. Do not parse, construct, or reuse them across endpoints
or API versions. An invalid or expired cursor returns 400 with
error: "invalid_cursor" — restart pagination from the beginning (no cursor).
Date-range list endpoints may still use after / before in addition to or
instead of cursors; see each endpoint.
Client checklist
- Check HTTP status, then
ok/error. - Treat
token_revokedas terminal for that credential. - On
missing_scope, useneeded/providedfor re-consent — do not parse prose. - Echo
nextCursorvalues unchanged; never invent cursors. - Tolerate new response fields (additive changes within a dated API version).