# Errors (/developers/errors)

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](mailto:support@fianto.xyz).

## The error body [#the-error-body]

```json
{
  "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 [#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 [#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](/developers/authentication)                    |
| 400    | `idempotency_key_required`, `idempotency_key_invalid` | The `Idempotency-Key` header is missing or malformed. See [Idempotency](/developers/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](/developers/pagination-and-rate-limits)                                              |
| 500    | `internal_error`                                      | Something failed on Fianto's side. Quote the `request_id`                                                                                         |

## In the SDK [#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)`:

```ts
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 [#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 [#see-also]

- [Idempotency](/developers/idempotency): The five idempotency codes, and safe retries.

- [Pagination and rate limits](/developers/pagination-and-rate-limits): 429, Retry-After and the limits.

- [Checkout sessions](/developers/checkout-sessions): The checkout codes in context.