Errors
Every status and error code the billing API answers with, and what to do about each.
Errors
Every failure answers in the same shape:
{
"message": "The request has invalid fields.",
"error": "VALIDATION_FAILED",
"errors": [
{
"field": "line_items[2].amount",
"code": "TOO_MANY_DECIMALS",
"message": "USD amounts have at most 2 decimal places"
}
],
"request_id": "req_01JQ8Z0Y5N6W3K2M4P7R9T1V3X"
}error— a stable code. It is a contract: branch on it in your integration. Codes are added over time, never renamed or repurposed.message— a fixed human sentence. It never contains your data.errors— present only onVALIDATION_FAILEDandUNSUPPORTED_IN_V1. Every problem with the request, each naming the exact path into your body.request_id— also in theX-Request-Idheader on every response. Quote it to support and we can find the exact request.
A code always answers with the same status.
400 — fix the request
| Code | What it means |
|---|---|
VALIDATION_FAILED | One or more fields are invalid. errors lists all of them. |
UNSUPPORTED_IN_V1 | The request uses a feature that is not supported yet — a subscription set, or a usage_based_recurring line. errors names the fallback for each. |
INVALID_JSON | The body is not valid JSON. |
BAD_REQUEST | The request could not be read. |
Validation reports every problem at once, so one round trip tells you everything to fix. Common field codes inside errors: REQUIRED, INVALID_TYPE, INVALID_FORMAT, TOO_LONG, TOO_MANY_DECIMALS, TOO_LARGE, OUT_OF_RANGE, MUST_BE_POSITIVE, MUST_NOT_BE_NEGATIVE, DUPLICATE, UNKNOWN_FIELD, FIELD_NOT_ALLOWED_FOR_TYPE, TOO_FEW_ITEMS, TOO_MANY_ITEMS, UNSUPPORTED_CURRENCY.
401 / 404 / 405
| Code | What it means |
|---|---|
UNAUTHORIZED | The API key is missing or invalid. |
NOT_FOUND | The object in the path does not exist, or the route does not. |
METHOD_NOT_ALLOWED | That method is not allowed here. The Allow header lists what is. |
409 — valid, but it conflicts with what is stored
These are the "you cannot change that" family. The request was fine; the stored object will not accept it.
| Code | What it means | What to do |
|---|---|---|
INVOICE_LOCKED | The invoice has a payment, refund, void or write-off, so its billed content is frozen. | Void it and send a corrected invoice. |
INVOICE_CURRENCY_LOCKED | An invoice's currency cannot change once it exists. | Void it and send a new one. |
INVOICE_VOIDED | A voided invoice cannot take transactions. | Record the money against a live invoice. |
PLAN_CADENCE_LOCKED | Invoices already use this plan's billing interval. | Create a new plan. |
TRANSACTION_IMMUTABLE | A transaction's type, amount, currency, invoice or parent cannot change. | Record a reversal instead. |
EVENT_IMMUTABLE | A subscription event cannot change once recorded. | Send a retraction. |
SUBSCRIPTION_BELONGS_TO_ANOTHER_CUSTOMER | That subscription id is already another customer's. | Use an id unique to this customer. |
TRANSACTION_EXISTS_IN_PAYMENTS_API | That id was already recorded through the payments API. | Use a different id, or keep that payment where it is. |
IMPORT_NOT_OPEN | The import no longer accepts items, a commit or a cancel. | Open a new import for whatever is left. |
IMPORT_TOO_LARGE | The import has staged as much data as one import may. | Commit it and open another. |
TOO_MANY_OPEN_IMPORTS | The site already holds 3 open imports. | Commit or cancel one first. |
CONCURRENT_UPDATE | Another request is writing the same object right now. | Retry shortly. |
413 / 415
| Code | What it means |
|---|---|
PAYLOAD_TOO_LARGE | The body is over 1 MB (5 MB on the import routes). Send fewer line items or fewer bulk items per call. |
UNSUPPORTED_MEDIA_TYPE | The body must be sent as Content-Type: application/json. |
422 — it names something that is not there
The request is well-formed, but it refers to an object that does not exist or breaks a rule across objects. Usually a sequencing problem: send the thing it names first.
| Code | What to do |
|---|---|
CUSTOMER_NOT_FOUND | Send the customer first. |
PLAN_NOT_FOUND | Send the plan first. |
INVOICE_NOT_FOUND | Send the invoice first. |
SUBSCRIPTION_NOT_FOUND | Only a subscription_start event or an invoice line can create a subscription. Send one of those first. |
TRANSACTION_NOT_FOUND | The payment a reversal names does not exist, did not succeed, or is on another invoice. |
EVENT_NOT_FOUND | The event a retraction names does not exist. |
CURRENCY_MISMATCH | The currency you sent is not the invoice's. Omit it, or send the invoice's. |
AMOUNT_EXCEEDS_INVOICE | Payments on an invoice cannot exceed its total. |
AMOUNT_EXCEEDS_PAYMENT | Refunds, chargebacks and reversals cannot exceed the payment they act on. |
CURRENCY_NOT_SUPPORTED | Faurya holds no exchange rate for that currency. |
429 — rate limited
| Code | What it means |
|---|---|
RATE_LIMITED | Over 25 requests/second (bursting to 100) for your site. The Retry-After header says how many seconds to wait. |
If you are hitting this regularly you are probably loading a history. Use the bulk import instead.
500 / 503 — ours
| Code | What it means |
|---|---|
INTERNAL_ERROR | Something went wrong on our side. Your request was not saved. Retry; if it keeps failing, contact support with the request_id. |
SERVICE_UNAVAILABLE | We are temporarily busy. Your request was not saved. Retry after the Retry-After header. |
Both are safe to retry: writes are idempotent on your external_id, so a retry of a request that actually did land is a replay, not a duplicate.
Retrying safely
- Every write is a
PUTkeyed on an id you chose, so retrying is always safe. Sending the same body again writes nothing and answers200. - Retry
429,500and503, honouringRetry-Afterwhere it is present. - Do not retry
400,409,415or422unchanged — they will fail the same way.422is usually fixed by sending the object it names first;409means the stored object genuinely will not take that change. - Log the
request_idwith the response status in your backend. It is the one value that joins your logs to ours.