Skip to content

No code, no charge. Every route is measured, and failing routes are removed.

How we measure

FastOTP APIdocumentationv1

Base URL https://fastotp.io/api/v1

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.

JSONExample error ยท 409 Conflict
{
  "error": {
    "code": "price_changed",
    "message": "The price for this route has changed.",
    "price": "0.16"
  }
}
Fields of the error object
FieldTypeDescription
codestringThe name of the error. One of the fourteen codes below. Branch on this; it is the stable part.
messagestringOne sentence for a person to read, in English. Log it or show it, but do not match on its wording, which may change.
โ€ฆvariesExtra 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?

Every error code of the FastOTP API
Code and statusMeaningCharged?What to do
unauthorizedHTTP 401The 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 403The 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 404No 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 405The 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 422A 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 402Your 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 409The 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 503The 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 409The 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 409The 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 409The 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 429This 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 502A 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 503Ordering 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.

Extra fields on error objects
FieldTypeDescription
fieldsobjectOn 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.
pricestringOn price_changed and insufficient_balance. The live price of the order in US dollars, as a decimal string.
retry_afterintegerOn cancel_too_early. Seconds until the order may be cancelled. The same number is sent in the Retry-After header.
JSONExample error ยท 422 Unprocessable Content
{
  "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 in Retry-After.
  • cancel_too_early: wait retry_after seconds.
  • 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 in fields.
  • insufficient_balance: add balance first.
  • price_changed: raise max_price, if you accept the new price.
  • unauthorized and forbidden: 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.

Node.jsBranching on the error code
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.