# Errors

> How the API reports errors as RFC 9457 Problem Details with stable codes and a request id.

Every error response has a `4xx` or `5xx` status and a body of type `application/problem+json` (RFC 9457
Problem Details). Your code decides by the HTTP status and the stable `code`; the texts are for people.

## Members of a problem

- **type** `string` (required): Problem type. `about:blank` unless a specific type is documented.
- **title** `string` (required): Short summary; for `about:blank` the HTTP status phrase, e.g. `Bad Request`.
- **status** `integer` (required): The HTTP status code, repeated in the body.
- **code** `string` (required): Stable, machine-readable code in snake_case, e.g. `validation_error`. See [Codes](#codes).
- **detail** `string`: Explanation of this occurrence, in English. Show it in logs, do not parse it.
- **instance** `string`: URI reference of the request that caused the problem, when present.
- **request_id** `string`: The id of the request, equal to the `X-Request-Id` response header. Quote it when you contact support.
- **errors** `object[]`: Field-level details for `validation_error`, and for `conflict` when a value must be unique.

  **field error**
  - **field** `string` (required): The body field (dotted, with list positions, e.g. `values[0].measured_at`), the query parameter or
    header name (e.g. `limit`), or `body` when the body as a whole is invalid (malformed JSON, an unknown
    property, a wrong content type).
  - **code** `string` (required): Stable reason code, see [Field error codes](#field-error-codes).
  - **message** `string` (required): Explanation in English; localise by `code`.

## Field error codes

| Code                  | Meaning                                                                                                            |
| --------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `required`            | A required field, parameter or header is missing.                                                                  |
| `too_long`            | A text is longer than allowed.                                                                                     |
| `too_short`           | A text is shorter than allowed.                                                                                    |
| `out_of_range`        | A number, date or time lies outside the allowed range, e.g. a `measured_at` in the future.                         |
| `invalid_format`      | The value has the wrong format (date, UUID, pattern), or the body is not valid JSON or not `application/json`.     |
| `invalid_check_digit` | The check digit of a GTIN or GLN is wrong.                                                                         |
| `invalid_value`       | The value is not allowed here: an unknown enum value, an invalid parameter or cursor, a failing cross-field check. |
| `unknown_field`       | The field is not known here, e.g. a field that is not condition data of the unit, or an unknown body property.     |
| `already_exists`      | A value that must be unique is already taken (with `conflict`).                                                    |

## Examples

A condition update with a measurement time in the future
([`updateCondition`](https://passport.kabin.sh/docs/en/api-reference/lifecycle/update-condition)) is refused as a whole:

```json title="400"
{
  "type": "about:blank",
  "title": "Bad Request",
  "status": 400,
  "code": "validation_error",
  "detail": "The request failed validation.",
  "request_id": "4f9c2a7d1e0b5c8a3f6e2d19",
  "errors": [
    {
      "field": "values[0].measured_at",
      "code": "out_of_range",
      "message": "measured_at lies in the future"
    }
  ]
}
```

Too many requests: the `Retry-After` header says how many seconds to wait.

```http title="429"
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 17
X-Request-Id: 8b1e6f03c2d94a7e5f0a1b2c

{
  "type": "about:blank",
  "title": "Too Many Requests",
  "status": 429,
  "code": "rate_limited",
  "detail": "Too many attempts. Try again later.",
  "request_id": "8b1e6f03c2d94a7e5f0a1b2c"
}
```

## Codes

The `ProblemCode` values of the published contract. Codes marked as portal only do not occur with an API
key; they are listed because the contract contains them.

| Code                         | HTTP status | Meaning                                                                                                                                  |
| ---------------------------- | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `validation_error`           | 400         | The request is invalid. \`errors\[]\` names each affected field, query parameter or header with a stable error code.                     |
| `invalid_token`              | 400         | Portal only: an email or invitation link is unknown, used or expired.                                                                    |
| `unauthenticated`            | 401         | No valid API key: the header is missing, the key is unknown or revoked, or the request also carries a session cookie.                    |
| `invalid_credentials`        | 401         | Portal only: the email address or password of a sign-in is wrong.                                                                        |
| `forbidden`                  | 403         | The key lacks the scope the operation needs, or a viewer's rights are not enough.                                                        |
| `email_not_verified`         | 403         | Portal only: the email address is not verified yet.                                                                                      |
| `not_found`                  | 404         | The resource does not exist, or it belongs to another organisation.                                                                      |
| `method_not_allowed`         | 405         | The path exists, but not with this HTTP method.                                                                                          |
| `conflict`                   | 409         | The request contradicts the current state, for example the unit's battery model is archived: its data can be read but no longer changed. |
| `identifiers_locked`         | 409         | Portal only: the identifiers of a published unit can no longer change.                                                                   |
| `payload_too_large`          | 413         | The request body is larger than allowed (1 MiB by default).                                                                              |
| `publish_blocked`            | 422         | Portal only: publishing fails on open required data; \`blockers\[]\` lists it.                                                           |
| `rate_limited`               | 429         | Too many requests. Wait the seconds given in the \`Retry-After\` header, then try again.                                                 |
| `request_closed`             | 410         | Supplier and service-partner links only: the link expired, was cancelled or has been used.                                               |
| `internal_error`             | 500         | An error on our side. Retry later; quote the \`request_id\` when you contact support.                                                    |
| `address_lookup_unavailable` | 503         | Portal only: the address search is not reachable right now; the address can be entered by hand.                                          |

## Request id

Every response, successful or not, carries an `X-Request-Id` header. In a problem, `request_id` has the same
value. The server creates the id itself and ignores an id you send. Log it with each failed request and quote
it when you contact [support](https://passport.kabin.sh/docs/en/general/support).

## Handling errors

- Decide by `status` and `code`, never by `title`, `detail` or `message`: those texts can change.
- Expect new codes over time. Treat an unknown `code` by its HTTP status class.
- Fix `400`, `401`, `403`, `404` and `409` instead of repeating the request unchanged.
- Retry `429` after `Retry-After`, and `5xx` or network errors with exponential backoff. Before you repeat
  a write, read [Retries and idempotency](https://passport.kabin.sh/docs/en/idempotency).
- Responses never contain SQL, stack traces or upstream errors; an unexpected failure is `500 internal_error`
  with a request id.

The guide [Handle errors](https://passport.kabin.sh/docs/en/guides/handle-errors) shows complete error handling in TypeScript, Python
and Go.
