> ## Documentation index
> Fetch the complete documentation index at: https://docs.htspilot.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The error body of the HTS Pilot API with its machine-readable code, the status codes in use, validation errors, plan refusals and how messages follow Accept-Language.

An error is answered with an HTTP status of `4xx` or `5xx` and a JSON body. It is never a `200` with an error
inside.

## The error body

```json
{ "detail": "Lookup not found", "code": "not_found" }
```

| Field | Type | Meaning |
| --- | --- | --- |
| `detail` | string | What went wrong, written for a person, in the language of the request |
| `code` | string | What went wrong, for a program. In every error the API writes |
| `request_id` | string | Only on a `500`: the value of the `X-Request-ID` header of the answer |
| `details` | object | More about the error, when there is more to say. Absent otherwise |
| `errors` | array | Only on a `422` for invalid input: one entry per invalid field, see below |

Branch on `code` and show `detail`. `code` is lower snake case, the same in every language, and is not changed
once it is published. `detail` is a sentence: it is translated ([Languages](/languages.md)) and its wording can
change. Every answer also carries the header `X-Request-ID` ([Request identifiers](/request-identifiers.md)).

## Error codes

Most errors carry the code of their status. A few are more specific:

| `code` | Status | Meaning |
| --- | --- | --- |
| `bad_request` | `400` | The request cannot be carried out as it was sent |
| `unauthenticated` | `401` | No valid API key was sent, or it was not accepted |
| `payment_required` | `402` | The call needs something the account has not paid for |
| `out_of_credits` | `402` | The credits of the account are used up |
| `forbidden` | `403` | The call is refused for this caller, usually because of its role |
| `plan_feature` | `403` | The plan of the account does not include this function. `details.key` names it |
| `plan_limit` | `403` | The call goes over a limit of the plan. `details` has the limit and what was asked |
| `not_found` | `404` | The address or the record does not exist, or belongs to another user |
| `method_not_allowed` | `405` | The address exists and does not take this method |
| `conflict` | `409` | The state of the record does not allow the call |
| `payload_too_large` | `413` | The body or the file sent is larger than the API accepts |
| `validation_error` | `422` | The input is not valid. `errors` or `details` say which field |
| `rate_limited` | `429` | Too many calls in a short time. `Retry-After`, when the answer has it, says how many seconds to wait |
| `internal_error` | `500` | An unexpected failure. The body also carries `request_id` |
| `upstream_error` | `502` | A service this API depends on failed or did not answer |
| `service_unavailable` | `503` | The function is not available at the moment |

More specific codes may be added later ([Versioning and changes](/versioning.md)). Treat a code you do not know
like the status it came with.

An answer produced before the request reached the API (a `502`, `503` or `504` during an outage or a release, or
a refusal at the edge of the network) may have another body, or none, and no `code`: fall back on the HTTP
status.

## Status codes

| Status | When |
| --- | --- |
| `200` | The call succeeded |
| `202` | A lookup was created and is being classified in the background ([Asynchronous work](/asynchronous-work.md)) |
| `400` | The request cannot be processed as sent |
| `401` | No valid API key was sent ([Authentication](/authentication.md)) |
| `402` | The account that owns the work cannot be charged for it. Whether this can occur depends on the account ([Rate limits and quotas](/rate-limits.md#credits)) |
| `403` | The role of the key, or the plan of the account, does not allow the call |
| `404` | The object does not exist, or belongs to another account and the key has the `entry` role |
| `405` | The address exists and does not take this method |
| `409` | The state of the object does not allow the call: a batch that is already running, a decision that is not there to reopen |
| `413` | The body or the file sent is larger than the API accepts |
| `422` | The input is not valid: a field failed validation, or a rule of the operation was not met |
| `429` | Too many requests of a kind that is limited per minute |
| `500` | An unexpected error on our side. The body carries a general message, `code` and `request_id` |
| `502` | A service the call depends on did not answer: the classification engine or an official source |
| `503` | A function is not available at the moment |

A lookup that fails while it is being classified is not an HTTP error: the lookup ends with the status `error`
and a message in its `error` field ([Asynchronous work](/asynchronous-work.md)).

## Validation errors

When a field of the request fails validation, the answer is `422` with one entry per field:

```json
{
  "detail": "Invalid input",
  "code": "validation_error",
  "errors": [
    { "field": "description", "message": "String should have at least 1 character" }
  ]
}
```

`field` is the path of the field in the request (`items.0.sku` for the first item of a list). The `message` of
an entry comes from the validation library and is in English.

When a rule of the operation is not met, the answer is also `422`, with `detail` alone or with `details`:

```json
{ "detail": "Enter the code to finalize", "code": "validation_error" }
```

A webhook URL that is refused names the field and the reason:
`"details": { "field": "url", "reason": "webhook_private" }`.

## State conflicts

```json
{ "detail": "The lookup is not finalized", "code": "conflict" }
```

`409` means the request was understood and the object is in a state that does not allow it. Read the object
again before deciding what to do.

## Plan refusals

A feature the plan of the account does not include, or a limit it would exceed, answers `403` with the code
`plan_feature` or `plan_limit` and a `details` object that names the entitlement. The code is repeated in
`details.code`, where it was before `code` existed:

| `code` | Fields of `details` | Meaning |
| --- | --- | --- |
| `plan_feature` | `key` | The plan does not include the feature `key` (for example `feature.webhooks`) |
| `plan_limit` | `key`, `limit`, `requested` | The call would make `requested`, and the plan allows `limit` (for example `batch_max_rows`) |

`GET /api/plans/me` lists what the plan allows ([Usage](/usage.md)).

## Language of the messages

`detail` follows the `Accept-Language` header of the request, and the answer names its language in
`Content-Language`:

```bash
curl -i https://htspilot.com/api/usage/me -H "Accept-Language: vi"
```

```
HTTP/1.1 401 Unauthorized
Content-Language: vi
X-Request-ID: fbefced8d1434ceb9407bf612a8b008b

{"detail":"Vui lòng đăng nhập","code":"unauthenticated"}
```

## Handling errors

- Treat `401` and `403` as configuration problems: do not retry them.
- `402`: when it occurs, stop sending work that creates lookups until the account can be charged again. Do not
  retry it.
- `404`, `409`, `422`: fix the request; the same request will fail again.
- `429`: wait a minute, or the number of seconds in `Retry-After` when the header is there, then retry.
- `500`, `502`, `503`, `504` and network errors: retry with a growing pause, and log `X-Request-ID`. Reads are safe to repeat. Before repeating a call
  that creates something, check whether the first one went through: there is no idempotency key, so a repeated
  `POST /api/classify` creates a second lookup, which can be charged a second time. `POST /api/skus`, and
  `POST /api/skus/bulk` without `classify_missing`, update an existing SKU and are safe to repeat; with
  `classify_missing`, a repeat classifies again every SKU that still has no code.
