FloPay
Examples

Delegated Mode (Custom Backend)

Use onTokenizedBody to handle payment processing on your own backend with full control over 3DS retry.

By default, CheckoutForm and SplitCardForm handle the entire payment flow internally -- tokenize, create intent, confirm, and call processPayment on the billing API. In delegated mode, you provide onTokenizedBody to intercept after tokenization and handle backend submission yourself.

This is useful when you need custom backend logic, additional validation, or want to call your own payment processing endpoint.

app/checkout/page.tsx
'use client';
 
import { useCallback, useMemo, useRef, useState } from 'react';
import { loadFloPay } from '@flopay/js';
import { FloPayProvider, CheckoutForm } from '@flopay/react';
import type { CheckoutFormRef } from '@flopay/react';
import type { TokenizedBody, PaymentResult } from '@flopay/shared';
 
const PUBLISHABLE_KEY = process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!;
const BILLING_API_URL = process.env.NEXT_PUBLIC_BILLING_API_URL!;
 
export default function DelegatedCheckoutPage() {
  const formRef = useRef<CheckoutFormRef>(null);
  const [processing, setProcessing] = useState(false);
  const [error, setError] = useState<string | null>(null);
 
  const floPayPromise = useMemo(
    () => loadFloPay(PUBLISHABLE_KEY),
    [],
  );
 
  const handleTokenizedBody = useCallback(
    async (body: TokenizedBody) => {
      setProcessing(true);
      setError(null);
 
      try {
        // Send the tokenized payment data to your own backend
        const response = await fetch('/api/process-payment', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            paymentMethodId: body.paymentMethodId,
            sessionId: body.sessionId,
            email: body.email,
            // Add any custom fields your backend needs
          }),
        });
 
        const result = await response.json();
 
        if (result.status === '3ds_required' && result.clientSecret) {
          // The backend requires 3DS authentication.
          // Use the ref to trigger the 3DS challenge modal.
          await formRef.current?.handleNextAction(result.clientSecret);
 
          // After 3DS, retry the backend call
          const retryResponse = await fetch('/api/process-payment', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              paymentMethodId: body.paymentMethodId,
              sessionId: body.sessionId,
              confirmed: true,
            }),
          });
 
          const retryResult = await retryResponse.json();
          if (retryResult.status === 'succeeded') {
            window.location.href = '/success';
          }
        } else if (result.status === 'succeeded') {
          window.location.href = '/success';
        } else {
          setError(result.error ?? 'Payment failed');
        }
      } catch (err) {
        setError(
          err instanceof Error ? err.message : 'Payment processing failed',
        );
      } finally {
        setProcessing(false);
      }
    },
    [],
  );
 
  return (
    <FloPayProvider
      flopay={floPayPromise}
      options={{ currency: 'usd', amount: 4999 }}
    >
      <CheckoutForm
        ref={formRef}
        sessionId="your-session-uuid"
        billingApiUrl={BILLING_API_URL}
        email="customer@example.com"
        userId="user_123"
        onTokenizedBody={handleTokenizedBody}
        isProcessing={processing}
        error={error}
        submitLabel="Pay $49.99"
      />
    </FloPayProvider>
  );
}

How It Works

  1. The user fills in card details and clicks submit
  2. CheckoutForm validates the fields, tokenizes the card, and confirms the PaymentIntent
  3. Instead of calling processPayment internally, it calls your onTokenizedBody callback with the TokenizedBody
  4. You send the data to your own backend
  5. If your backend returns 3ds_required, call formRef.current.handleNextAction(clientSecret) to show the 3DS challenge
  6. After 3DS completes, retry your backend call

Key Props for Delegated Mode

PropDescription
onTokenizedBodyCallback receiving the tokenized payment data. Enables delegated mode.
isProcessingControls the form's loading state externally (disables submit button).
errorDisplays an error message in the form from your external logic.
refExposes handleNextAction(clientSecret) for triggering 3DS challenges.

The same pattern works with SplitCardForm -- it accepts the same onTokenizedBody, isProcessing, error, and ref props.

On this page