@flopay/node
Server-side FloPay SDK for Node.js. Creates checkout sessions through the FloPay billing API, retrieves and expires Stripe checkout sessions, manages Stripe customers, and verifies Stripe webhooks.
Installation
pnpm add @flopay/node
stripe and @flopay/shared install automatically as dependencies. Use it only from trusted server code: the constructor takes a Stripe secret key, which must never reach a browser.
Node.js version
Run @flopay/node on a Node.js release patched for CVE-2026-111497 ("Node2Shell"), an out-of-bounds write in the Brotli decoder of Node.js itself. The Node.js security release of 21 October 2026 fixed it in 22.23.4, 24.22.1 and 26.12.1, so run 22.23.4+ on 22.x, 24.22.1+ on 24.x, or 26.12.1+ on 26.x.
20.x (end of life) and 25.x get no fix: move off them to a patched 22.x, 24.x or 26.x release. The supported range, Node.js 20.19+, 22.13+ and 24+, still includes both lines, so nothing in the package stops you running on them.
Usage
Initialize
Create one client with your Stripe secret key. Direct Stripe operations use the API version in STRIPE_API_VERSION unless you pass apiVersion; pass stripeSecretKey to use a different key for them.
import { FloPay } from '@flopay/node';
const flopay = new FloPay('sk_test_...'); // load the real key from your secret store
Create a checkout session
flopay.checkout.sessions.create() posts to the billing API's POST /v1/checkouts/sessions and resolves with a CheckoutSessionResult:
{ status: 201, redirectUrl, nonce }: send the buyer toredirectUrl, the hosted checkout page (checkoutBaseUrl+/secure, with the session id and yourredirectParams).nonceis the session-bound checkout token; forward it as thex-checkout-session-tokenheader on every continuation call for the same session.{ status: 204 }: the buyer already has a payment method on file and nothing needs to be shown.
The session currency is required. Pass currency on the session, or on the first item, subscription, or product. checkoutMode defaults to 'confirm' on this method, and timeoutMs to 12 seconds.
checkoutMetadata accepts Record<string, string> | null and is sent unchanged: omitting it leaves the request body unchanged, an explicit null is preserved, and {} stays an empty map. The billing API is the only authority on metadata limits.
Retry safely with an idempotency key
Pass the same idempotencyKey when you retry one logical session creation after an unknown outcome. A non-blank key of at most 255 characters is sent unchanged as the Idempotency-Key header. Empty, whitespace-only, overlong, and header-unsafe values are ignored without throwing, and no key is generated when you omit the option. @flopay/node has no internal retry loop, so your server owns retry timing and key reuse.
Authorise now, capture later
To authorise an eligible one-time card cart without capturing it, pass captureMethod: 'manual'. Manual capture cannot be combined with subscriptions, and omitting the option keeps immediate capture. The later capture is deliberately not an @flopay/node method: call the merchant-authenticated REST endpoint from trusted server code with a stable Idempotency-Key, as described in the payments and authorisations reference.
Saved cards
Saved-card management is also not an @flopay/node credential surface. From trusted server code, use the authenticated REST API to list display-only payment methods, create a setup session, and delete an eligible method, then pass only the setup session's sessionId and nonce to FloPayCardSetup in @flopay/react. Replace a card setup-first, then delete, so a failed or cancelled setup leaves the existing card intact. See the payment methods reference.
Type the setup request body with CreateCardSetupSessionRequest from @flopay/shared. Alongside the required userId and the optional successUrl and cancelUrl, it accepts transaction context your server may already know, which the billing API routes card verification on. Each field is optional and independent of the others, and a request without any of them stays valid and unchanged:
country: the ISO 3166-1 alpha-2 country the card will be used in.currency: the three-letter currency later payments will be charged in. A suppliedcurrencybecomes the setup session's currency.futureUse: deprecated (CardSetupFutureUse,'on_session'or'off_session'). Future use is platform policy: the billing API sets every saved method up'off_session'and ignores this value. It is still accepted so existing requests keep working; leave it out of new code.
The context needs billing API v1.9.48 or later. Older billing APIs silently ignore it and set the card up exactly as before. Send values as you hold them: the billing API trims and uppercases country and currency, validates all three fields, and rejects an invalid value with a 400. The context describes the transaction and never selects a gateway; the billing API alone decides which gateway verifies the card.
Saved payment methods
Customers can keep saved payment methods of any type, not only cards, and the server-side flow is the same one as for saved cards. Merchant credentials stay on the merchant server: your server creates the setup session through its own authenticated billing API client, and no FloPay package sends the setup request. Type the body with CreateCardSetupSessionRequest from @flopay/shared exactly as for a card. Saving another method type needs no extra field, and the request carries no gateway hint, provider allow-list, or eligibility table. The billing API advertises which methods a setup session can save, and the SDK never decides which gateway or method is eligible.
@flopay/shared types the mixed method types for list and delete:
SavedPaymentMethodFamilyis'card','wallet','paypal', or'apm': the family a saved method belongs to, for grouping.SAVED_PAYMENT_METHOD_FAMILIESlists every value for exhaustive handling.SavedPaymentMethodTypeandSAVED_PAYMENT_METHOD_TYPESare deprecated aliases with the same values.SavedPaymentMethodDisplayis one display-only saved method of any type, discriminated by its familytype. Every variant has anidand a lifecyclestatus, and may carry the billing API row'smethodTypeanddisplayLabel.methodTypetells methods of one family apart: an Apple Pay row and a Google Pay row are bothtype: 'wallet', withmethodTypeapple_payandgoogle_pay. A wallet, PayPal, or APM method always has a safe displaylabel, deprecated in favour ofdisplayLabel, which holds the same value. A card hasbrand,lastFour,expiryMonth, andexpiryYear, itsmethodTypeiscard, and itslabelis optional and itsdisplayLabelisnull: build the display text frombrandandlastFourinstead, as the example below does. No variant carries a reusable credential, token, or card number. An existingSavedCardDisplayis a validSavedPaymentMethodDisplaywithout atype, so card-only code keeps compiling.ListSavedPaymentMethodsQueryis the list request's optionaltypefilter, aSavedPaymentMethodListType. Leave it out to list every saved method, as card-only integrations do today.SavedPaymentMethodListTypeis a method type the billing API accepts for that filter:card,paypal,apple_pay,google_pay,sepa_debit, orus_bank_account, andSAVED_PAYMENT_METHOD_LIST_TYPESlists every value. It filters by method type, never by family, so there is nowalletorapmfilter: ask forapple_payorgoogle_payinstead. PayPal ispaypal, not the setup flow'spaypal_vaulted.isSetupCheckoutSessionistruefor a no-charge setup session (checkoutMode: 'setup', zero amount, no products), whatever methods itsadvertisedPaymentMethodslist.isCardSetupCheckoutSessionis unchanged.
Deleting a saved method works the same way for every type, with the same PaymentMethodRemovalError responses.
Stripe checkout sessions and customers
checkout.sessions.retrieve(), expire(), and listLineItems() call Stripe directly and normalize the result. customers.create(), retrieve(), and update() manage Stripe customers and return a normalized Customer.
Verify webhooks
webhooks.constructEvent() verifies the Stripe-Signature header against your webhook signing secret and returns a normalized WebhookEvent (id, type, data, created). Pass the raw, unparsed request body.
Examples
Create a checkout session and redirect the buyer:
import { FloPay } from '@flopay/node';
function requiredEnv(name: string): string {
const value = process.env[name];
if (!value) throw new Error(`${name} is not set`);
return value;
}
const flopay = new FloPay(requiredEnv('STRIPE_SECRET_KEY'));
const result = await flopay.checkout.sessions.create({
billingApiUrl: 'https://billing.example.com',
checkoutBaseUrl: 'https://checkout.example.com',
clientId: 'client_123',
currency: 'USD',
items: [
{
providerItemId: 'prod_abc',
providerItemName: 'Pro Plan',
totalAmount: 49.99,
overrideAmount: 24.99,
},
],
subscriptions: [{ providerPlanId: 'plan_xyz', providerPlanName: 'Monthly Plan', totalAmount: 9.99 }],
account: { userId: 'user_1', email: '[email protected]', firstName: 'Jane', lastName: 'Doe' },
successUrl: 'https://shop.example.com/success',
cancelUrl: 'https://shop.example.com/cancel',
checkoutMode: 'full',
checkoutMetadata: { merchantOrderId: 'order-123', campaign: 'summer' },
couponCodes: ['SAVE10'],
tagsData: { googleContainerId: 'GTM-XXXX' },
redirectParams: { email: '[email protected]' },
timeoutMs: 12_000,
idempotencyKey: 'checkout:order-123',
});
if (result.status === 201 && 'redirectUrl' in result) {
console.log('Redirect the buyer to', result.redirectUrl);
} else if (result.status === 204) {
console.log('A payment method is already on file.');
}
Type a card-setup request with optional transaction context, send it with the billing API client your server already authenticates, and return only the session handle for FloPayCardSetup:
import type { CreateCardSetupSessionRequest } from '@flopay/shared';
// Your server's existing billing API client; it adds your merchant authentication.
type AuthenticatedPost = (
path: string,
body: CreateCardSetupSessionRequest,
) => Promise<{ data: { uuid: string; nonce: string } }>;
export async function createCardSetupSession(post: AuthenticatedPost, userId: string) {
const request: CreateCardSetupSessionRequest = {
userId,
country: 'GB',
currency: 'GBP',
};
const { data } = await post('/v1/checkouts/sessions/setup', request);
return { sessionId: data.uuid, nonce: data.nonce };
}
List saved payment methods of any type, optionally filtered by method type, and label each one without branching on a gateway:
import type { ListSavedPaymentMethodsQuery, SavedPaymentMethodDisplay } from '@flopay/shared';
// Your server's existing billing API client; it adds your merchant authentication.
type AuthenticatedList = (query: ListSavedPaymentMethodsQuery) => Promise<SavedPaymentMethodDisplay[]>;
export function describeSavedMethod(method: SavedPaymentMethodDisplay): string {
switch (method.type) {
case 'wallet':
case 'paypal':
case 'apm':
return method.displayLabel ?? method.label;
default:
// A card; card-only responses may omit `type`, and a card's `displayLabel` is null.
return method.displayLabel ?? method.label ?? `${method.brand} ending ${method.lastFour}`;
}
}
export async function listSavedApplePay(list: AuthenticatedList): Promise<string[]> {
// Filter by method type: the billing API accepts `apple_pay`, never the `wallet` family.
const methods = await list({ type: 'apple_pay' });
return methods.map(describeSavedMethod);
}
Verify a Stripe webhook in a plain Node.js HTTP server:
import { createServer } from 'node:http';
import { FloPay } from '@flopay/node';
const flopay = new FloPay(process.env.STRIPE_SECRET_KEY ?? 'sk_test_...');
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET ?? 'whsec_...';
const MAX_BODY_BYTES = 64 * 1024; // Pick a limit that fits your largest expected event.
createServer((request, response) => {
const chunks: Buffer[] = [];
let received = 0;
request.on('data', (chunk: Buffer) => {
received += chunk.length;
if (received > MAX_BODY_BYTES) {
response.writeHead(413, { Connection: 'close' }).end('Payload too large');
request.destroy();
return;
}
chunks.push(chunk);
});
request.on('end', () => {
try {
const event = flopay.webhooks.constructEvent(
Buffer.concat(chunks),
String(request.headers['stripe-signature']),
webhookSecret,
);
console.log(event.type, event.created);
response.writeHead(200).end();
} catch {
response.writeHead(400).end('Invalid signature');
}
});
}).listen(3000);
Manage Stripe customers:
import { FloPay } from '@flopay/node';
const flopay = new FloPay(process.env.STRIPE_SECRET_KEY ?? 'sk_test_...');
const customer = await flopay.customers.create({
email: '[email protected]',
name: 'Jane Doe',
metadata: { plan: 'pro' },
});
const updated = await flopay.customers.update(customer.id, { metadata: { plan: 'enterprise' } });
const session = await flopay.checkout.sessions.retrieve('cs_test_...');
console.log(updated.email, session.status, session.amount);
Public API
Exports
| Export | Description |
|---|---|
FloPay | Server-side client: new FloPay(secretKey, options?) with a Stripe secret key (sk_test_... or sk_live_...) |
FloPayNodeOptions | Constructor options |
STRIPE_API_VERSION | '2026-04-22.dahlia', the default Stripe API version for direct Stripe operations |
FloPayNodeOptions
| Option | Type | Description |
|---|---|---|
apiVersion | string? | Stripe API version override for direct Stripe operations |
stripeSecretKey | string? | Stripe secret key for direct Stripe operations, instead of the constructor key |
FloPay methods
| Method | Returns | Description |
|---|---|---|
checkout.sessions.create(params) | Promise<CheckoutSessionResult> | Creates a session through the billing API (POST /v1/checkouts/sessions). Accepts CreateSessionParams. |
checkout.sessions.retrieve(id) | Promise<CheckoutSession> | Retrieves and normalizes a Stripe checkout session |
checkout.sessions.expire(id) | Promise<CheckoutSession> | Expires a Stripe checkout session |
checkout.sessions.listLineItems(id) | Promise<LineItem[]> | Lists a Stripe checkout session's line items |
customers.create(params) | Promise<Customer> | Creates a Stripe customer |
customers.retrieve(id) | Promise<Customer> | Retrieves a Stripe customer |
customers.update(id, params) | Promise<Customer> | Updates a Stripe customer |
webhooks.constructEvent(payload, signature, secret) | WebhookEvent | Verifies and parses a Stripe webhook event |
The parameter and result types (CreateSessionParams, CheckoutSessionResult, CheckoutSession, Customer, LineItem, WebhookEvent) are exported by @flopay/shared.
@flopay/node has no card-setup method and holds no merchant credential. CreateCardSetupSessionRequest and CardSetupFutureUse, also exported by @flopay/shared, type the body your server sends to the authenticated POST /v1/checkouts/sessions/setup route with its own billing API client.
Saved payment methods of any type are typed by @flopay/shared too: SavedPaymentMethodDisplay, SavedPaymentMethodFamily (with the deprecated alias SavedPaymentMethodType), ListSavedPaymentMethodsQuery and its SavedPaymentMethodListType filter values, AdvertisedPaymentMethod, and the isSetupCheckoutSession guard. @flopay/node adds no list or delete method for them.
Errors
SDK validation and billing API failures are FloPayErrors from @flopay/shared; branch on type and code.
| Code | Type | When |
|---|---|---|
CurrencyRequired | validation_error | checkout.sessions.create() could not resolve a session currency. Thrown before any request, with param: 'currency'. |
CaptureMethodUnsupportedForSubscription | validation_error | captureMethod: 'manual' was requested for a cart with a subscription. Thrown before any request. |
The billing API's code, or http_<status> | api_error | The billing API rejected session creation. The error keeps the billing API's message and code, with the HTTP status in statusCode. |
MissingCheckoutSessionToken | api_error | The billing API created a session but returned no nonce, which means it is too old for this SDK. |
resource_missing | api_error | customers.retrieve() found a deleted customer. |
Constructing FloPay without a secret key throws an authentication_error. A create request that exceeds timeoutMs is aborted, and network failures reject with the underlying fetch error rather than a FloPayError. Stripe operations, including webhook signature verification, reject with Stripe's own errors. Treat a failed constructEvent() as an untrusted request.