Node.js Quick Start
Add the optional Node backend pieces for an embedded FloPayCheckout integration.
Node.js Quick Start
For the recommended embedded flow, the browser creates the checkout session inline through FloPayCheckout createSession. You do not need a Node endpoint just to create a session ahead of time.
Use @flopay/node when you need server-side responsibilities such as:
- Verifying Flo webhook deliveries
- Fulfilling orders or activating subscriptions after payment
- Managing customers
- Supporting an older hosted/session-ID-based flow
If you're following the React Quick Start, webhook verification is usually the first backend step to add. Session creation can stay on the client.
1. Configure Secrets
STRIPE_SECRET_KEY=sk_test_...
FLO_WEBHOOK_SECRET=flo_whsec_...2. Initialize the SDK
import { FloPay } from '@flopay/node';
export const flopay = new FloPay(process.env.STRIPE_SECRET_KEY!);3. Verify Webhooks
// app/api/webhooks/route.ts
import { createHmac, timingSafeEqual } from 'node:crypto';
import { NextResponse } from 'next/server';
const webhookSecret = process.env.FLO_WEBHOOK_SECRET!;
const SIGNATURE_TOLERANCE_SECONDS = 300;
// Back this with a durable transaction and a UNIQUE eventId constraint. Return
// false when the event and its business side effects were already committed.
declare function processWebhookOnce(
eventId: string,
handler: () => Promise<void>,
): Promise<boolean>;
function verifyFloSignature(rawBody: string, header: string, secret: string) {
const values = Object.fromEntries(
header.split(',').map((part) => {
const entry = part.trim();
const separator = entry.indexOf('=');
return separator === -1
? [entry, '']
: [entry.slice(0, separator), entry.slice(separator + 1)];
}),
);
if (!values.t || !values.v1) return false;
const timestamp = Number(values.t);
const now = Math.floor(Date.now() / 1000);
if (
!Number.isSafeInteger(timestamp) ||
Math.abs(now - timestamp) > SIGNATURE_TOLERANCE_SECONDS
) {
return false;
}
const expected = createHmac('sha256', secret)
.update(`${values.t}.${rawBody}`)
.digest('hex');
const expectedBytes = Buffer.from(expected, 'hex');
const suppliedBytes = Buffer.from(values.v1, 'hex');
return (
expectedBytes.length === suppliedBytes.length &&
timingSafeEqual(expectedBytes, suppliedBytes)
);
}
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get('flo-signature');
if (!signature) {
return NextResponse.json({ error: 'Missing Flo signature' }, { status: 400 });
}
if (!verifyFloSignature(body, signature, webhookSecret)) {
return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
}
const event = JSON.parse(body);
const processed = await processWebhookOnce(event.eventId, async () => {
switch (event.eventType) {
case 'item.purchased':
// Fulfil the captured one-time item idempotently.
break;
case 'payment.capture_failed':
// Keep the order unfulfilled and reconcile payment state.
break;
default:
console.log('Unhandled event type:', event.eventType);
}
});
if (!processed) return NextResponse.json({ received: true });
return NextResponse.json({ received: true });
}Read the webhook request body as raw text (request.text()), not parsed JSON. Signature verification requires the original bytes. Keep server clocks synchronized; this example rejects signatures more than five minutes from the current time.
4. How It Fits the Embedded Flow
- Your React app renders
FloPayCheckoutwithcreateSession - The component creates the billing session directly and renders the embedded payment form
- The customer completes payment on the page
- Flo sends a webhook to your Node endpoint
- Your server fulfills the order and records the final payment state
5. Need Server-Created Sessions?
If you still want a pre-created session or hosted redirect flow, @flopay/node still supports it:
const result = await flopay.checkout.sessions.create({
billingApiUrl: 'https://api.stage.flopay.com',
checkoutBaseUrl: 'https://checkout.example.com',
clientId: 'client_123',
currency: 'USD',
products: [
{
code: 'pro_plan',
name: 'Pro Plan',
totalAmount: 49.99,
},
],
account: {
userId: 'user_1',
email: 'user@example.com',
},
successUrl: 'https://example.com/success',
cancelUrl: 'https://example.com/cancel',
});That flow still works, but it is no longer the main quick-start path for embedded checkout.
This remains an immediate capture example and immediate capture is the default. Omitting captureMethod preserves the existing charge-at-checkout behavior. For authorise-now/capture-later, follow the pre-authorisation guide and capture only from a trusted server.
Next Steps
- React Quick Start — embed checkout inline with
createSession - Webhooks — full webhook handling examples
- Pre-authorisation and capture — trusted-server capture and fulfilment rules
- Saved-card management — trusted-server card maintenance
- FloPayCheckout Guide — embedded checkout options
- Node API Reference — all server-side methods