> ## 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.

# During the payment

> [!NOTE]
>
> This feature is not available on all Primer accounts. Contact your Customer Success Manager to receive access. See [Get started](https://primer.io/docs/vault/get-started.md) for one-time setup.

This page covers everything between creating the client session and showing the customer a result: capturing the card, deciding where the payment goes, calling your PSP through the proxy, handling 3D Secure, and reporting the outcome. For the simplest end-to-end path, see [Your first payment](https://primer.io/docs/vault/get-started.md#your-first-payment).

## Create the client session

Create a [client session](https://primer.io/docs/checkout/client-session.md) with the vault's approval mode and a `customerId`:

```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,
    "customerId": "customer-456",
    "mode": "MANUAL"
  }'
```

| Field | Required | Description |
| - | - | - |
| `mode` | Yes | `MANUAL` pauses the flow after the card is tokenized, so your backend can either approve the payment for Primer to process, or take the token to your own PSP. Set at creation only; immutable afterwards. |
| `customerId` | **Always**, even for a one-off payment | The token Primer issues is only reusable later if it's attached to a `customerId`. Without it, saving the card for a returning customer or moving to orchestration later means re-tokenizing instead of reusing what you already have. |
| `orderId` | Recommended | Your own reference, so you can match a Primer request ID back to your order when something goes wrong. |
| `amount`, `currencyCode` | Recommended | Not needed by the proxy itself, but the checkout uses them to show the total, and Primer needs them if you approve the attempt for Primer to process. |

### The decision window

Once the customer submits their card, your backend has 15 minutes to either approve the attempt or report an outcome from your own PSP, covering your own checks, the PSP request, and any 3D Secure challenge at your PSP. See [Payment attempt states and decision window](https://primer.io/docs/vault/reference.md#payment-attempt-states-and-decision-window) for what happens if you miss it.

## Show the checkout and capture the card

Render the checkout with the client token from the session:

```html theme={"dark"}
<primer-checkout client-token="your-client-token"></primer-checkout>
```

The customer sees your usual Primer Checkout card form; there's nothing vault-specific in the UI itself. When they submit the form, the SDK tokenizes the card and waits; no payment is created yet.

Register a listener for `primer:payment-approval-required`:

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

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

  await fetch('/my-backend/handle-payment', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ paymentAttemptId, token: paymentMethodToken.token }),
  });

  await continueFlow();
});
```

`paymentMethodToken` carries the token your backend uses next, plus the data it needs to decide where the payment goes. See [Decide where the payment goes](#decide-where-the-payment-goes) below.

> [!WARNING]
>
> If the customer refreshes the page while an attempt is still awaiting your decision, the SDK re-emits `primer:payment-approval-required` for the same `paymentAttemptId`. Deduplicate on that ID: if you've already sent this attempt to your PSP or to Primer, don't do it again, just call `continueFlow()`.

## Decide where the payment goes

Your backend receives, from the approval event, the payment method token and the card's BIN data: card network, first digits, last four digits, and issuer. If you only use the vault and have no Primer processing connected, the decision is made for you. Take the token and go to the proxy.

If you've also connected a processor in Primer, you choose per payment, for example by card network, issuer country, or any rule you already run:

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

  await fetch('/my-backend/handle-payment-approval', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      paymentAttemptId,
      network: paymentMethodToken.paymentInstrumentData?.network,
      firstDigits: paymentMethodToken.paymentInstrumentData?.firstDigits,
    }),
  });

  await continueFlow();
});
```

A common rule is to route by BIN, for example sending issuer countries you already process well at your own PSP through the proxy, and routing everything else to Primer.

**Route to Primer:** call `approve` on the attempt. Primer creates the payment and manages the rest of the lifecycle, including 3DS and retries.

```bash theme={"dark"}
curl -X POST https://api.primer.io/payment-attempts/{paymentAttemptId}/approve \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Version: 2.4"
```

`approve` needs the `transactions:authorize` scope. If the decision window has expired, `approve` returns `410 PaymentAttemptExpired` and the attempt can no longer be acted on; the customer has to submit a new payment.

