Errors
The error envelope, every code, what to retry, rate limits, and the versioning policy.
The API answers with real HTTP status codes, so res.ok and ordinary error
handling work. Every failure has the same body:
{
"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 | 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
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
| 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
| 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
| 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
| 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
| 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
| 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.
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
A refusal is data, not an outage. Where the accounting system said no, the error carries what it said:
{
"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
| 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
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
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: 1782892860X-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: 12The 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
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.