The checkout flow connects your payment form with FunnelFox Billing’s subscription engine. When customers purchase subscriptions, the system generates an Order ID that links the payment with the subscription being created.

Build your checkout flow

To implement checkout, you’ll need to build a checkout form that integrates two components:
  • Primer Universal Checkout
  • FunnelFox Billing API
Check our implementation example with complete integration.

1. Integrate Primer Universal Checkout

First, integrate Primer Universal Checkout to handle payment collection and processing. Follow the Primer documentation on configuring Universal Checkout.

2. Connect FunnelFox Billing API

Your checkout form needs to communicate with FunnelFox Billing at key points during the payment process. Follow FunnelFox Billing API documentation and add calls to the endpoints below:
1

Create client session

When a user selects a product, create a client session by calling //v1/checkout/create_client_session. Include:
  • metadata — any extra fields you want to pass to your Primer workflow (analytics, internal logic).
  • email — the user’s email address.
  • external_id — the user identifier from your system.
  • pp_ident — the price point identifier for the selected product.
The response returns an order_id. Use this in later requests.
2

Update client session

If the user changes products on the same page, update the session by calling //v1/checkout/update_client_session with the new pp_ident.You don’t need to start over — you’re updating the same checkout session.
3

Create payment

After the user enters payment details, create a payment via //v1/checkout/create_payment, passing:
  • order_id — the current order ID.
  • payment_method_token — the token from the Primer SDK.
If additional authentication is required (e.g., 3DS), the response includes an action_required_token. Hand this off to the Primer SDK and, once completed, resume the payment.
4

Resume payment

After the 3DS/auth step completes, call //v1/checkout/resume_payment to finish processing.
5

Special case: multiple payments in one session

Sometimes a customer retries payment in the same session (e.g., first card is declined, then they try a different card). In that case, the second attempt can return a new order_id. Make sure you use the updated order_id when resuming the payment after 3DS.
The Order ID connects a payment in Primer with the subscription being created. It’s generated during the create_client_session step and used as the payment identifier in the API. The Order ID can be updated within a single checkout form. This happens when the first payment attempt fails and the customer retries. Each retry is treated as a new payment, generating a new Order ID. You’ll need this updated Order ID for additional steps like 3D Secure dialogs.

Implementation example

Here’s a complete implementation that demonstrates the integration:
Checkout form
 <!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Primer Test (sndbx) - Demo</title>
  <link rel="stylesheet" href="https://sdk.primer.io/web/v2.0.0/Checkout.css" />
  <script src="https://sdk.primer.io/web/v2.54.0/Primer.min.js" crossorigin="anonymous"></script>
</head>
<body>



<div id="checkout-container"></div>

<script>

checkout_container = document.createElement("div");
checkout_container.id = 'checkout-container'

var order_id = undefined
var clientToken = undefined
var pp_ident = ''
var order_id = undefined
var checkout = undefined


function processResponse(data, handler) {
  if (data.status == 'error') {
    return handler.handleFailure(JSON.stringify(data, null, 2))
  } else if (data.data.action_required_token) {
    order_id = data.data.order_id
    return handler.continueWithNewClientToken(data.data.action_required_token)
  } else if (data.data.checkout_status == 'failed') {
    return handler.handleFailure(
      'payment failed. Reason:' + data.data.failed_message_for_user
    )
  } else if (data.data.checkout_status == 'succeeded') {

    const container = document.getElementById('checkout-container');
    const link = document.createElement('a');
    link.href = `https://billing.funnelfox.com/${orgId}/admin/support_tool?external_id=` + external_id;
    link.textContent = 'check it in Support Tool';
    link.target = '_blank';
    container.appendChild(link);
    document.querySelectorAll('.startblock').forEach(el => el.remove());

    return handler.handleSuccess()
  } else if (data.data.checkout_status == 'cancelled') {
    return handler.handleFailure(
      'Payment cancelled.  it is expected case for FREE TRIAL. Or it is a case of: target bank rejected payment'
    )
  } else if (data.data.checkout_status == 'processing') {
    return handler.handleFailure(
      'For some reason payment is not finished yet.  ' +
      'Need to wait  or check logs. ' +
      'Usualy broken  PrimerWrokwlow is a reason.'
    )
  } else {
    alert('Need to fix FE')
    return handler.handleFailure('unpocessed status' + JSON.stringify(data, null, 2))
  }
}


async function initCheckout(ident) {
  checkout_container.style.padding = '10px'
  if (!pp_ident) {
    pp_ident = ident
    const res = await fetch(`https://billing.funnelfox.com/${orgId}/v1/checkout/create_client_session`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        region: 'default',
        integration_type: 'primer',
        pp_ident: ident,
        external_id: external_id,
        email_address: email,
        client_metadata: {test_client_metadata: "xxxxx"}
      })
    });
    const data = await res.json();
    clientToken = data.data.client_token;
    order_id = data.data.order_id;

    checkout = await Primer.showUniversalCheckout(clientToken, {
      clientToken,
      container: '#checkout-container',
      paymentHandling: 'MANUAL',
      apiVersion: '2.4',
      paypal: {
        buttonColor: 'blue',
        paymentFlow: 'PREFER_VAULT',
      },
      async onTokenizeSuccess(paymentMethodTokenData, handler) {
        const res = await fetch(`https://billing.funnelfox.com/${orgId}/v1/checkout/create_payment`, {
          method: 'POST',
          body: JSON.stringify({
            order_id: order_id,
            payment_method_token: paymentMethodTokenData.token,
          })
        });
        const data = await res.json();
        if (!res) {
          alert('api response failed..')
          return handler.handleFailure('The payment failed. Please try with another payment method.')
        }
        processResponse(data, handler)

      },
      async onResumeSuccess(resumeTokenData, handler) {
        const res = await fetch(`https://billing.funnelfox.com/${orgId}/v1/checkout/resume_payment`, {
          method: 'POST',
          body: JSON.stringify({
            resume_token: resumeTokenData.resumeToken,
            order_id: order_id,
          })
        });
        const data = await res.json();
        if (!res) {
          alert('api response failed..')
          return handler.handleFailure('Api response failed.')
        }
        processResponse(data, handler)
      },
    })
  } else {
    pp_ident = ident
    const res = await fetch(`https://billing.funnelfox.com/${orgId}/v1/checkout/update_client_session`, {
      method: 'POST',
      body: JSON.stringify({
        order_id: order_id,
        client_token: clientToken,
        pp_ident: ident,
      })
    });
    console.info('Refresh session');
  }

}

const orgId = "primer_test";
const external_id = crypto.randomUUID()
const email = crypto.randomUUID().slice(0, 8) + '@example.com'
initCheckout('MyNewPricePlan')
</script>
</body>


</html>

Next steps

Now that you’ve successfully integrated and set up your checkout, you’re ready to Manage subscriptions.