Skip to main content
Early access. Manual payment approval is in early access and rolling out with selected partners. Contact your Primer account team to enable it. Available on the Web SDK today; iOS and Android are coming.
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.
This is a focused companion to the main Migration Guide. Start there for the SDK-wide changes (package, initialization, events, styling); this page covers only what’s specific to the manual flow.

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.
ConcernLegacy manual flowNew manual approval flow
Enabling the flowpaymentHandling: 'MANUAL' (SDK setting)approvalMode: 'MANUAL' (client session field)
TriggeronTokenizeSuccess(token, handler) callbackprimer:payment-approval-required event
Your backend callPOST /payments (then POST /payments/{id}/resume)POST /payment-attempts/{id}/approve (or /abort)
Creating the paymentYou create it with the paymentMethodTokenPrimer creates it when you approve
3DS / required actionsYou route requiredAction.clientToken back via handler.continueWithNewClientToken()Primer handles 3DS and resume automatically
ResumingonResumeSuccess(resumeToken, handler) + POST /payments/{id}/resumeRemoved (no resume step)
Resolving the UIhandler.handleSuccess() / handler.handleFailure()continueFlow() (the SDK resolves the UI itself)
OutcomeReturned from your POST /payments callprimer: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):
Primer.showUniversalCheckout(clientToken, {
  paymentHandling: 'MANUAL',
  // ...
});
New (set on the client session when you create it server-side):
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); 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):
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):
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:
// 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:
// 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:
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 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

Manual payment approval

The full guide for the new flow

Migration Guide

SDK-wide changes for moving to Primer Checkout

Events Reference

primer:payment-approval-required and all events

Client session

Create a session with approvalMode