DevelopersBuild safely
Errors
The v1 error body, request ids, the codes you will meet most, and how the SDK turns errors into classes and retries.
Every v1 error has the same JSON body. Branch on code, show or log message, and quote
request_id when you write to support@fianto.xyz.
The error body
{
"statusCode": 409,
"error": "Conflict",
"code": "order_already_paid",
"message": "That order_id is already paid. Use a new order_id to charge again.",
"request_id": "req_…",
"field": "order_id"
}| Field | What it is |
|---|---|
statusCode | The HTTP status |
error | The HTTP reason phrase, such as Conflict |
code | A stable, machine-readable code. Branch on this |
message | What went wrong and what to do, for people. Always a string |
request_id | The request's id, also sent as the X-Request-Id header |
field | The request field at fault, when there is one |
details | Every problem, when one request had several |
An unexpected server error always reads internal_error with the message "Something went wrong on
our side. Quote the request_id when you contact support."
Request ids
Every response carries an X-Request-Id header: req_ followed by 24 characters. If you send your
own X-Request-Id of 8 to 64 letters, digits, _ or -, fianto keeps it, so your logs and
fianto's share one id.
Codes
The list of codes is open-ended: fianto can add codes, so treat any code you do not know by its HTTP
status. When a response has no specific code, fianto uses a general one for its status, such as
bad_request, unauthorized, forbidden, conflict, unprocessable or rate_limited.
The codes you are most likely to meet:
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_failed | A field failed validation, or the body has an unknown field. field names the first one, message the first problem and details every problem |
| 401 | invalid_api_credentials | Wrong or disabled credentials, or an account that is not approved and active. See Authentication |
| 400 | idempotency_key_required, idempotency_key_invalid | The Idempotency-Key header is missing or malformed. See Idempotency |
| 422 | idempotency_key_reused | The key was used with a different request |
| 409 | idempotency_request_in_progress | The first request with this key is still running |
| 409 | idempotency_response_unreadable | The request already completed; fetch the resource |
| 400 | amount_or_price_required | Send price_id, or amount with description |
| 400 | amount_out_of_range | The amount is outside 0.01 to 1,000,000 USDC |
| 400 | url_insecure | success_url or cancel_url is not https |
| 400 | expires_at_out_of_range | expires_at is not 5 minutes to 24 hours away |
| 404 | price_not_found | No price with that id belongs to you |
| 409 | order_already_paid | This order_id is paid; use a new one |
| 409 | payment_in_progress | A payment for this session is being processed, or the last transaction prepared for it could still land |
| 409 | session_not_reissuable | The session is not open, or has under 120 seconds left |
| 409 | session_not_cancelable | The session is no longer open |
| 409 | checkout_unavailable | fianto could not reach or check Solana in time. Retry after Retry-After (5 seconds) |
| 422 | merchant_token_account_missing | Your receiving wallet has no USDC token account |
| 400 | price_id_required, amount_not_allowed | A subscription session needs price_id and no amount or description |
| 422 | price_not_recurring | A subscription session needs a recurring price |
| 429 | plan_limit_reached | Your account created 50 new subscription plans in the last 24 hours |
| 409 | subscription_already_ended | The subscription has already ended |
| 409 | webhook_endpoint_not_active | No verified webhook URL to send a test event to |
| 429 | rate_limited | Too many requests. Wait for Retry-After. See rate limits |
| 500 | internal_error | Something failed on fianto's side. Quote the request_id |
In the SDK
The SDK throws one class per status. Every class extends FiantoError.
| Class | When |
|---|---|
APIError | Any error response. Has status, code, message, field, details, requestId, headers |
AuthenticationError | 401 |
PermissionDeniedError | 403 |
InvalidRequestError | 400 and 422 |
NotFoundError | 404 |
ConflictError | 409 |
RateLimitError | 429, with retryAfterSeconds |
ServiceUnavailableError | 503 |
InternalServerError | Other 5xx |
ConnectionError | No response arrived |
TimeoutError | The request took longer than timeoutMs |
The status classes extend APIError. Check a code with isFiantoError(err, ErrorCode.X):
import { APIError, ErrorCode, Fianto, isFiantoError } from '@fianto/sdk';
const fianto = new Fianto();
export async function createCheckout(orderId: string) {
try {
return await fianto.checkoutSessions.create({
mode: 'payment',
order_id: orderId,
amount: '12.50',
description: 'Coffee beans, 1 kg',
success_url: 'https://shop.example/thank-you',
cancel_url: 'https://shop.example/cart',
});
} catch (err) {
if (isFiantoError(err, ErrorCode.OrderAlreadyPaid)) {
return null; // this order is paid; do not charge it again
}
if (err instanceof APIError) {
console.error(err.status, err.code, err.requestId); // quote requestId to support
}
throw err;
}
}What the SDK retries
By default the SDK retries a call up to 2 times (maxRetries, 0 to 10). It retries network errors,
timeouts, 408, 429, every 5xx, and a 409 only when the code is idempotency_request_in_progress or
checkout_unavailable. It never retries any other 4xx, idempotency_key_reused included.
Between attempts it waits for Retry-After when the response sends one, and otherwise backs off from
500 ms, doubling each time up to 8 s, with ±25 % jitter. If Retry-After asks for more than
10 seconds, the SDK does not wait: it throws the error at once. Every retry of a POST reuses the
same idempotency key.
Route helpers pass only some codes to the browser
The SDK's checkout route helpers (Checkout(), checkout(), createCheckoutHandler) relay only an
allowlist of codes to the browser, such as payment_in_progress, url_insecure,
price_id_required and plan_limit_reached. Every other error, including 401, 403, 5xx and
merchant_token_account_missing, reaches the browser as 500 internal_error. Your onError
always receives the original error, so log it there.
instanceof can fail when one app loads the SDK twice, once as ESM and once as CommonJS.
isFiantoError works either way.
See also
Was this page helpful? Tell us