# @fianto/sdk (/developers/sdks/sdk)

`@fianto/sdk` is the server side of every integration: a typed client for the v1 API, webhook
verification under `@fianto/sdk/webhooks`, and route handlers that work with any `Request` /
`Response` framework under `@fianto/sdk/handlers`. The framework packages wrap those handlers.

#### pnpm

```bash
pnpm add @fianto/sdk
```

#### npm

```bash
npm install @fianto/sdk
```

#### yarn

```bash
yarn add @fianto/sdk
```

#### bun

```bash
bun add @fianto/sdk
```

## Create the client [#create-the-client]

```ts
import { Fianto } from '@fianto/sdk';

// Reads FIANTO_APP_ID and FIANTO_APP_SECRET, and FIANTO_BASE_URL when it is set.
const fianto = new Fianto();

// Or pass everything yourself, for example where there is no process.env.
export function clientFor(env: { FIANTO_APP_ID: string; FIANTO_APP_SECRET: string }) {
  return new Fianto({ appId: env.FIANTO_APP_ID, appSecret: env.FIANTO_APP_SECRET, maxRetries: 3 });
}

console.log(await fianto.application.retrieve());
```

| Option                    | Env variable        | Default                  | Notes                                                                                 |
| ------------------------- | ------------------- | ------------------------ | ------------------------------------------------------------------------------------- |
| `appId`                   | `FIANTO_APP_ID`     | none, required           | `fian_app_…`                                                                          |
| `appSecret`               | `FIANTO_APP_SECRET` | none, required           | `fian_sk_live_…`. Server only.                                                        |
| `baseUrl`                 | `FIANTO_BASE_URL`   | `https://api.fianto.xyz` | `https`, except `localhost`, `127.0.0.1` and `[::1]`. Without `/v1`: the SDK adds it. |
| `timeoutMs`               | —                   | `30000`                  | Per attempt, 1 to 600000.                                                             |
| `maxRetries`              | —                   | `2`                      | Retries after the first attempt, 0 to 10.                                             |
| `fetch`                   | —                   | the global `fetch`       | Your own `fetch`, for custom networking or tests.                                     |
| `dangerouslyAllowBrowser` | —                   | `false`                  | Without it, `new Fianto()` throws in a browser.                                       |

A missing `appId` or `appSecret` throws at construction, naming the option and the variable to set.
Keys belong to the deployment that issued them: for a self-hosted or local backend, set
`FIANTO_BASE_URL`, or calls go to `https://api.fianto.xyz` and answer 401 `invalid_api_credentials`.

> **Never construct the client in a browser:**
>
> The app secret would ship to every visitor. There is no publishable key: create checkout sessions on
> your server and open them with [`@fianto/js`](/developers/sdks/js). `dangerouslyAllowBrowser: true`
> only turns off the check.

## Resources [#resources]

| Call                                                  | API route                                                           |
| ----------------------------------------------------- | ------------------------------------------------------------------- |
| `fianto.application.retrieve()`                       | `GET /v1/application`                                               |
| `fianto.checkoutSessions.create(params, options?)`    | `POST /v1/checkout-sessions`                                        |
| `fianto.checkoutSessions.retrieve(id)`                | `GET /v1/checkout-sessions/{id}`                                    |
| `fianto.checkoutSessions.cancel(id)`                  | `POST /v1/checkout-sessions/{id}/cancel`                            |
| `fianto.checkoutSessions.reissueLink(id)`             | `POST /v1/checkout-sessions/{id}/link`                              |
| `fianto.orders.retrieve(id)`, `.list(params?)`        | `GET /v1/orders/{id}`, `GET /v1/orders`                             |
| `fianto.orders.retrieveByOrderId(orderId)`            | `GET /v1/orders/lookup?order_id=`                                   |
| `fianto.payments.retrieve(id)`, `.list(params?)`      | `GET /v1/payments/{id}`, `GET /v1/payments`                         |
| `fianto.subscriptions.retrieve(id)`, `.list(params?)` | `GET /v1/subscriptions/{id}`, `GET /v1/subscriptions`               |
| `fianto.subscriptions.cancel(id, { at })`             | `POST /v1/subscriptions/{id}/cancel`, `at` is `now` or `period_end` |
| `fianto.products.retrieve(id)`, `.list(params?)`      | `GET /v1/products/{id}`, `GET /v1/products`                         |
| `fianto.prices.retrieve(id)`, `.list(params?)`        | `GET /v1/prices/{id}`, `GET /v1/prices`                             |
| `fianto.events.retrieve(id)`, `.list(params?)`        | `GET /v1/events/{id}`, `GET /v1/events`                             |
| `fianto.webhookEndpoint.sendTestEvent()`              | `POST /v1/webhook/test-event`                                       |

