# API errors

Every failure uses the same envelope, with a code from the fixed set below. Branch on `error.code`, log
`requestId`, and never parse `error.message`; it is written for your logs and is not stable.

```json
{
  "ok": false,
  "error": {
    "code": "GONE",
    "message": "Share token has expired",
    "requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
  }
}
```

> **Warning: Check ok, not the HTTP status**
>
> One error is returned with a 2xx status: a record that has not finished rendering comes back as **`202`
> with `ok: false`**. Clients built on a library that treats any 2xx as success (`fetch`'s `response.ok`,
> for one) will log a success and then find no data. Test `body.ok` on every response.

## Codes

| Status | Code | Cause | Retry? |
|---|---|---|---|
| 202 | `CDR_PENDING` | The record exists but its visual record has not finished rendering. Only on the share-receive path. | Yes, after `Retry-After` |
| 400 | `INVALID_ARGUMENT` | A malformed request: an unrecognized field in a request body, `metadataKey` without `metadataValue`, a metadata key and value pair too long to index, an unusable `pageToken`, a non-boolean `collect`, acceptance criteria that failed validation, an acceptance phone number that is not a normalizable North American number, or a share token your own organization created. | No |
| 401 | `UNAUTHENTICATED` | Missing, malformed, unknown, or revoked API key. | No |
| 403 | `API_ACCESS_DISABLED` | Your organization is not enabled for the server-to-server API. | No |
| 403 | `PASSTHROUGH_ORG_CANNOT_COLLECT` | Your organization has no billing set up, so it cannot collect. Only reachable on such an organization that was granted API access explicitly; otherwise `API_ACCESS_DISABLED` comes first. | No |
| 403 | `PASSTHROUGH_ORG_CANNOT_CLAIM` | Your organization has no billing set up, so it cannot receive shared records. Same reachability caveat. | No |
| 404 | `NOT_FOUND` | No such record or token for your organization, an organization record we could not read, or (before authentication) an unknown path. Also what a record that is still rendering returns on a direct read. | Sometimes |
| 405 | `METHOD_NOT_ALLOWED` | Right path, wrong HTTP method. The `Allow` header lists what it accepts. | No |
| 409 | `CONFLICT` | Two documents on our side share one identifier, either a record or an API key. A data problem, not something you caused. | No, tell us |
| 410 | `GONE` | The share token has expired. Ask the sender for a new one. | No |
| 410 | `CDR_INVALID` | The record was flagged invalid. No organization can read, collect, or share it. | No |
| 422 | `CDR_ACCEPTANCE_FAILED` | The record did not meet the `acceptance` requirements you sent. `details.failures[]` says which. | No |
| 500 | `INTERNAL` | Our fault. | Yes, with backoff |

Every code in that table is reachable. There are no others: the published error type contains exactly
these, so a `switch` over them is exhaustive.

## Structured detail

Two codes carry `error.details`. The rest carry none.

**`details.issues`** (`array`, optional)

On `INVALID_ARGUMENT` from a rejected request body. Each entry is `{ path, message }`, with `path` a
dotted route to the field at fault: `disclosure.requiredText.0.ruleId` for a bad acceptance rule, or
the key itself for an unrecognized field.

**`details.failures`** (`array`, optional)

On `CDR_ACCEPTANCE_FAILED`. Each entry is `{ ruleId, status, code }` naming a requirement the record
did not satisfy. See [acceptance criteria](https://app.expressconsent.com/docs/reference/api/acceptance).

**`details.outcome`** (`string`, optional)

On `CDR_ACCEPTANCE_FAILED`, alongside `failures`. Always the string `rejected`. Branch on the error
code rather than on this.

## What to retry

**`CDR_PENDING`** is the one worth building for. Wait for the interval in the `Retry-After` header, which is 2
seconds, and repeat the request. Records are usually readable within a few seconds of capture, so a
handful of attempts is enough; if it persists for a minute, treat it as a failure and tell us.

**`404` on a record captured moments ago** is the same situation wearing a different status. Reading a record
directly does not distinguish "not finished rendering" from "never existed", so allow a few seconds before
concluding a `cdrId` is bad.

**`500`** deserves a retry with exponential backoff. Everything else is definitive: the same request will
fail the same way, and retrying it is wasted work.

## Authentication failures

Every rejected key returns `401 UNAUTHENTICATED`, whether it was missing, malformed, unrecognized, or
revoked. That is deliberate, since the response does not help someone find out which of their guesses was
closest, but it means the code alone will not debug it for you. Check that the header is `X-API-Key`, that
the value is the key identifier and secret joined by a single dot, and that the key has not been revoked.

`403 API_ACCESS_DISABLED` is different, and is about the organization rather than the key: the key is valid
and the account is not enabled for API access. Passthrough organizations start this way, which is why
they see this error on every endpoint rather than the more specific passthrough codes. Talk
to us to have it enabled.

A revoked key can keep working for up to five minutes.

## Two kinds of 404

A `404` has two sources, and they differ in whether your key was checked.

An **unknown path** is rejected by the router before authentication, so a typo in the URL returns `404`
whether your key is good, bad, or absent. That `404` tells you nothing about your key.

A **valid path with no matching record** is the opposite: authentication ran and passed first. A bad key
would have returned `401` and an organization without API access `403`, so this `404` is genuinely about the
record: it does not exist, it does not belong to your organization, or it has not finished rendering. Check
the path against [the CDR endpoints](https://app.expressconsent.com/docs/reference/api/cdrs); if the path is right, the problem is the
identifier rather than the key.
