Fiatside

Developers

Error catalogue

Every error carries a stable, machine-readable code and a coherent HTTP status. The code is what your integration should branch on: the text may evolve, the code will not.

01

Shape of an error

The current shape is deliberately minimal. It is stable, and that is what matters: a code, and nothing that leaks an internal detail.

Current shape — real responsejson
{ "error": "rate_stale" }
With a human-readable reason — real responsejson
{
  "error": "rail_not_open",
  "reason": {
    "fr": "La retenue TDS de 1 % et l’enregistrement FIU-IND exigent une entite locale…",
    "en": "The 1% TDS withholding and FIU-IND registration require a local entity…"
  }
}

Some errors carry a bilingual reason object, meant to be shown as-is to the user. A closed rail is one: the reason explains a named regulatory constraint, not an outage, and showing it saves a support request. The versioned API will add a request id to this envelope, so you can point us at a precise line in our logs.

02

Codes in force

These codes are returned today by the quote endpoint.

Codes in force
codeHTTPWhat it meansWhat to do
invalid_request400The request body fails schema validation: missing field, wrong type, value outside the allowed bounds.Check that amount is a string and not a number, and that direction is exactly “sell” or “receive”.
invalid_amount400The amount cannot be parsed, or carries more decimals than the asset or currency accepts.Truncate to the unit precision: 6 decimals for USDT, 8 for BTC, 2 for the euro, 0 for the CFA franc.
unknown_asset_or_rail404The asset or rail identifier does not exist in the catalogue.Identifiers are stable and never reassigned. Reload the catalogue rather than guessing them.
rail_not_open409The rail exists but is not open. The response carries the exact reason, in both languages.Show the reason as-is: it explains a regulatory constraint, not an outage. Offer an open rail in the same country.
asset_not_offered409The asset is in the catalogue but not offered — the case for enhanced-anonymity assets.Do not offer it in your selector. The catalogue returns it with its reason so you can explain it.
rate_stale503The last known price exceeds the maximum tolerated age. The engine refuses to quote rather than serve a stale price as a firm one.Retry after a few seconds. Do not cache the last successful quote to fill the gap: that would be exactly the mistake this code prevents.
rate_unavailable503No price is available for this asset / currency pair.Disable the pair in your interface rather than showing an estimate. A displayed estimate becomes an expectation.
quote_failed500Unexpected error during computation. No quote is returned.Retry once; if the error persists, write to us with the exact timestamp of the call.
03

Codes from the published contract

These codes accompany endpoints that are not open yet. They are published so your error handling is written once.

Codes in force
codeHTTPWhat it meansWhat to do
unauthorized401Key missing, revoked, or used on the wrong environment.An sk_test_ key does not work in production, and the reverse does not either: that is deliberate.
idempotency_conflict409The same idempotency key has already been used with a different request body.A key belongs to one intent. If the content changes, the key changes; otherwise there is no telling which of the two orders to replay.
quote_expired409The quote has passed its lock window.Re-quote and have the new amount accepted. We never silently re-price against the customer.
beneficiary_rejected422The beneficiary details fail the rail validation: invalid checksum, non-conforming format, or a name that does not match the verified identity.The response names the offending field. The beneficiary name must be the verified identity: no third-party payout is possible.
country_not_served451The destination country is not served: sanctions, restrictive measures, or the absence of a local framework.The 451 code is chosen deliberately over a 404: when we refuse, we say it is a refusal and why.
rate_limited429Too many calls within the sliding window.Honour the Retry-After header. Quotes are cheap to queue, expensive to hammer.
not_found404The resource does not exist, or does not belong to your key.Both cases return the same code: a response that told them apart would let anyone enumerate other people’s references.
04

What is not an error

An amount outside the rail bounds returns a valid quote, with a limitError object. It is not a failure: it is information your interface should display.

Response excerptjson
"limitError": { "code": "below_min", "limit": "20.00" }

Treating this case as an HTTP error would deprive you of the computed amount, and therefore of the ability to tell the user “you are 16.53 EUR short of the minimum for this payout method”. The bound applies to the net amount, the one the beneficiary receives.

05

Retry, or not

Three families, three behaviours. Retrying a validation error in a loop only fills your logs.

Never retry

invalid_request, invalid_amount, unknown_asset_or_rail, asset_not_offered, unauthorized. The request is at fault: retrying it unchanged gives the same result. Fix it, or display the reason.

Retry with backoff

rate_stale, rate_unavailable, quote_failed, and any rate limiting. Wait a few seconds, with a growing gap between attempts. Do not fill the gap with a cached quote.

Ask for a decision

rail_not_open, quote_expired, beneficiary_rejected, country_not_served. The situation needs a human choice: offer another rail, a fresh quote, corrected details.