API documentation ยท 3 of 4
Errors: every code and what to do about it
Every failed request returns a status outside the 2xx range and a JSON body with a stable code, so your integration branches on a name instead of parsing a sentence.
On this page
The error body
An error is always one object under the key error. It has a code and a message, and for some codes a few extra fields at the same level.
{
"error": {
"code": "price_changed",
"message": "The price for this route has changed.",
"price": "0.16"
}
}| Field | Type | Description |
|---|---|---|
| code | string | The name of the error. One of the fourteen codes below. Branch on this; it is the stable part. |
| message | string | One sentence for a person to read, in English. Log it or show it, but do not match on its wording, which may change. |
| โฆ | varies | Extra fields that only certain codes carry. See extra fields. |
A successful request always has a 2xx status and never contains an error key, so checking the status is enough to tell the two apart.
All error codes
Fourteen codes cover every failure. The third column answers the question that matters most when money is involved: did this request cost anything?
| Code and status | Meaning | Charged? | What to do |
|---|---|---|---|
| unauthorizedHTTP 401 | The Authorization header is missing or malformed, or the key has been revoked. | No. | Send Authorization: Bearer sk_live_โฆ. Create a new key in the account area if this one was revoked. |
| forbiddenHTTP 403 | The key is valid but lacks the scope this endpoint needs. | No. | Use a key that has the scope, for example orders:create for POST /orders. |
| not_foundHTTP 404 | No order with this id exists on your account, or the path is wrong. | No. | Check the id. An order is visible only to the account that created it. |
| method_not_allowedHTTP 405 | The path exists but does not accept this HTTP method. The Allow header lists the ones it does. | No. | Use the method shown for the endpoint in the API reference. |
| validation_errorHTTP 422 | A body field or query parameter is missing or malformed. fields names each one. | No. | Fix the fields listed and send the request again. Unchanged, it fails the same way. |
| insufficient_balanceHTTP 402 | Your balance is lower than the live price. price is what the order costs. | No. | Add balance, then send the request again. |
| route_not_on_saleHTTP 409 | The route does not exist or its status is not on_sale, for example because the performance rule suspended it. | No. | Pick a route that GET /routes lists as on_sale. Do not retry the same route in a loop. |
| no_numbersHTTP 503 | The route is on sale but has no stock at this moment. | No. Your balance is unchanged when the response arrives. | Retry later with backoff, or order the same service in another country. |
| price_changedHTTP 409 | The live price is above your max_price. price is the live price. | No. | Decide whether the new price is acceptable. If it is, send the request again with a higher max_price. |
| cancel_too_earlyHTTP 409 | The order is less than two minutes old. retry_after is the number of seconds left. | Nothing new. The order is unchanged and still active. | Keep polling. Cancel again after retry_after seconds if there is still no code. |
| not_cancellableHTTP 409 | The order has already ended: completed, cancelled or expired. | Nothing new. The order is unchanged. | Fetch the order. If it completed, the code is in it; otherwise it was already refunded. |
| rate_limitedHTTP 429 | This key sent too many requests in the current minute. | No. | Wait the number of seconds in the Retry-After header, then carry on. |
| upstream_unavailableHTTP 502 | A number could not be obtained just now. | No. If a charge was made it was refunded before the response was sent. | Retry with exponential backoff, reusing the same Idempotency-Key. |
| service_unavailableHTTP 503 | Ordering is temporarily switched off. | No. | Retry later with a long backoff. Do not retry in a tight loop. |
Extra fields
Four codes carry more than a code and a message.
| Field | Type | Description |
|---|---|---|
| fields | object | On validation_error. Each key is the name of a body field or query parameter that was rejected; its value says what is wrong with it. |
| price | string | On price_changed and insufficient_balance. The live price of the order in US dollars, as a decimal string. |
| retry_after | integer | On cancel_too_early. Seconds until the order may be cancelled. The same number is sent in the Retry-After header. |
{
"error": {
"code": "validation_error",
"message": "The request is not valid.",
"fields": {
"country": "This field is required."
}
}
}Which errors are safe to retry
Sort an error into one of three groups before deciding what your code does next. Sending POST /orders again is safe in every case below as long as you reuse its Idempotency-Key.
Retry the same request after waiting
rate_limited: wait the seconds given inRetry-After.cancel_too_early: waitretry_afterseconds.no_numbers,upstream_unavailable,service_unavailable, and any other 5xx: retry with exponential backoff and a cap on the number of attempts.- A timeout or a dropped connection, where you got no response at all: treat it like a 5xx.
Retry only after changing something
validation_error: correct the fields named infields.insufficient_balance: add balance first.price_changed: raisemax_price, if you accept the new price.unauthorizedandforbidden: use a valid key that has the scope.
Do not retry
route_not_on_sale: the route will not come back within the span of a retry loop. Choose another route.not_found: the id is wrong.method_not_allowed: the request itself is wrong. Fix the method.not_cancellable: the order has ended. Fetch it to see how.
The waiting times, the backoff and ready-made retry code are in limits and safe retries.
Handling errors in code
Branch on error.code. The functions called here are yours to write; the point is that each group of codes leads to one decision.
async function createOrder(request) {
const res = await fetch("https://fastotp.io/api/v1/orders", request);
if (res.ok) return res.json();
const { error } = await res.json();
switch (error.code) {
case "price_changed": // nothing charged; error.price is the live price
return askAgain(error.price);
case "insufficient_balance": // nothing charged; error.price is what it costs
return stopAndTopUp(error.price);
case "route_not_on_sale": // pick another route from GET /routes
return chooseAnotherRoute();
case "rate_limited":
case "no_numbers":
case "upstream_unavailable":
case "service_unavailable": // nothing charged; wait, then send it again
return retryLater(res.headers.get("retry-after"));
default: // unauthorized, forbidden, validation_error: a bug or a bad key
throw new Error(error.code + ": " + error.message);
}
}For a script that handles these cases from start to finish, see the complete Node.js and Python examples in the quickstart.