# 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:

```json
{
  "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):

```json
{
  "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:

```json
{
  "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:

```http
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`](#missing_scope-details) |
| `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](/docs/api/api#rate-limiting) for the full table.

```http
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
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 available
- `Retry-After` — seconds to wait after a `429`; honor this over `Reset`
- `RateLimit-Policy` / `RateLimit` — IETF draft equivalents (`q`/`w` and `r`; `t` only when `r` is 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

1. Check HTTP status, then `ok` / `error`.
2. Treat `token_revoked` as terminal for that credential.
3. On `missing_scope`, use `needed` / `provided` for re-consent — do not parse prose.
4. Echo `nextCursor` values unchanged; never invent cursors.
5. Tolerate new response fields (additive changes within a dated API version).