**Route to your own PSP:** take `paymentMethodToken.token` and send your usual PSP request through the [proxy](#send-the-request-through-the-proxy), then [report the outcome](#report-the-outcome).

**Stop the payment:** call `abort` if you decide not to proceed at all, for example because your own fraud check failed before you've even tried a PSP.

```bash theme={"dark"}
curl -X POST https://api.primer.io/payment-attempts/{paymentAttemptId}/abort \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Version: 2.4" \
  -H "Content-Type: application/json" \
  -d '{ "reason": "fraud-check-failed" }'
```

No payment is created and the token is invalidated. Use `abort` for a decision made before you've sent anything to a PSP; if your own PSP processing fails after you've already tried it, that's a [fallback to Primer](#fall-back-to-primer) or a reported failure, not an abort.

> [!NOTE]
>
> Only card payments can route to the proxy. Alternative payment methods always go through Primer orchestration, so mixing card and non-card payment methods in the same checkout needs no extra integration work.

## Send the request through the proxy

Send the same request you'd normally send to your PSP, but to the proxy, with the card fields replaced by placeholders referencing the token:

| Header | Purpose |
| - | - |
| `X-Primer-Api-Key` | Your Primer API key. Set once wherever your PSP client is configured. |
| `X-Target-Host` | The PSP host to forward the request to, for example `api.stripe.com`. Must be on your account's destination allowlist. |
| `Authorization` (or your PSP's usual auth header) | Your own PSP credential, unchanged. |

Replace card fields in the request body with `{{<token>.<FIELD>}}` placeholders:

| Field | Value |
| - | - |
| `NUMBER` | The card number |
| `EXPIRY_MONTH` | Unpadded month, e.g. `6` |
| `EXPIRY_MONTH_PADDED` | Zero-padded month, e.g. `06` |
| `EXPIRY_YEAR` | Four-digit year |
| `EXPIRY_YEAR_SHORT` | Two-digit year |
| `CVV` | The security code. Only available on a single-use token from the current checkout session, not on a saved token. |
| `CARDHOLDER_NAME` | As captured, if captured |

Placeholders work regardless of how your PSP structures its request body. They're only resolved in the request body; a placeholder in a header or in the URL path is never substituted and the request fails. A placeholder that's malformed, references an unknown field, or references a field not available on that token (such as `CVV` on a saved token) fails the whole request rather than being silently dropped.

**Stripe example** (form-encoded):

```bash theme={"dark"}
curl -X POST https://proxy.primer.io/v1/payment_intents \
  -H "X-Primer-Api-Key: YOUR_API_KEY" \
  -H "X-Target-Host: api.stripe.com" \
  -H "Authorization: Bearer YOUR_PSP_SECRET_KEY" \
  -d "amount=1000&currency=usd&card[number]={{tok_abc.NUMBER}}&card[exp_month]={{tok_abc.EXPIRY_MONTH}}&card[exp_year]={{tok_abc.EXPIRY_YEAR}}&card[cvc]={{tok_abc.CVV}}"
```

**Adyen example** (JSON):

```bash theme={"dark"}
curl -X POST https://proxy.primer.io/payments \
  -H "X-Primer-Api-Key: YOUR_API_KEY" \
  -H "X-Target-Host: YOUR_ADYEN_API_HOST" \
  -H "X-API-Key: YOUR_ADYEN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": { "value": 1000, "currency": "USD" },
    "paymentMethod": {
      "type": "scheme",
      "number": "{{tok_abc.NUMBER}}",
      "expiryMonth": "{{tok_abc.EXPIRY_MONTH_PADDED}}",
      "expiryYear": "{{tok_abc.EXPIRY_YEAR}}",
      "cvc": "{{tok_abc.CVV}}"
    },
    "reference": "order-123",
    "merchantAccount": "YOUR_MERCHANT_ACCOUNT"
  }'
```

Most PSP SDKs let you override the base URL and add custom headers, so in many cases you only need to change the base URL your existing PSP client points to and add the two Primer headers; the rest of your integration, including how you build the request body, stays the same. Some SDKs can't easily add custom headers on every call (notably on OAuth/token calls some SDKs make internally); in that case, call the PSP's HTTP API directly for the proxied request instead of going through the SDK. See [Proxy reference](https://primer.io/docs/vault/reference.md#proxy-reference) for the full header and error catalogue.

The proxy relays your PSP's response untouched, including its status code. If Primer can't process the request (for example, an unknown or expired token), the response carries an `X-Primer-Proxy-Error` header and a problem-details body instead of a PSP response. See [Errors and retries](https://primer.io/docs/vault/reference.md#errors-and-retries).

## Handle 3DS at your PSP

3D Secure, when it's needed, runs at your own PSP. Primer doesn't run 3DS on a proxied payment.

* **In-page challenge:** your PSP's response tells you a challenge is required; render it in your page using your PSP's SDK, exactly as you do today. The customer completes it without leaving your checkout.
* **Redirect challenge:** the customer is sent to the issuer's page and back. Your return URL should land them back on your checkout with the same client session.

While the customer is at your PSP or completing a challenge, the Primer checkout has no visibility into what's happening there, so it shows a loading state while it waits for you to report the outcome. If the customer reloads the page mid-challenge, reload the checkout with the same client token; the SDK reads the current state of the attempt and shows the loading state again, or the result, once you've resolved it.

This all has to happen inside the 15-minute decision window. See [Payment attempt states and decision window](https://primer.io/docs/vault/reference.md#payment-attempt-states-and-decision-window) for what happens if a challenge runs long.

## Report the outcome

Once you have a result from your PSP, resolve the attempt so the checkout can show it:

```bash theme={"dark"}
curl -X POST https://api.primer.io/payment-attempts/{paymentAttemptId}/resolve \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Version: 2.4" \
  -H "Content-Type: application/json" \
  -d '{
    "outcome": "SUCCEEDED",
    "vaultPaymentMethod": true
  }'
```

| Field | Type | Required | Description |
| - | - | - | - |
| `outcome` | `SUCCEEDED` or `FAILED` | Yes | The result from your PSP. |
| `vaultPaymentMethod` | boolean | No, default `false` | Save the card before the token is consumed. Only valid with `outcome: SUCCEEDED`. Saved against the `customerId` already on the client session, so you don't repeat it here. See [Save and reuse cards](https://primer.io/docs/vault/after-the-payment.md#save-and-reuse-cards). |

`resolve` needs the `payment_instrument:write` scope. It's idempotent on `paymentAttemptId`: a retried call with the same body returns the original response instead of failing, so you can safely retry after a timeout without risking a duplicate. See [Errors and retries](https://primer.io/docs/vault/reference.md#errors-and-retries) for the full error catalogue, including what happens if you resolve after the window has expired.

Once the outcome is reported, call `continueFlow()` from your frontend. The checkout listens for the result and shows a success or failure screen:

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

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

`primer:payment-success` and `onPaymentSuccess` always carry a Primer `payment` object and don't fire for a proxied payment. Listen for `primer:payment-success-external` instead if your integration handles success itself.

## Fall back to Primer

If you send a card through the proxy and your own PSP declines or fails, the token is still usable, since the attempt is still open. You can retry with a different PSP of your own, or fall back by calling `approve` on the same attempt, so Primer processes the same card instead of you reporting `FAILED`:

```bash theme={"dark"}
curl -X POST https://api.primer.io/payment-attempts/{paymentAttemptId}/approve \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "X-Api-Version: 2.4"
```

Primer doesn't see your PSP's response, so it's your responsibility to only approve once you've confirmed your own PSP hasn't already charged the card.

There's no path in the other direction. Once you approve an attempt for Primer to process, you can't switch that same attempt to the proxy. If you need your own PSP after an approved payment fails, that's a new payment attempt, created when the customer retries.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.