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

# Completing a purchase

> The agent submits a placeholder card, the user approves with their passkey, the real card pays, and you confirm the order.

Once the SDK is attached and the cart is built, the purchase is four moments: submit, approve, pay, confirm.

## 1. The agent submits a placeholder card

Have your agent fill the checkout form with placeholder card data and submit it: any of [Stripe's published test cards](https://docs.stripe.com/testing), any future expiry, any CVC. The real card never enters the browser.

When the page sends that card to a recognized payment processor, the SDK intercepts the request and pauses it. `onApprovalUrl` fires with an approval link.

## 2. The user approves with their passkey

Send the approval link to the user. They open it on their own device, see the merchant and the amount you passed, and confirm with fingerprint or face unlock on Android, Face ID or Touch ID on iPhone and Mac, or their master password. Their passkey decrypts the vaulted card on the device.

The user can also decline. Nobody approving within 15 minutes expires the authorization.

## 3. The user's device pays

The device sends the real card to the payment processor directly and reports the processor's response back. The SDK replays that response into the paused request, and your browser continues as if it had sent the real card itself. Your agent never sees the card number, and neither does Agentcard.

Your agent stays in control of the browser. If the merchant asks for a bank challenge or a redirect, surface it to the user.

## 4. Confirm the order

Approval is not a purchase. Read the merchant's order result before you tell the user anything or run post-payment steps.

```ts theme={null}
const checkout = await attachToPlaywright(page, {
  // ...as in Creating a cart
  requireMerchantResult: true,
  resolveMerchantResult: (state) => readMerchantOrder(page, state),
});

await runAgentCheckout(page);

const result = await checkout.reconcile();
switch (result.status) {
  case 'completed':
    await notifyUser(`Order ${result.orderId} confirmed`);
    break;
  case 'pending':
  case 'unknown':
  case 'requires_user_action':
    await keepBrowserForFollowUp(page, result); // don't retry the payment
    break;
  case 'failed':
    await notifyUser('The merchant did not complete the order');
    break;
}
```

`resolveMerchantResult` is yours: read the confirmation page, an order id, or the merchant's API. A missing receipt is not proof of failure, so never retry a payment automatically. Retry only after the merchant confirms it failed, or after the authorization's `settlement` says `not_settled`: for a Stripe PaymentIntent confirm, Agentcard reads the payment back from the processor once the approval lands and records whether it settled, so a page that never showed a receipt still gets a definite answer (`GET /api/v2/checkout/authorizations/{id}`, field `settlement`, and the two webhooks below).

## Webhooks

Your server learns the outcome through the same signed webhooks as every other Agentcard event.

| Event | Fires when |
| - | - |
| `checkout_authorization.approved` | The user approved with their passkey. Carries what the processor said it charged (`charged_amount`, `charged_kind`, `amount_verified`). |
| `checkout_authorization.declined` | The user said no, the amount changed before the card was sent, one of your presets refused it right before the card was sent, or the processor refused the card (`reason`, `psp_error_code`). |
| `checkout_authorization.expired` | Nobody approved within 15 minutes. |
| `checkout_authorization.refused` | One of your presets refused the purchase before anyone was asked. No authorization exists. |
| `checkout_authorization.watched` | The purchase went through and broke a rule one of your presets watches. Nothing is blocked. |
| `checkout_authorization.settled` | Agentcard read the payment back from the processor and it succeeded. Stripe PaymentIntent confirms only; the `settlement` object on `approved` says whether this read happens (`final: false`). |
| `checkout_authorization.settlement_unconfirmed` | The read rested anywhere else: no charge was made (`not_settled`, with why), or the answer stayed unknown for a named reason and Agentcard stopped checking. |

```json checkout_authorization.approved theme={null}
{
  "type": "checkout_authorization.approved",
  "data": {
    "authorization_id": "cauth_2q9d1x8f3k2m4t7w",
    "user_id": "usr_8f3k2m",
    "merchant": "shop.example.com",
    "amount": 2306,
    "currency": "usd",
    "amount_display": "$23.06",
    "psp": "stripe",
    "amount_verified": true,
    "charged_amount": 2306,
    "charged_kind": "captured",
    "settlement": { "status": "unknown", "reason": "check_pending", "final": false, "processor_reference": "pi_3Qxample" }
  }
}
```

None of these confirms a merchant order. Only the merchant does. `settled` confirms that the processor took the money, which is the question left open when the merchant page never answered.

## Supported processors

| | |
| - | - |
| **Global** | Stripe · Shopify · Square · Recurly · Razorpay |
| **Client-side encryption** | Adyen (the card is encrypted on the device with the merchant's Adyen key; Sessions flow only, and Adyen Web's own request timeout applies after Pay, see below) |
| **Hosted form** | Tranzila (the device submits Tranzila's own form) |

Adyen has two limits the other processors do not. Only the Sessions payments request on Adyen's own hosts is paused; a checkout that posts the encrypted card fields to the merchant's own server is not recognized, so nothing pauses there. And because the card is encrypted for the paused request, approval starts when the agent clicks Pay. Adyen Web's own request timeout then applies: observed at 60 seconds on Adyen Web 6.41 and 6.44, it abandons the payment call, and an approval that lands after that has nothing left to complete. Agentcard does not enforce that limit and the authorization still lasts 15 minutes. Prepare the checkout (`controller.prepare({ psp: 'adyen', environment })`, `sandbox` for Adyen's test host, `production` for its live hosts) so the cardholder approves before Pay and only the device-side encryption runs inside that window.

The live list is `GET /v2/checkout/recognizers`. `syncRegistry()` reads it on every run, so new processors reach your agents without an SDK update. Validate each merchant you care about end to end before launch: reaching a recognized processor is not the same as a confirmed order.

## Without the SDK

If you run your own interception, make the authorization call yourself with the processor request your automation captured:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/checkout/authorizations \
  -H "Authorization: Bearer $ORG_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user": "usr_8f3k2m",
    "merchant": "shop.example.com",
    "amount": 2306,
    "currency": "usd",
    "psp": "stripe",
    "request": {
      "url": "https://api.stripe.com/v1/tokens",
      "method": "POST",
      "headers": { "content-type": "application/x-www-form-urlencoded" },
      "body": "card[number]=<card number>&..."
    }
  }'
```

The response carries `approvalUrl` to send the user and an `id` to read back with `GET /v2/checkout/authorizations/:id`. When you fulfill the paused request with the approved response, add the CORS headers the page expects (`access-control-allow-origin` echoing the request's `Origin`). The SDK's `withCorsHeaders` helper does this for you.

## Test it

Test mode follows your credential. The pause, approval, and replay are identical to live. Rehearse against [shop.agentcard.sh](https://shop.agentcard.sh), a demo store on Stripe test mode: store one of [Stripe's published test cards](https://docs.stripe.com/testing) in the vault, attach the SDK, add a product, submit the placeholder card, approve on the device that holds your passkey, and the order completes.


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