# Errors

> The error envelope, every code, what to retry, rate limits, and the versioning policy.

Source: https://www.tabba.io/developers/api/errors

The API answers with real HTTP status codes, so `res.ok` and ordinary error
handling work. Every failure has the same body:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "VALIDATION_FAILED",
    "message": "The customer failed validation.",
    "field_errors": {
      "email": ["EMAIL_REQUIRED_FOR_EMAIL_SEND"],
      "vat_zone_ref": ["VAT_ZONE_REQUIRED"]
    },
    "request_id": "req_…"
  }
}
```

| Field          | Meaning                                                                                                            |
| -------------- | ------------------------------------------------------------------------------------------------------------------ |
| `type`         | The family: `authentication_error`, `permission_error`, `invalid_request_error`, `rate_limit_error` or `api_error` |
| `code`         | The stable code to branch on                                                                                       |
| `message`      | English, for a log or a developer. Not for an end user                                                             |
| `field_errors` | Per field of the request, the codes of what to fix. Present where a field is at fault                              |
| `refusal`      | Why the accounting system refused, where it did                                                                    |
| `record_id`    | On `CREATE_UNFINISHED` only: the `id` of the record the request did create                                         |
| `request_id`   | The id of the request                                                                                              |

**Branch on `code`, never on `message`.** Codes are added and never renamed.
An unknown code should fall through to a generic handler. A success is the
bare resource, with an `object` field that names its kind, and has no
envelope around it.

## Status codes [#status-codes]

| Status | Meaning                                                            | Codes                                                                      |
| ------ | ------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| `400`  | The request is malformed                                           | `BAD_REQUEST`                                                              |
| `401`  | The key is missing, unknown or revoked                             | `UNAUTHORIZED`, `INVALID_API_KEY`                                          |
| `403`  | The key lacks the scope the endpoint needs                         | `SCOPE_INSUFFICIENT`                                                       |
| `404`  | The company holds no such record                                   | `NOT_FOUND` and the resource's own, below                                  |
| `409`  | The request runs into the state of the record                      | see below                                                                  |
| `422`  | The request failed validation, or the accounting system refused it | `VALIDATION_FAILED` and the refusals below                                 |
| `429`  | Over a rate limit                                                  | `RATE_LIMITED`                                                             |
| `500`  | An error on Tabba's side                                           | `INTERNAL_ERROR`, `DB_ERROR`, `MIRROR_WRITE_FAILED`, `REGISTRATION_FAILED` |
| `502`  | The accounting system did not answer, or did not finish            | `PLATFORM_API_ERROR`, `CREATE_UNFINISHED`                                  |

## Every code [#every-code]

### The key and the request [#the-key-and-the-request]

| Code                 | Status | Meaning                                                         |
| -------------------- | ------ | --------------------------------------------------------------- |
| `UNAUTHORIZED`       | `401`  | The request carries no `Authorization: Bearer` header           |
| `INVALID_API_KEY`    | `401`  | The key is malformed, unknown or revoked                        |
| `SCOPE_INSUFFICIENT` | `403`  | A `read` key on an endpoint that needs `read_write`             |
| `RATE_LIMITED`       | `429`  | Over a rate limit. Retry after `Retry-After` seconds            |
| `BAD_REQUEST`        | `400`  | The body is not JSON, or a reference is not in one of its forms |
| `VALIDATION_FAILED`  | `422`  | The body or the query failed validation. See `field_errors`     |
| `NOT_FOUND`          | `404`  | A path names a record the company does not hold                 |

### Customers, products and people [#customers-products-and-people]

| Code                           | Status | Meaning                                                                                                                                        |
| ------------------------------ | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `DUPLICATE_EXTERNAL_ID`        | `409`  | Another record of the same kind carries the `external_id`                                                                                      |
| `DUPLICATE_PRODUCT_NUMBER`     | `409`  | A product with the number exists under another `external_id`                                                                                   |
| `DELETE_BLOCKED_BY_REFERENCES` | `409`  | An invoice, a recurring series or a credit note of the company names the record. The accounting system was not asked, and nothing was deleted  |
| `DELETE_BLOCKED_BY_BOOKINGS`   | `409`  | The accounting system holds bookings against the record. Nothing was deleted                                                                   |
| `RECORD_ARCHIVED`              | `409`  | The `external_id` names a record the accounting system has switched off. It is neither updated nor created again until it is offered once more |
| `CREATE_IN_PROGRESS`           | `409`  | Another request under the same `external_id` is still creating the record. Carries `Retry-After`                                               |
| `CREATE_UNFINISHED`            | `502`  | The accounting system made the record and the create did not finish. `record_id` names it                                                      |

### Invoices [#invoices]

| Code                       | Status | Meaning                                                                               |
| -------------------------- | ------ | ------------------------------------------------------------------------------------- |
| `INVOICE_NOT_FOUND`        | `404`  | A body names an invoice the company does not hold                                     |
| `INVOICE_ALREADY_ISSUED`   | `409`  | The invoice is issued and frozen. Raise a credit note                                 |
| `INVOICE_NOT_ISSUED`       | `409`  | A credit note was asked for an invoice that is not issued                             |
| `INVOICE_IS_OCCURRENCE`    | `409`  | The invoice is an occurrence of a recurring series, and is written through its series |
| `ISSUE_IN_PROGRESS`        | `409`  | Another attempt holds the invoice. Carries `Retry-After`                              |
| `HOURLY_INCOMPLETE`        | `409`  | An hourly line is not fully complete. Nothing was issued                              |
| `NOTHING_TO_BILL`          | `409`  | The invoice has no hours to bill. Nothing was issued                                  |
| `CUSTOMER_GONE`            | `409`  | The accounting system no longer holds the customer                                    |
| `PRODUCT_GONE`             | `409`  | The accounting system no longer holds a product of the invoice                        |
| `MISSING_LINES`            | `422`  | The invoice has no lines to issue                                                     |
| `INVALID_SEND_METHOD`      | `422`  | The customer cannot be sent to this way. Nothing was issued                           |
| `MISSING_EINVOICE_DETAILS` | `422`  | The customer lacks e-invoice details. Nothing was issued                              |
| `ISSUE_REFUSED`            | `422`  | The accounting system refused to book the invoice. See `refusal`                      |

### Credit notes [#credit-notes]

| Code                        | Status | Meaning                                                                                            |
| --------------------------- | ------ | -------------------------------------------------------------------------------------------------- |
| `INVOICE_ALREADY_CREDITED`  | `409`  | A full credit was asked for an invoice that already carries a credit                               |
| `NOTHING_TO_CREDIT`         | `409`  | Nothing the invoice billed is left to credit                                                       |
| `CREDIT_IN_PROGRESS`        | `409`  | Another credit of the invoice is running. Carries `Retry-After`                                    |
| `CREDIT_ATTEMPT_UNRESOLVED` | `409`  | An earlier credit of the invoice got no definite answer. Send the request that made it again first |
| `CREDIT_REFUSED`            | `422`  | The accounting system refused to book the credit note. See `refusal`                               |

### Recurring series and occurrences [#recurring-series-and-occurrences]

| Code                             | Status | Meaning                                                                                                   |
| -------------------------------- | ------ | --------------------------------------------------------------------------------------------------------- |
| `SERIES_NOT_FOUND`               | `404`  | A body names a recurring series the company does not hold                                                 |
| `SERIES_LINE_NOT_FOUND`          | `404`  | The line is not on that occurrence of the series                                                          |
| `OCCURRENCE_OUT_OF_RANGE`        | `404`  | The date starts no billing period of the series                                                           |
| `SERIES_NOT_ACTIVE`              | `409`  | The series is a draft, and nothing is invoiced from it                                                    |
| `SERIES_SETUP_LOCKED`            | `409`  | The series has issued an invoice: its customer, currency, schedule and title are locked                   |
| `OCCURRENCE_ISSUED`              | `409`  | The occurrence is issued and its lines are frozen                                                         |
| `OCCURRENCE_SKIPPED`             | `409`  | An admin skipped the occurrence                                                                           |
| `DELETE_EMPTIES_OCCURRENCE`      | `409`  | Taking the line away would leave an occurrence that is an invoice with no lines                           |
| `SCHEDULE_DROPS_REGISTERED_WORK` | `409`  | The change leaves out occurrences with registered work. Send `acknowledge_dropped_work: true` to go ahead |
| `END_HAS_PASSED`                 | `409`  | The end date of the series has passed and cannot be moved                                                 |
| `END_CREDITED`                   | `409`  | A credit note has settled the end of the series, and it cannot be moved                                   |
| `END_BLOCKED_BY_CREDIT`          | `409`  | An invoice of the series carries a credit that is not finished                                            |
| `SERIES_END_CONFLICT`            | `409`  | The series changed while its end was being set. Nothing was written                                       |
| `OCCURRENCE_HAS_NO_LINES`        | `422`  | The series states no line for the occurrence                                                              |
| `END_BEFORE_START`               | `422`  | The end date lies before the start of the series                                                          |

### Work registrations [#work-registrations]

| Code                  | Status | Meaning                                                           |
| --------------------- | ------ | ----------------------------------------------------------------- |
| `LINE_NOT_FOUND`      | `404`  | The invoice or the occurrence has no such line                    |
| `NOT_ASSIGNED`        | `409`  | The person holds no place on the line                             |
| `HOURS_BILLED_LOCKED` | `409`  | The hours were billed when the invoice was issued, and are locked |
| `REGISTRATION_FAILED` | `500`  | The registration could not be saved                               |

### The accounting system, and Tabba's own side [#the-accounting-system-and-tabbas-own-side]

| Code                      | Status | Meaning                                                                                               |
| ------------------------- | ------ | ----------------------------------------------------------------------------------------------------- |
| `PLATFORM_REFUSED`        | `422`  | The accounting system refused the write. Nothing was written. See `refusal`                           |
| `PLATFORM_NOT_CONFIGURED` | `409`  | The company's connection is missing a setting the write needs. An admin completes it in the workspace |
| `PLATFORM_UNAVAILABLE`    | `409`  | The company has no accounting system to write to                                                      |
| `PLATFORM_API_ERROR`      | `502`  | The accounting system could not be reached, or did not answer. It may or may not hold the change      |
| `MIRROR_WRITE_FAILED`     | `500`  | The accounting system took the write, and Tabba could not record it                                   |
| `DB_ERROR`                | `500`  | A read or a write on Tabba's side failed                                                              |
| `INTERNAL_ERROR`          | `500`  | An error on Tabba's side                                                                              |

No code names an accounting system: see
[Accounting platforms](/developers/api/platforms).

## Field errors [#field-errors]

`field_errors` holds, per field of the request, the codes of what to fix. A
field is named by its dotted path as the request spells it, such as
`lines.0.agreed_value`, and `_` stands for the value as a whole. Where a rule
has a code of its own the entry is that code, such as `VAT_ZONE_REQUIRED`.
Otherwise it is a sentence that says what the field should have been.

A customer, a product or a person that a body names and the company does not
hold is reported here, as `CUSTOMER_NOT_FOUND`, `PRODUCT_NOT_FOUND` or
`ASSIGNEE_NOT_FOUND` under the field.

## Refusals by the accounting system [#refusals-by-the-accounting-system]

A refusal is data, not an outage. Where the accounting system said no, the
error carries what it said:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "ISSUE_REFUSED",
    "message": "The platform refused the invoice. Nothing was booked.",
    "refusal": {
      "reason": "…",
      "params": {},
      "platform_reason": "…"
    },
    "request_id": "req_…"
  }
}
```

