DevelopersTools
@fianto/sdk
The server SDK - constructor options and env, resources, per-call options, the webhooks and handlers subpaths, errors and retries.
@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 add @fianto/sdkCreate the client
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. dangerouslyAllowBrowser: true
only turns off the check.
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 and
Subscriptions.
list() returns a PagePromise: await it for one page ({ items, next_cursor }), or for await
it to walk every item. See Pagination.
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 }.
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.
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:
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
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 for the codes.
@fianto/sdk/webhooks
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. In a test, sign a sample and post it to your own handler:
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
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
| 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
| 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
Was this page helpful? Tell us