Errors
One shape for every failure, one code to branch on, and what to do about each one.
One shape, every time
Every failure, from a bad key to a bug on our side, comes back as the same JSON object with the same three fields.
{
"error": {
"code": "validation_failed",
"message": "Unknown field: notes.",
"requestId": "api:0190a1b2-c3d4-7e5f-8a9b-0c1d2e3f1001:0190a1b2-c3d4-7e5f-8a9b-0c1d2e3fee00"
}
}code comes from a closed list. message is a sentence written for a person and may be reworded at any time, so do not match on it. requestId names this exact request, and is also on every response as the x-request-id header.
Branch on the code, not the status
Statuses are shared. period_locked and conflict are both 409. insufficient_scope and entity_not_allowed are both 403. The status is for a proxy or a log; code is what your software should switch on.
Every code, and what to do
invalid_key(401). Your key is missing, malformed, revoked, or expired. Check the Authorization header, then check in Settings → API keys that the key is still there and still active. Create a new one if it was revoked or has expired.insufficient_scope(403). Your key does not carry the scope this endpoint needs. A Read only key cannot reach a write endpoint. Create a Read and write key and move your software over to it.entity_not_allowed(403). Your key is limited to certain businesses, and this is not one of them. CallGET /businessesto see which businesses this key may use, and send one of those ids.period_locked(409). The date falls inside a closed period, so nothing can post to it. Use a date outside the closed period, or ask an Owner to reopen the month in the app. Nothing in the API can post into a closed period.read_only_billing_state(402). Your subscription is past due, so your books are read only for now. Reads keep working. Sort the subscription out in Settings → Billing and the writes come back.rate_limited(429). You've made too many requests for this key. The retry-after header says how long to wait. Sleep for that many seconds, then send the request again. Do not retry in a tight loop.validation_failed(422). Something in the path, the query, or the body did not pass validation. Readmessage. It names the field and what was wrong with it. Fix the request before sending it again.not_found(404). There is no such record, or none your key can reach. Check the id. A row in a business your key may not use answers this way too, so checkGET /businessesas well.conflict(409). The request clashed with something that already happened. Readmessage. It says which clash it was: anIdempotency-Keyreused for a different request, a request with that key that has not finished, or finished without its result being saved, or a reminder already sent for that invoice.internal_error(500). Something went wrong on our side. Try again. Send the same request again, with the sameIdempotency-Keyif you sent one, never a new one. KeeprequestIdso we can find the exact request.
A validation message names the field
A validation_failed message begins with the field that failed, written as a path such as lines.0.quantity, then what was wrong with it. A field the endpoint does not know is refused rather than ignored, and the message lists the names it did not recognize.
A path parameter repeated in the query or the body is refused too, rather than one of the two quietly winning. So is a body that is not a JSON object, and a request body larger than 1,048,576 bytes.
Money is always whole cents as a digit string: "12500", never 125.00 and never a JSON number. Dates are YYYY-MM-DD.
Which methods a path takes
The API serves GET and POST. A PUT, PATCH or DELETE, and a GET or POST sent to a path that does not take it, all answer 404 not_found with the usual error body, the same as a path that was never there. The reference lists the methods each path has.
Quote the requestId
Log requestId for every failed call. It is the one thing that lets us find the exact request in our own logs, and it is on the successful responses as well, in x-request-id.
What is worth retrying
Retry a rate_limited response after the wait in retry-after. Retry an internal_error once or twice with a short backoff. Do not retry anything else: it will fail the same way until the request changes. If the call was a write, retry it with the same Idempotency-Key you first sent, never a new one. If it comes back 409 conflict, do not retry with a new key: either the key is held by a request that has not finished, or finished without its result being saved, or the write itself clashed, as a second reminder inside its cooldown does. message says which. See Idempotency and safe retries.