`reason` is a code of Tabba's own, `params` are its parameters, and
`platform_reason` is the accounting system's own words, or `null` where it
gave none. Show `platform_reason` to a person as the accounting system's
words. An outage is `PLATFORM_API_ERROR`, never a refusal.

## What to retry [#what-to-retry]

| Answer                                                                       | What to do                                                                                                                                                              |
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `429`                                                                        | Wait `Retry-After` seconds, then send the request again                                                                                                                 |
| `409` with `CREATE_IN_PROGRESS`, `ISSUE_IN_PROGRESS` or `CREDIT_IN_PROGRESS` | Wait `Retry-After` seconds, then send the same request again. It finds what the other request came to                                                                   |
| `502` with `PLATFORM_API_ERROR`                                              | Retry with backoff. A create sent under an `external_id` is safe to send again unchanged within the hour. A create sent without one is not: it can make a second record |
| `502` with `CREATE_UNFINISHED`                                               | Send the same request again under its `external_id`, or a `PATCH` by `record_id`                                                                                        |
| `500`                                                                        | Retry with backoff, and quote the `request_id` if it persists                                                                                                           |
| `400`, `401`, `403`, `404`, `422`, and every other `409`                     | Do not retry unchanged. The same request is refused again                                                                                                               |

Issuing an invoice and creating a credit note are safe to send again whatever
the first answer was: an issued invoice and a booked credit note are answered
as they stand.

