# Errors

## Error response shape

Every error is returned as JSON in the same shape:

```json
{
  "error": "Lead not found or access denied",
  "details": "...",
  "status": 404
}
```

| Field | Always? | Description |
|---|---|---|
| `error` | Yes | A short, stable description of the error. Safe to rely on in logs and alerts. |
| `status` | Yes | The HTTP status code, identical to the response status. |
| `details` | No | Extra explanation, when available. |
| `reason` | On 401 and 503, and some 403s | On 401, why authentication failed; on 503, `auth_service_error`. On 403, `subscription_inactive` when the organization's subscription is not active, or `note_locked` when a locked note can be changed only by its creator or an admin key. See [Authentication & permissions](https://docs.banta.co.il/guides/authentication.md). |
| `retry_after` | On 429 | When the next request window opens (ISO 8601). The `Retry-After` header carries the same wait in seconds. |
| `code` | Sometimes | A database error code, when it is safe to expose: on 400 (e.g. `23514` for a status the organization does not define), on 409 (`23505`, a duplicate value) and on 500. |

Some errors add context fields, for example `existing_link_id` when you try to link a contact that is already linked to the file.

## Status codes

| Status | Meaning | What to do |
|---|---|---|
| `400 Bad Request` | Invalid body (malformed JSON or not an object), a missing required field, an invalid value (for example an unknown status, a malformed ID, or an ID that belongs to another organization), or an update with no updatable field | Fix the request. Don't retry it as is. |
| `401 Unauthorized` | API key missing, invalid, expired or revoked | Check `reason` |
| `403 Forbidden` | The key lacks permission for this action on this resource | Add the permission to the key |
| `404 Not Found` | The record doesn't exist, was deleted or belongs to another organization - or the path doesn't exist (for example an extra segment) | Check the ID and the path |
| `405 Method Not Allowed` | The HTTP method isn't supported on this path, for example `POST /tasks/{id}` (it doesn't create a task). The `Allow` header lists the supported methods. | Use one of the methods in `Allow` |
| `409 Conflict` | The action conflicts with the current state: an existing link, a locked note, a cancelled payment, or a duplicate unique value (such as an ID number) | Read the current record and decide how to proceed |
| `429 Too Many Requests` | Rate limit exceeded | Wait until `retry_after`. See [Rate limits](https://docs.banta.co.il/guides/rate-limits.md). |
| `500 Internal Server Error` | A failure on banta's side | Retry with exponential backoff; contact support if it persists. |
| `503 Service Unavailable` | A temporary failure in the authentication service (`reason: auth_service_error`). The key was not rejected. | Retry after a few seconds, with backoff |

> The API also returns `404` for a record that exists in another organization, so an ID can't be probed for existence.

## Partial success

`POST /mortgage-files` validates everything you send, including `borrowers`, before it creates anything - an invalid item returns `400` and nothing is saved. After the file is saved, its nested records (borrowers, incomes, bank accounts, obligations) are saved one by one. If one of them fails on the database side, the call still returns `201` with the file and a `child_errors` array, for example `["borrower[1]: 23505"]`.

Don't re-send the `POST` - that creates a second file. Add what is missing to the existing file with `POST /mortgage-files/{id}/borrowers` or `PATCH /mortgage-files/{id}`. On `PUT`/`PATCH`, `incomes`, `bank_accounts` and `obligations` are `null` in the response when you didn't send them, not when they are empty.

## Retries

- **Retry:** `429` (after the `Retry-After` wait), `500`, `503`, and network errors or timeouts. Use exponential backoff (e.g. 1, 2, 4, 8 seconds) with some jitter.
- **Don't retry without changing something:** `400`, `401`, `403`, `404`, `405`, `409`.
- **Careful with `POST` after a timeout:** the first request may have succeeded. The API doesn't support idempotency keys yet, so before re-sending a create, check with `search` whether the record already exists.

---

Part of the banta API documentation. Full index: https://docs.banta.co.il/llms.txt