Ids that go into the path (every `retrieve`, `cancel` and `reissueLink`) are checked against
`^[A-Za-z0-9_]{1,64}$` before any request is sent. The `orderId` of `retrieveByOrderId` is your own
id, sent as a query parameter, and is not checked. Request and response
fields are the API's own, in snake_case: see [Checkout sessions](/developers/checkout-sessions) and
[Subscriptions](/developers/subscriptions).

`list()` returns a `PagePromise`: `await` it for one page (`{ items, next_cursor }`), or `for await`
it to walk every item. See [Pagination](/developers/pagination-and-rate-limits).

### Per-call options [#per-call-options]

Every method takes a last `options` argument:

| Option           | What it does                                                                                                       |
| ---------------- | ------------------------------------------------------------------------------------------------------------------ |
| `idempotencyKey` | The `Idempotency-Key` for a `POST`. Default: a `crypto.randomUUID()` made once per call and reused on its retries. |
| `timeoutMs`      | Overrides the client's timeout for this call.                                                                      |
| `signal`         | An `AbortSignal` that cancels the call.                                                                            |
| `maxRetries`     | Overrides the client's retry count for this call.                                                                  |

Every `retrieve(id, …)`, `checkoutSessions.cancel`, `reissueLink`, `application.retrieve` and
`sendTestEvent` also take a `params` object that is empty today and sits before `options`, so pass
`{}` to reach it. `subscriptions.cancel` is different: its `params` is the required `{ at }`.

```ts
import { Fianto } from '@fianto/sdk';

const fianto = new Fianto();

export async function cancelCheckout(sessionId: string) {
  // params is {} today; options comes after it.
  return fianto.checkoutSessions.cancel(sessionId, {}, { idempotencyKey: `cancel:${sessionId}` });
}
```

For crash-safe keys, see [Idempotency](/developers/idempotency).

## Amounts [#amounts]

The API takes amounts as decimal strings (`'12.50'`) and returns them as strings of USDC base units
with 6 decimals (`'12500000'`). `usdc` converts between the two without floating point:

```ts
import { usdc } from '@fianto/sdk';

usdc.toBaseUnits('12.5'); // '12500000'
usdc.fromBaseUnits('12500000'); // '12.5'
usdc.format('12500000'); // '12.50 USDC'
usdc.format('12500000', { symbol: false }); // '12.50'
```

`toBaseUnits` throws a `UsdcError` for more than 6 decimals, a sign, or anything that is not a number.

> **The amount is what you receive:**
>
> The `amount` you send is the price. When the service fee is above zero, it is added on top and the
> payer pays it; `total_amount` in the API is `amount` plus `fee_amount`.

## Errors and retries [#errors-and-retries]

Every error the SDK throws extends `FiantoError`. An error response becomes an `APIError` with
`status`, `code`, `message`, `field`, `details`, `requestId` and `headers`, in one subclass per status:

| Class                     | Status                        |
| ------------------------- | ----------------------------- |
| `AuthenticationError`     | 401                           |
| `PermissionDeniedError`   | 403                           |
| `InvalidRequestError`     | 400, 422                      |
| `NotFoundError`           | 404                           |
| `ConflictError`           | 409                           |
| `RateLimitError`          | 429, with `retryAfterSeconds` |
| `ServiceUnavailableError` | 503                           |
| `InternalServerError`     | other 5xx                     |

Next to them: `ConnectionError` (no response arrived, or the API answered with a redirect, which
the SDK never follows), `TimeoutError` (over `timeoutMs`),
`AbortError` (your `signal` aborted) and `UsdcError`. After a network failure on a `POST`,
`ConnectionError` and `TimeoutError` carry the `idempotencyKey` to retry with.

The SDK retries network errors, timeouts, 408, 429, every 5xx, and a 409 only for
`idempotency_request_in_progress` or `checkout_unavailable`. It waits for `Retry-After`, or backs off
from 500 ms, doubling up to 8 s, with ±25 % jitter. A `Retry-After` over 10 seconds is not waited
on: the error is thrown at once.

Check errors with `isFiantoError(err, ErrorCode.X)` or `isAPIError(err)` rather than `instanceof`.
An app that loads the SDK both as ESM and as CommonJS has two copies of every class, and
`instanceof` a subclass fails across them. See [Errors](/developers/errors) for the codes.

## `@fianto/sdk/webhooks` [#fiantosdkwebhooks]

Verifies and builds webhook requests without a framework.

| Export                                                            | What it does                                                                                                                                                               |
| ----------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `verifyWebhook(rawBody, headers, { secret?, toleranceSeconds? })` | Checks the signature and timestamp over the raw body and returns the typed event. `secret` is a string or an array during a roll, and defaults to `FIANTO_WEBHOOK_SECRET`. |
| `WebhookVerificationError`, `isWebhookVerificationError`          | The error `verifyWebhook` throws, with a `reason`.                                                                                                                         |
| `WEBHOOK_EVENT_TYPES`, `isKnownEventType(type)`                   | The event types this SDK version knows, and whether a type string is one of them.                                                                                          |
| `isEventType(event, type)`                                        | Narrows an event to one type.                                                                                                                                              |
| `signWebhook({ event, secret, id?, timestamp? })`                 | Signs a body the way Fianto does and returns `{ body, headers }`.                                                                                                          |
| `sampleEvent(type)`, `sampleVerificationEvent()`                  | A realistic fixture for each event type, and for the URL check.                                                                                                            |

