Errors
# Errors
Every error the API returns is an [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem
with content type `application/problem+json`. Nothing else in the server writes an error body.
## Shape
```json
{
"type": "https://homestead.app/problems/validation",
"title": "Some fields need attention",
"status": 400,
"code": "validation_failed",
"i18nKey": "errorValidationFailed",
"detail": "handle: must match platform pattern",
"fields": [{ "path": "entries[2].handle", "code": "pattern", "i18nKey": "errorFieldPattern" }]
}
```
| Field | Meaning |
| --- | --- |
| `type` | A URI for the problem type: `https://homestead.app/problems/` plus the code with dashes. `validation_failed` and `unknown_field` share `…/problems/validation`. |
| `title` | A short human summary. Don't branch on it. |
| `status` | The HTTP status, repeated. |
| `code` | **Stable and machine-readable.** Branch on this. |
| `i18nKey` | A key in `i18n/*/common.arb` to show a localised message: `"error"` plus the code in PascalCase when that key exists (`rate_limited` becomes `errorRateLimited`), otherwise `errorGeneric`. |
| `detail` | Optional extra detail, for logs and developers. |
| `fields` | Optional per-field problems, with a JSON-style `path`, a `code` and an `i18nKey`. |
## Handling errors well
- Branch on `code`, never on `title` or `detail`.
- Show the localised message for `i18nKey`. Apps fall back to `errorGeneric`.
- On `429 rate_limited`, wait for the `Retry-After` header (seconds) before trying again.
- On `409 idempotency_conflict`, you reused an `Idempotency-Key` with a different body. Use a
new key for a new request.
- On `501 not_implemented`, the operation is in the spec but not built yet on this server.
## Stable codes
| Code | When |
| --- | --- |
| `validation_failed` | The request body or parameters don't match the schema (`400`). |
| `unknown_field` | The request has a field the schema doesn't define (`400`). |
| `unauthenticated` | No valid credentials (`401`). |
| `forbidden` | Signed in, but the role isn't enough, or a cookie write failed the CSRF or origin check (`403`). |
| `agent_not_allowed` | An agent token called an operation agents can't use (`403`). |
| `scope_missing` | The agent token lacks the scope this operation needs (`403`). |
| `not_found` | The thing doesn't exist (`404`). |
| `conflict` | The request conflicts with the current state (`409`). |
| `already_exists` | The account or item already exists. |
| `setup_required` | The household hasn't been set up yet. |
| `setup_done` | Setup was already run. |
| `rate_limited` | Too many requests (`429`, with `Retry-After`). |
| `upstream_unavailable` | A platform or other upstream service didn't answer. |
| `platform_unsupported` | The platform isn't in the registry. |
| `share_expired` | The share link has expired. |
| `share_revoked` | The share link was revoked. |
| `invite_invalid` | The invite code is wrong, used up or expired. |
| `lease_expired` | An agent completed a task after its 10-minute lease ran out. |
| `idempotency_conflict` | Same `Idempotency-Key`, different body (`409`). |
| `payload_too_large` | The upload or body is over the limit (`413`). |
| `circle_full` | The write would put more than 30 capturable accounts in a circle (`409`). |
| `capture_closed` | The capture no longer accepts uploads. |
| `upload_token_invalid` | The `hsu_` upload token is wrong, expired or for another capture. |
| `not_implemented` | The operation isn't built yet (`501`). |
| `internal` | Something went wrong on the server. Check its logs. |
## Codes inside successful replies
Some operations report per-item problems inside a `200` reply rather than failing the whole
request:
- **Ingest** (`POST /ingest`) stores the valid items and lists the others under `rejected`, each
with an `index` and a `code`: `invalid_kind`, `invalid_url`, `media_host_not_allowed`,
`media_fetch_failed` or `too_old`.
- **Quick-add** (`POST /quick-add`) lists entries it didn't add under `skipped`, for example with
code `already_exists` and the `existingAccountId`.
- **Import preview** (`POST /imports/preview`) gives per-person `warnings`:
`platform_unsupported`, `invalid_handle`, `invalid_profile_url` or `no_accounts`.
Every error the API returns is an RFC 9457 problem
with content type application/problem+json. Nothing else in the server writes an error body.
{ "type": "https://homestead.app/problems/validation", "title": "Some fields need attention", "status": 400, "code": "validation_failed", "i18nKey": "errorValidationFailed", "detail": "handle: must match platform pattern", "fields": [{ "path": "entries[2].handle", "code": "pattern", "i18nKey": "errorFieldPattern" }]}| Field | Meaning |
|---|---|
type | A URI for the problem type: https://homestead.app/problems/ plus the code with dashes. validation_failed and unknown_field share …/problems/validation. |
title | A short human summary. Don’t branch on it. |
status | The HTTP status, repeated. |
code | Stable and machine-readable. Branch on this. |
i18nKey | A key in i18n/*/common.arb to show a localised message: "error" plus the code in PascalCase when that key exists (rate_limited becomes errorRateLimited), otherwise errorGeneric. |
detail | Optional extra detail, for logs and developers. |
fields | Optional per-field problems, with a JSON-style path, a code and an i18nKey. |
Handling errors well
Section titled “Handling errors well”- Branch on
code, never ontitleordetail. - Show the localised message for
i18nKey. Apps fall back toerrorGeneric. - On
429 rate_limited, wait for theRetry-Afterheader (seconds) before trying again. - On
409 idempotency_conflict, you reused anIdempotency-Keywith a different body. Use a new key for a new request. - On
501 not_implemented, the operation is in the spec but not built yet on this server.
Stable codes
Section titled “Stable codes”| Code | When |
|---|---|
validation_failed | The request body or parameters don’t match the schema (400). |
unknown_field | The request has a field the schema doesn’t define (400). |
unauthenticated | No valid credentials (401). |
forbidden | Signed in, but the role isn’t enough, or a cookie write failed the CSRF or origin check (403). |
agent_not_allowed | An agent token called an operation agents can’t use (403). |
scope_missing | The agent token lacks the scope this operation needs (403). |
not_found | The thing doesn’t exist (404). |
conflict | The request conflicts with the current state (409). |
already_exists | The account or item already exists. |
setup_required | The household hasn’t been set up yet. |
setup_done | Setup was already run. |
rate_limited | Too many requests (429, with Retry-After). |
upstream_unavailable | A platform or other upstream service didn’t answer. |
platform_unsupported | The platform isn’t in the registry. |
share_expired | The share link has expired. |
share_revoked | The share link was revoked. |
invite_invalid | The invite code is wrong, used up or expired. |
lease_expired | An agent completed a task after its 10-minute lease ran out. |
idempotency_conflict | Same Idempotency-Key, different body (409). |
payload_too_large | The upload or body is over the limit (413). |
circle_full | The write would put more than 30 capturable accounts in a circle (409). |
capture_closed | The capture no longer accepts uploads. |
upload_token_invalid | The hsu_ upload token is wrong, expired or for another capture. |
not_implemented | The operation isn’t built yet (501). |
internal | Something went wrong on the server. Check its logs. |
Codes inside successful replies
Section titled “Codes inside successful replies”Some operations report per-item problems inside a 200 reply rather than failing the whole
request:
- Ingest (
POST /ingest) stores the valid items and lists the others underrejected, each with anindexand acode:invalid_kind,invalid_url,media_host_not_allowed,media_fetch_failedortoo_old. - Quick-add (
POST /quick-add) lists entries it didn’t add underskipped, for example with codealready_existsand theexistingAccountId. - Import preview (
POST /imports/preview) gives per-personwarnings:platform_unsupported,invalid_handle,invalid_profile_urlorno_accounts.