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.
{
"ok": false,
"error": {
"code": "GONE",
"message": "Share token has expired",
"requestId": "3f2504e0-4f89-11d3-9a0c-0305e82c3301"
}
}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.issuesarrayoptionalOn
INVALID_ARGUMENTfrom a rejected request body. Each entry is{ path, message }, withpatha dotted route to the field at fault:disclosure.requiredText.0.ruleIdfor a bad acceptance rule, or the key itself for an unrecognized field.details.failuresarrayoptionalOn
CDR_ACCEPTANCE_FAILED. Each entry is{ ruleId, status, code }naming a requirement the record did not satisfy. See acceptance criteria.details.outcomestringoptionalOn
CDR_ACCEPTANCE_FAILED, alongsidefailures. Always the stringrejected. 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; if the path is right, the problem is the
identifier rather than the key.