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

# Migrate the manual flow to Primer Checkout

> Move from the legacy manual payment creation flow to backend-driven manual approval.

<Note>
  **New feature.** Manual payment approval is available to all merchants; no allowlisting or account setup is required. Because it's new, **test your integration end to end in the [sandbox](/docs/testing/overview) before going live in production.** Available on the **Web** SDK today; iOS and Android are coming.
</Note>

If your legacy integration uses the **manual payment creation** flow (`paymentHandling: 'MANUAL'` with `onTokenizeSuccess` / `onResumeSuccess` callbacks), this guide covers the changes needed to move to the equivalent flow on Primer Checkout: **[manual payment approval](/docs/checkout/primer-checkout/guides-and-recipes/manual-payment-approval)**.

<Note>
  This is a focused companion to the main [Migration Guide](/docs/checkout/primer-checkout/migration/overview). Start there for the SDK-wide changes (package, initialization, events, styling); this page covers only what's specific to the manual flow.
</Note>

## What changes

The core shift: in the legacy flow your backend **owned the payment lifecycle**: it created the payment, inspected `requiredAction`, and routed each step (3DS, resume) back to the SDK. In the new flow your backend makes a single **approve-or-abort decision**, and Primer owns everything after that.

| Concern                | Legacy manual flow                                                                     | New manual approval flow                                   |
| ---------------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| Enabling the flow      | `paymentHandling: 'MANUAL'` (SDK setting)                                              | `approvalMode: 'MANUAL'` (client session field)            |
| Trigger                | `onTokenizeSuccess(token, handler)` callback                                           | `primer:payment-approval-required` event                   |
| Your backend call      | `POST /payments` (then `POST /payments/{id}/resume`)                                   | `POST /payment-attempts/{id}/approve` (or `/abort`)        |
| Creating the payment   | You create it with the `paymentMethodToken`                                            | Primer creates it when you approve                         |
| 3DS / required actions | You route `requiredAction.clientToken` back via `handler.continueWithNewClientToken()` | Primer handles 3DS and resume automatically                |
| Resuming               | `onResumeSuccess(resumeToken, handler)` + `POST /payments/{id}/resume`                 | Removed (no resume step)                                   |
| Resolving the UI       | `handler.handleSuccess()` / `handler.handleFailure()`                                  | `continueFlow()` (the SDK resolves the UI itself)          |
| Outcome                | Returned from your `POST /payments` call                                               | `primer:payment-success` / `primer:payment-failure` events |

The result is far less code: there is no payment creation request, no resume request, and no required-action routing on your side.

## Step 1. Enable the flow on the client session, not the SDK

Move the manual switch from an SDK setting to a client session field.

**Legacy** (set on the SDK):

```javascript theme={"dark"}
Primer.showUniversalCheckout(clientToken, {
  paymentHandling: 'MANUAL',
  // ...
});
```

**New** (set on the client session when you create it server-side):

```bash theme={"dark"}
curl -X POST https://api.primer.io/client-session \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Version: 2.4" \
  -H "Content-Type: application/json" \
  -d '{ "orderId": "order-123", "currencyCode": "USD", "amount": 1000, "approvalMode": "MANUAL" }'
```

The SDK is initialized the same way as any Primer Checkout integration (see the [Migration Guide](/docs/checkout/primer-checkout/migration/overview#initialization)); there is no `paymentHandling` option.

## Step 2. Replace the tokenize/resume callbacks with the approval event

The two callbacks collapse into a single event.

**Legacy** (create the payment and route required actions yourself):

```javascript theme={"dark"}
Primer.showUniversalCheckout(clientToken, {
  paymentHandling: 'MANUAL',

  async onTokenizeSuccess(paymentMethodTokenData, handler) {
    const response = await createPayment(paymentMethodTokenData.token); // POST /payments
    if (!response) return handler.handleFailure('Payment failed.');
    if (response.requiredAction?.clientToken) {
      return handler.continueWithNewClientToken(response.requiredAction.clientToken);
    }
    return handler.handleSuccess();
  },

  async onResumeSuccess(resumeTokenData, handler) {
    const response = await resumePayment(resumeTokenData.resumeToken); // POST /payments/{id}/resume
    if (!response) return handler.handleFailure('Payment failed.');
    if (response.requiredAction?.clientToken) {
      return handler.continueWithNewClientToken(response.requiredAction.clientToken);
    }
    return handler.handleSuccess();
  },
});
```

**New** (decide, then resume):

```javascript theme={"dark"}
const checkout = document.querySelector('primer-checkout');

checkout.addEventListener('primer:payment-approval-required', async (event) => {
  const { paymentAttemptId, continueFlow } = event.detail;

  // Your backend calls /approve or /abort (Step 3)
  await fetch('/my-backend/handle-payment-approval', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ paymentAttemptId }),
  });

  await continueFlow();
});
```

Note what disappears: no `POST /payments`, no `requiredAction` inspection, no `continueWithNewClientToken`, and no `onResumeSuccess` / resume call. Primer creates and orchestrates the payment after you approve.

## Step 3. Swap payment creation for approve / abort

Your backend endpoint changes from creating (and resuming) a payment to approving or aborting the attempt.

**Legacy:**

```javascript theme={"dark"}
// Create the payment with the tokenized method
await fetch('https://api.primer.io/payments', {
  method: 'POST',
  headers: { 'X-Api-Key': KEY, 'X-Api-Version': '2.4', 'Content-Type': 'application/json' },
  body: JSON.stringify({ paymentMethodToken }),
});
```

**New:**

```javascript theme={"dark"}
// Approve the attempt; Primer creates and processes the payment
await fetch(`https://api.primer.io/payment-attempts/${paymentAttemptId}/approve`, {
  method: 'POST',
  headers: { 'X-Api-Key': KEY, 'X-Api-Version': '2.4' },
});

