DevelopersTools

@fianto/sdk

The server SDK - constructor options and env, resources, per-call options, the webhooks and handlers subpaths, errors and retries.

6 min read

@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/sdk

Create 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());
OptionEnv variableDefaultNotes
appIdFIANTO_APP_IDnone, requiredfian_app_…
appSecretFIANTO_APP_SECRETnone, requiredfian_sk_live_…. Server only.
baseUrlFIANTO_BASE_URLhttps://api.fianto.xyzhttps, except localhost, 127.0.0.1 and [::1]. Without /v1: the SDK adds it.
timeoutMs—30000Per attempt, 1 to 600000.
maxRetries—2Retries after the first attempt, 0 to 10.
fetch—the global fetchYour own fetch, for custom networking or tests.
dangerouslyAllowBrowser—falseWithout 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

CallAPI 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:

OptionWhat it does
idempotencyKeyThe Idempotency-Key for a POST. Default: a crypto.randomUUID() made once per call and reused on its retries.
timeoutMsOverrides the client's timeout for this call.
signalAn AbortSignal that cancels the call.
maxRetriesOverrides 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:

ClassStatus
AuthenticationError401
PermissionDeniedError403
InvalidRequestError400, 422
NotFoundError404
ConflictError409
RateLimitError429, with retryAfterSeconds
ServiceUnavailableError503
InternalServerErrorother 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.

ExportWhat 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, isWebhookVerificationErrorThe 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

OptionDefaultWhat it does
secretFIANTO_WEBHOOK_SECRET, read on each requestwhsec_…, or an array during a roll. A given value is checked when the handler is created: an invalid one, [] included, throws there.
toleranceSeconds3001 to 3,600.
maxBodyBytes1048576 (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

OptionDefaultWhat it does
createSession(request, context)requiredReturns the session's params, decided on your server, or a Response to refuse. ui_mode is not accepted: the handler always uses popup.
fiantonew Fianto() from the FIANTO_* variables, made on the first requestPass a client to set credentials yourself.
allowedOriginsthe request URL's own originOrigins 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

On this page