> ## Documentation Index
> Fetch the complete documentation index at: https://funnelfox.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Checkout flow

> Configure the subscription engine checkout flow in FunnelFox. Payment processing steps, validation, and completion handling.

<Danger>This is a deprecated flow to build your checkout manually. Integrate our [Billing Web SDK](/docs/develop/web-sdk-quickstart) instead.</Danger>

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**

<Tip>Check our [implementation example](/docs/billing/subscription-engine-checkout-flow#implementation-example) with complete integration.</Tip>

### 1. Integrate Primer Universal Checkout

First, integrate Primer Universal Checkout to handle payment collection and processing.

Follow the [Primer documentation on configuring Universal Checkout](https://primer.io/docs/checkout/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](/docs/develop/api-billing) and add calls to the endpoints below:

<Steps titleSize="h3">
  <Step title="Create client session">
    When a user selects a product, create a client session by calling
    <a href="https://funnelfox.com/docs/api-reference/checkout/create-a-checkout-session-client-token"><code>/{org_id}/v1/checkout/create\_client\_session</code></a>.
    Include:

    <ul>
      <li><code>metadata</code> — any extra fields you want to pass to your Primer workflow (analytics, internal logic).</li>
      <li><code>email</code> — the user’s email address.</li>
      <li><code>external\_id</code> — the user identifier from your system.</li>
      <li><code>pp\_ident</code> — the price point identifier for the selected product.</li>
    </ul>

    The response returns an <code>order\_id</code>. Use this in later requests.
  </Step>

  <Step title="Update client session">
    If the user changes products on the same page, update the session by calling
    <a href="https://funnelfox.com/docs/api-reference/checkout/update-a-checkout-session"><code>/{org_id}/v1/checkout/update\_client\_session</code></a>
    with the new <code>pp\_ident</code>.

    You don’t need to start over — you’re updating the same checkout session.
  </Step>

  <Step title="Create payment">
    After the user enters payment details, create a payment via
    <a href="https://funnelfox.com/docs/api-reference/checkout/complete-payment-for-an-order"><code>/{org_id}/v1/checkout/create\_payment</code></a>, passing:

    <ul>
      <li><code>order\_id</code> — the current order ID.</li>
      <li><code>payment\_method\_token</code> — the token from the Primer SDK.</li>
    </ul>

    If additional authentication is required (e.g., 3DS), the response includes an <code>action\_required\_token</code>. Hand this off to the Primer SDK and, once completed, resume the payment.
  </Step>

  <Step title="Resume payment">
    After the 3DS/auth step completes, call
    <a href="https://funnelfox.com/docs/api-reference/checkout/resume-a-pending-payment"><code>/{org_id}/v1/checkout/resume\_payment</code></a>
    to finish processing.
  </Step>

  <Step title="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 <strong>new</strong> <code>order\_id</code>. Make sure you use the updated <code>order\_id</code> when resuming the payment after 3DS.
  </Step>
</Steps>

The Order ID connects a payment in Primer with the subscription being created. It's generated during the <a href="https://funnelfox.com/docs/api-reference/checkout/create-a-checkout-session-client-token"><code>create\_client\_session</code></a> 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:

```javascript Checkout form expandable theme={null}
 <!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](/docs/billing/subscription-engine-subscription-management).
