> ## 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 Face ID, 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. Use `4242 4242 4242 4242`, 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 Face ID

Send the approval link to the user. They open it on their phone, see the merchant and the amount you passed, and confirm with Face ID. 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.

## 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 Face ID. Carries what the processor said it charged (`charged_amount_cents`, `charged_kind`, `amount_verified`). |
| `checkout_authorization.declined` | The user said no, the amount changed before the card was sent, or the processor refused the card (`reason`, `psp_error_code`).          |
| `checkout_authorization.expired`  | Nobody approved within 15 minutes.                                                                                                      |

```json checkout_authorization.approved theme={null}
{
  "type": "checkout_authorization.approved",
  "data": {
    "authorization_id": "cauth_2q9d1x8f3k2m4t7w",
    "user_id": "usr_8f3k2m",
    "merchant": "shop.example.com",
    "amount": "$23.06",
    "amount_cents": 2306,
    "currency": "usd",
    "psp": "stripe",
    "amount_verified": true,
    "charged_amount_cents": 2306,
    "charged_kind": "captured"
  }
}
```

None of these confirms a merchant order. Only the merchant does.

## Amount protection

When you pass `amountCents` and `currency` on a checkout, Agentcard checks the intent against that amount three times: when the authorization is created, right before the device sends the card, and after the charge. A mismatch before the card is sent declines the authorization and nothing is charged. A mismatch after is recorded and reported on the `approved` webhook as `amount_verified: false`.

On other processors the amount is shown to the user and reported, not enforced. What you show the user is the guarantee.

## 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) |
| **Hosted form**            | Tranzila (the device submits Tranzila's own form)                         |

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_cents": 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]=4242424242424242&..."
    }
  }'
```

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 `4242 4242 4242 4242` in the vault, attach the SDK, add a product, submit the placeholder card, approve on your phone, and the order completes.