The tolerance is 300 seconds by default, from 1 to 3,600, and cannot be switched off. The reasons
and the route code per framework are on [Verify webhooks](/developers/webhooks/verify). In a test,
sign a sample and post it to your own handler:

```ts
import { sampleEvent, signWebhook } from '@fianto/sdk/webhooks';

const { body, headers } = await signWebhook({ event: sampleEvent('order.paid'), secret: process.env.FIANTO_WEBHOOK_SECRET! });
await fetch('http://localhost:3000/api/webhooks/fianto', { method: 'POST', headers, body });
```

## `@fianto/sdk/handlers` [#fiantosdkhandlers]

`createWebhookHandler(options)` and `createCheckoutHandler(options)` return a
function that takes a `Request` and resolves to a `Response`. `Webhooks()` / `Checkout()` in Next.js and
`webhooks()` / `checkout()` in Express and Hono are these two handlers, adapted to the framework.

### Webhook handler options [#webhook-handler-options]

| Option                                       | Default                                       | What it does                                                                                                                             |
| -------------------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `secret`                                     | `FIANTO_WEBHOOK_SECRET`, read on each request | `whsec_…`, or an array during a roll. A given value is checked when the handler is created: an invalid one, `[]` included, throws there. |
| `toleranceSeconds`                           | `300`                                         | 1 to 3,600.                                                                                                                              |
| `maxBodyBytes`                               | `1048576` (1 MiB)                             | A larger body is answered 413 `{"error":"payload_too_large"}`.                                                                           |
| `onCheckoutSessionCompleted` … `onTestEvent` | —                                             | One callback per event type, 14 in all.                                                                                                  |
| `onEvent`                                    | —                                             | Runs after the type's own callback, for every verified event except the URL check.                                                       |
| `onVerificationError(error)`                 | —                                             | Why a request was refused. The response never says.                                                                                      |
| `onError(error, event)`                      | —                                             | A callback threw. The response is still 500, so Fianto retries.                                                                          |

The handler answers `endpoint.verification` itself, 200 `{"received": true}` for a handled event,
400 `{"error": "invalid_webhook"}` for a failed check, 500 `{"error": "handler_failed"}` when a
callback throws, and 405 for anything but `POST`.

### Checkout handler options [#checkout-handler-options]

| Option                            | Default                                                                 | What it does                                                                                                                                 |
| --------------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `createSession(request, context)` | required                                                                | Returns the session's params, decided on your server, or a `Response` to refuse. `ui_mode` is not accepted: the handler always uses `popup`. |
| `fianto`                          | `new Fianto()` from the `FIANTO_*` variables, made on the first request | Pass a client to set credentials yourself.                                                                                                   |
| `allowedOrigins`                  | the request URL's own origin                                            | Origins allowed to call the route when the browser does not send `Sec-Fetch-Site: same-origin`.                                              |
| `onError(error)`                  | —                                                                       | Receives the original error for every failure, including the ones the browser sees as 500.                                                   |

The handler accepts only `POST` (else 405 `method_not_allowed`), and only a same-origin request:
`Sec-Fetch-Site: same-origin`, or an `Origin` in `allowedOrigins` (else 403 `forbidden_origin`). It answers 200 `{ id, url }`. When the order already has an open session with
the same terms, it reissues that session's link; with different terms it answers 409
`order_session_mismatch` and leaves the open session alone.

Errors reach the browser as `{ error: { code, message } }` only for an allowlist of codes, each with
the SDK's own message: `payment_in_progress`, `order_already_paid`, `checkout_unavailable`,
`rate_limited`, `session_not_reissuable`, `subscription_preparing`, `plan_limit_reached`,
`order_session_mismatch`, and the validation codes for your own params (`validation_failed`,
`url_insecure`, `amount_or_price_required`, `amount_out_of_range`, `amount_not_allowed`,
`expires_at_out_of_range`, `mode_not_supported`, `price_id_required`, `price_not_found`,
`price_archived`, `product_archived`, `price_not_recurring`, `price_type_mismatch`). Everything
else, including a 401 or 403 from Fianto and every 5xx, answers 500 `internal_error`.

> **Wire onError:**
>
> A wrong app secret or an unapproved account reaches the browser only as 500 `internal_error`.
> Without `onError`, nothing on your side says why.

## See also [#see-also]

- [Verify webhooks](/developers/webhooks/verify): The handler per framework, and verifyWebhook by hand.

- [Errors](/developers/errors): The error body and every code.

- [Idempotency](/developers/idempotency): Stable keys for crash recovery.

- [Next.js](/developers/sdks/nextjs): The handlers as App Router routes.