## request\_id [#request_id]

Every response carries the id of its request in the `X-Request-Id` header,
and every error repeats it in the body as `request_id`. It is `req_` and a
random string. Log it beside your own trace id, and quote it when you ask
about a call. Every call under a key is in the account's activity log under
the same id.

## Rate limits [#rate-limits]

Limits are counted per key, in a sliding window of 60 seconds.

| Bucket            | Limit                                          | Counted for                                                                                                                                                                                                            |
| ----------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `default`         | 300 requests per 60 seconds, per key           | Every endpoint                                                                                                                                                                                                         |
| `platform_write`  | 30 requests per 60 seconds, per key            | On top of `default`, the endpoints that make the accounting system book something inside the request: `POST /invoices/{ref}/issue`, `POST /recurring-series/{ref}/occurrences/{period}/issue` and `POST /credit-notes` |
| `unauthenticated` | 60 requests per 60 seconds, per client address | A request with no key, or with a key that is not a live key of Tabba's                                                                                                                                                 |

Every metered response carries three headers. They state the bucket with the
least room left:

```
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 287
X-RateLimit-Reset: 1782892860
```

`X-RateLimit-Reset` is when the window has room again, in seconds since the
epoch. Past a limit the answer is `429` with `RATE_LIMITED`, and one header
more:

```
Retry-After: 12
```

The separate `platform_write` bucket means that a retry loop on an issue
spends its own budget and leaves your reads alone. A request refused for its
scope spends nothing.

A request that carries no key, or a key that is unknown or revoked, is
counted by the address it came from. Past that limit it is answered `429` in
place of the `401`. A request under your live key is not counted there,
whatever else the same address sends.

## Versioning [#versioning]

Changes within `/api/v1` are additive. Within it, Tabba may:

* Add endpoints.
* Add optional fields to a request body.
* Add fields to a response body.
* Add event types to webhooks.
* Add error codes.

Within `/api/v1`, an endpoint, a field and an error code are not renamed, and
a code is not moved to another status. Write your client so that an unknown
field is ignored and an unknown `code` reaches a generic handler.