// Or abort it; no payment is created
await fetch(`https://api.primer.io/payment-attempts/${paymentAttemptId}/abort`, {
  method: 'POST',
  headers: { 'X-Api-Key': KEY, 'X-Api-Version': '2.4', 'Content-Type': 'application/json' },
  body: JSON.stringify({ reason: 'fraud-check-failed' }),
});
```

The legacy flow had no first-class way to *decline* after tokenization other than failing the payment. The new flow gives you `/abort` as an explicit, payment-free cancellation.

## Step 4. Read the outcome from events

The payment result is no longer the return value of your `POST /payments` call. Listen for the standard payment events instead, the same ones every Primer Checkout integration uses:

```javascript theme={"dark"}
checkout.addEventListener('primer:payment-success', (event) => {
  const { payment } = event.detail;
  window.location.href = `/confirmation?order=${payment.orderId}`;
});

checkout.addEventListener('primer:payment-failure', (event) => {
  const { error } = event.detail;
  console.error('Payment failed:', error.diagnosticsId);
});
```

## Behavior differences to be aware of

* **3DS is automatic.** You no longer route `requiredAction.clientToken` back to the SDK. After you approve, Primer stages and resolves 3DS itself.
* **Retries create a new attempt.** On a decline the checkout unlocks the payment form and the customer can retry in the same session; re-tokenizing creates a new `paymentAttemptId`. You don't create a new client session.
* **There's a 90-second approval window.** If your backend calls neither `/approve` nor `/abort` within 90 seconds, the attempt expires. The legacy flow had no such window.
* **`paymentAttemptId` is the idempotency key.** Duplicate or concurrent approves for the same attempt converge on one payment.

See the [Manual payment approval](/docs/checkout/primer-checkout/guides-and-recipes/manual-payment-approval) guide for the full lifecycle, error matrix, and browser-refresh recovery.

## Migration checklist

* **Moved the switch**: replaced `paymentHandling: 'MANUAL'` (SDK) with `approvalMode: 'MANUAL'` (client session)
* **Replaced callbacks**: swapped `onTokenizeSuccess` / `onResumeSuccess` for the `primer:payment-approval-required` event + `continueFlow()`
* **Replaced backend calls**: swapped `POST /payments` (and `/resume`) for `POST /payment-attempts/{id}/approve` (or `/abort`)
* **Removed required-action routing**: deleted `continueWithNewClientToken` and resume handling (Primer handles 3DS)
* **Read outcomes from events**: using `primer:payment-success` / `primer:payment-failure`
* **Handled abort and the 90s timeout**: see the manual payment approval guide

## Next steps

<CardGroup cols={2}>
  <Card title="Manual payment approval" icon="circle-check" href="/docs/checkout/primer-checkout/guides-and-recipes/manual-payment-approval">
    The full guide for the new flow
  </Card>

  <Card title="Migration Guide" icon="arrow-right-arrow-left" href="/docs/checkout/primer-checkout/migration/overview">
    SDK-wide changes for moving to Primer Checkout
  </Card>

  <Card title="Events Reference" icon="bolt" href="/docs/sdk/primer-checkout-web/events-reference">
    primer:payment-approval-required and all events
  </Card>

  <Card title="Client session" icon="key" href="/docs/checkout/client-session">
    Create a session with approvalMode
  </Card>
</CardGroup>
