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

# Enable auto-approval

> Ask a user once for permission to pay with their card, so your app's supported purchases go through without an approval link.

Ask the user once, in the vault session link you send them, to let your app pay with their card. The user saves or picks a card in the Vault and reads what they allow, and the permission starts with that save or pick. From then on, Agentcard pays with that card without sending the user an approval link, whenever a purchase meets the conditions in [Pay without an approval](#pay-without-an-approval). Every other purchase waits for the user's approval, as in [Completing a purchase](/vault/completing-a-purchase).

Ask the user yourself before each purchase. Agentcard pays without asking them and never checks that your app asked.

## Check what your app needs

App auto-approval works with production credentials, for any company's app that has a `client_id` and a `client_secret`. Your app gets its platform access token with client credentials, and the examples on this page send that token as `$ORG_TOKEN`. Every purchase your app pays this way charges a real card.

A public OAuth app has no secret, so it can't ask for app auto-approval. Test mode can't either: the dashboard shows the option as unavailable until you switch it to Live, and the API refuses the link under sandbox credentials. A purchase in test mode always waits for the user's approval.

Your checkout code also has to tell Agentcard which page each purchase comes from. The checkout SDK, `@agent-cards/checkout`, does this for you from version 0.7.0, as [Send the checkout origin](#send-the-checkout-origin) explains.

## Set a default in the dashboard

Set a default once, and every vault session your app creates from then on asks for app auto-approval. Only an owner or admin of your company can change the default. The dashboard's labels call the user the customer.

1. Open the [dashboard](https://app.agentcard.sh) and switch it to Live.
2. Go to Settings → Vault → Customize.
3. Under Card enrollment, pick your app.
4. Choose `Request app auto-approval`.
5. Check "Our app will obtain customer approval before each purchase. Agentcard does not verify that approval."
6. Select `Save enrollment default`.

The default applies to new vault sessions only. Links you already sent, cards users already stored, and permissions users already granted stay as they are.

A link Agentcard sends for you with [Send a user their vault link](/api-reference/vault/vault-link) never asks for app auto-approval. To ask that user, create the vault session yourself and send its `url`.

To stop asking, choose `Customer chooses` and save. New links stop asking, and permissions users already granted stay on.

## Ask on one link

To ask on one link, or to override your app's default, send `approval_mode` when you create the vault session:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/vault_sessions \
  -H "Authorization: Bearer $ORG_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"approval_mode": "partner", "expires_in": 86400}'
```

```json theme={null}
{
  "object": "vault_session",
  "id": "vs_2q9d1x8f3k2m4t7w",
  "user_id": null,
  "url": "https://vault.agentcard.sh/v?vs=vs_2q9d1x8f3k2m4t7w.3k1v…",
  "channel": null,
  "destination": null,
  "expires_at": "2026-10-03T18:00:00Z",
  "poll_interval": 3,
  "test_mode": false
}
```

`approval_mode` takes `partner` to ask for app auto-approval, or `customer` to leave the choice to the user, as on any other link. Without it, the link follows your app's default from the dashboard. Send `url` to the user wherever you already talk to them.

`expires_in` sets how long the link lives, from 60 seconds to 48 hours, and defaults to 24 hours. Add `user_id` to send the link to a user already connected to your app: when they open it, Agentcard sends them a one-time code instead of a new account setup.

Under sandbox credentials, Agentcard refuses the request and creates no link:

```text theme={null}
HTTP 403
{
  "error": {
    "code": "partner_enrollment_disabled",
    "message": "App auto-approval is not available. Set approval_mode to customer to let the customer choose their own settings.",
    "docs": "https://docs.agentcard.sh"
  }
}
```

Create the session with your production token. To send an ordinary link instead, send `approval_mode: "customer"`, or set your app's default back to `Customer chooses`.

## See what the user approves

The link opens the Vault. A new user adds a card and sets up a passkey. A returning user signs in with their passkey and picks one of their cards. On Add your card, the Vault says what the user allows:

```text theme={null}
When you save this card, Acme can pay with it without asking you here. No spending limit or end date. You can turn this off anytime in the Vault.
```

On Select your card, the same note opens with "When you pick a card". The Vault names your company by the display name under Settings → Vault → Customize, or by your company name when you set none.

Before the permission starts, the Vault asks a user who has no master password to set one, and the user can't skip that screen. A master password lets the user recover access to their Vault if they lose their device. An ordinary card link saves a card with a passkey alone.

When the permission starts, the Vault shows "Your card is ready for Acme". The permission has no spending limit and no end date, and it stays on after the link expires. The user's bank can still decline a purchase.

The permission works with any card the Vault saves, including a 12-digit Maestro card and an American Express card with a 4-digit security code. Each link gives your app one card. To let your app pay with a second card, send the user another link.

The user can turn the permission off at any time, from that screen or from the card in their Vault, on any phone or computer that opens their Vault. From then on, your app's purchases with that card wait for the user's approval. [Ask for permission again](#ask-for-permission-again) brings it back.

## Learn when the card is ready

Receive these events at your [webhook endpoint](/webhooks/overview), or poll the session.

| Event | When |
| - | - |
| `vault.session_linked` | The link got its user. Carries the `user_id` to store. |
| `vault.card_stored` | The user saved a new card. No event fires when they pick a card they already stored. |
| `vault.payment_permission.updated` | The permission changed. `status` is `active` once your app can pay with `card_id`. |

A link created with `user_id` never fires `vault.session_linked`, because you already hold the id.

```json vault.payment_permission.updated theme={null}
{
  "type": "vault.payment_permission.updated",
  "data": {
    "user_id": "usr_8f3k2m",
    "card_id": "vc_7f3a9c2e1b4d6f8a0c2e4b6d",
    "client_id": "b1e0c6d2-5a7f-4c3e-9d8b-2f6a1e4c7b90",
    "grant_id": "apg_3585dcb59976ad25751aa7752ddbf257dfb1c3770e3cf3aaed845786ffbfa827",
    "allowance_id": "allowance_0b40a52f809fc78933413e03034129fd0392dbfef461a635016a98987aaf2632",
    "status": "active",
    "revision": 1
  }
}
```

`grant_id` names the permission on that card, and `allowance_id` is the same for every card the user gives your app. The same event reports every later change: `revoked` when the user turns the permission off or your app's connection to the user ends, and `retired` when the user removes the card. A delivery can repeat or arrive late, so keep the highest `revision` you have for each `grant_id` and ignore any event with a lower or equal one. The [event's page](/webhooks/vault/vault-payment_permission-updated) shows every field.

To poll instead, read the session every `poll_interval` seconds until `payment_permission.ready` is `true`:

```bash theme={null}
curl https://api.agentcard.sh/api/v2/vault_sessions/vs_2q9d1x8f3k2m4t7w \
  -H "Authorization: Bearer $ORG_TOKEN"
```

```json theme={null}
{
  "object": "vault_session",
  "id": "vs_2q9d1x8f3k2m4t7w",
  "status": "linked",
  "user_id": "usr_8f3k2m",
  "linked_at": "2026-10-02T18:41:07Z",
  "poll_interval": 3,
  "code_sends": null,
  "verify_attempts": null,
  "channel": null,
  "expires_at": "2026-10-03T18:00:00Z",
  "created_at": "2026-10-02T18:00:00Z",
  "test_mode": false,
  "payment_permission": {
    "card_id": "vc_7f3a9c2e1b4d6f8a0c2e4b6d",
    "grant_id": "apg_3585dcb59976ad25751aa7752ddbf257dfb1c3770e3cf3aaed845786ffbfa827",
    "revision": 1,
    "status": "active",
    "ready": true
  }
}
```

The session's `status` turns `linked` as soon as the user signs in or sets up their passkey, before they grant the permission, so wait for `ready`.

| `payment_permission.status` | Meaning | What to do |
| - | - | - |
| `pending` | The user hasn't granted it yet. | Read again after `poll_interval` seconds. |
| `active` | Your app can pay with `card_id`. `ready` is `true`. | Stop polling. |
| `revoked` | The user turned the permission off, or your app's connection to the user ended. | [Ask for permission again](#ask-for-permission-again). |
| `retired` | The user removed the card. | Send a new link so the user adds a card. |
| `unavailable` | Agentcard couldn't read the permission. | Read again. |

Later, [List a user's vaulted cards](/api-reference/vault/cards-list) returns the same `payment_permission` on each card.

## Get the user's tokens

A purchase needs only your platform access token, so most apps skip this step. When your app also calls endpoints that run with the user's own connection token, exchange the linked session once:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/vault_sessions/vs_2q9d1x8f3k2m4t7w/exchange \
  -H "Authorization: Bearer $ORG_TOKEN"
```

```json theme={null}
{
  "object": "connection",
  "access_token": "eyJhbGciOiJIUzI1NiIs…",
  "refresh_token": "acr_Vd3k9QmZx…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "user": { "id": "usr_8f3k2m" }
}
```

Store both tokens and keep them fresh with [Refresh the connection](/api-reference/connections/refresh). Each session exchanges once, so Agentcard refuses a second exchange:

```text theme={null}
HTTP 410
{
  "error": {
    "code": "already_exchanged",
    "message": "You already exchanged the session. Store the tokens from that call.",
    "docs": "https://docs.agentcard.sh"
  }
}
```

When the user signed in to an Agentcard account that already existed, Agentcard refuses the exchange until you connect the user with a one-time code:

```text theme={null}
HTTP 403
{
  "error": {
    "code": "account_verification_required",
    "message": "The link proved access to the vault, not to the account: the user signed in to an account that already existed. Get connection tokens by connecting the user with POST /api/v2/connect/start, then POST /api/v2/connect/verify.",
    "docs": "https://docs.agentcard.sh"
  }
}
```

Connect that user with [Send a code](/api-reference/connections/start) and [Verify the code](/api-reference/connections/verify) instead. The permission doesn't depend on these tokens: your app's purchases work either way.

## Send the checkout origin

Send `checkout_origin`, the origin of the top-level checkout page such as `https://shop.example.com`, with every checkout authorization. Without it, the purchase waits for the user: Agentcard pays without asking only when it knows which page the purchase comes from.

The checkout SDK sends it for you from version 0.7.0. When the merchant's card request pauses, the SDK reads the origin of the top-level page, never the payment processor's frame. An older version never sends it, so none of its purchases go through without the user. Install the current release:

```bash theme={null}
npm i @agent-cards/checkout playwright-core
```

When your own code pauses the card request instead of the SDK, add `checkout_origin` to each checkout authorization you create:

```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",
    "checkout_origin": "https://shop.example.com",
    "amount": 2306,
    "currency": "usd",
    "card_id": "vc_7f3a9c2e1b4d6f8a0c2e4b6d",
    "psp": "stripe",
    "request": {
      "url": "https://api.stripe.com/v1/payment_methods",
      "method": "POST",
      "headers": { "content-type": "application/x-www-form-urlencoded" },
      "body": "type=card&card[number]=<card number>&..."
    }
  }'
```

Write `checkout_origin` as the exact origin: `https://`, the host in lower case, and nothing after the host except a port other than 443. A path, a trailing slash, an uppercase letter or `:443` makes the value invalid, and Agentcard refuses the checkout authorization:

```text theme={null}
HTTP 400
{
  "error": {
    "code": "invalid_request",
    "message": "checkout_origin must be the exact https origin of the checkout page (https://shop.example.com).",
    "docs": "https://docs.agentcard.sh"
  }
}
```

A checkout authorization created without `checkout_origin` still goes through, but the purchase waits for the user, and the authorization says why:

```json theme={null}
{
  "id": "cauth_2q9d1x8f3k2m4t7w",
  "object": "checkout_authorization",
  "status": "awaiting_approval",
  "execution_mode": "user_approval",
  "execution_reason": "unverified_origin",
  "execution_detail": "No auto-approval rule could take this purchase: the create named no checkout_origin, so Agentcard saw no verified origin for it. Send checkout_origin (the exact https origin of your app or checkout page) on the create and a rule the person confirmed will take purchases under it.",
  "approvalUrl": "https://vault.agentcard.sh/authorize?id=cauth_2q9d1x8f3k2m4t7w"
}
```

## Pay without an approval

Create the checkout authorization with your platform access token, as in [Completing a purchase](/vault/completing-a-purchase), and pass the stored `user_id` in the `user` field. The [checkout SDK](/vault/creating-a-cart) makes this call for you. Agentcard pays without asking the user when every one of these is true:

* The purchase runs under production credentials.
* The checkout authorization carries `checkout_origin`.
* The purchase is in US dollars, and Agentcard knows the amount. Pass `amount` and `currency` with a purchase that turns the card into a token, because that request carries no amount. A one-step Stripe payment qualifies without them, because Agentcard reads the amount from Stripe.
* Exactly one of the user's cards carries an active permission for your app, or the checkout authorization names the card with `card_id`, which the SDK takes as `cardId`.
* The checkout sends the card in a step the table below marks as paid without asking.

| Where the checkout sends the card | Example | Paid without asking |
| - | - | - |
| A card request that turns the card into a token | Stripe's `/v1/payment_methods` or `/v1/tokens`, Shopify's card vault | Yes |
| A one-step Stripe payment confirm, with the card inside | `/v1/payment_intents/{id}/confirm` | Yes, unless the bank asks for 3D Secure |
| A request that saves the card for later charges | `/v1/setup_intents/{id}/confirm`, or a confirm with `setup_future_usage` | No |
| A Stripe Checkout Session confirm, with the card inside | `/v1/payment_pages/{id}/confirm` | No |
| The processor's own hosted form | Tranzila | No |
| A checkout you prepared before Pay | `controller.prepare()` | No |

The other `token` processors in [List recognized processors](/api-reference/vault/recognizers) work like the first row: Agentcard completes their card request in place of the user's device.

When the purchase qualifies, Agentcard pays with the card the user gave your app and asks nobody. The authorization carries `execution_mode: "autopilot"`, the `grant_id` of the permission that paid, an `autopilot_status`, and no `approvalUrl`. Read it until it finishes:

```json theme={null}
{
  "id": "cauth_2q9d1x8f3k2m4t7w",
  "object": "checkout_authorization",
  "status": "approved",
  "execution_mode": "autopilot",
  "grant_id": "apg_3585dcb59976ad25751aa7752ddbf257dfb1c3770e3cf3aaed845786ffbfa827",
  "autopilot_status": "succeeded",
  "merchant": "shop.example.com",
  "amount": 2306,
  "currency": "usd",
  "amount_display": "$23.06",
  "amount_authority": "agent",
  "amount_verified": null,
  "charged_kind": "none",
  "outcome": {
    "reported_by": "partner",
    "reports": 0,
    "last_reported_at": null,
    "status": "unreported",
    "charged_amount": null,
    "refunded_amount": null,
    "net_amount": null,
    "currency": null
  },
  "psp": "stripe",
  "mode": "token",
  "response": { "status": 200, "headers": { "content-type": "application/json" }, "body": "{\"id\":\"pm_…\"}" }
}
```

Replay `response` into the merchant request your browser paused, as after an approval. The SDK does this for you. On `approved`, your webhook endpoint also receives `checkout_authorization.approved` with `execution_mode: "autopilot"` and the same `grant_id`.

A purchase that turned the card into a token finishes with `charged_kind: "none"`. The merchant charges that token from its own server afterwards, so Agentcard never sees the charge, and you [report what the merchant did](#report-what-the-merchant-did). A one-step Stripe payment finishes with `charged_kind: "captured"`, or `"authorized"` when the merchant captures later, and Agentcard reads the payment back from Stripe into `settlement`, as after any approval.

A user can also turn on auto-approval rules of their own in the Vault, with limits they set. Those rules pay only for apps the user never gave a permission. Once the user gives your app one, only your app's permission pays for its purchases.

## Handle a purchase that waits

Any other purchase waits for the user. The authorization stays `awaiting_approval` with `execution_mode: "user_approval"` and an `approvalUrl` to send the user, as without app auto-approval. With the SDK, `onApprovalUrl` fires only for these purchases.

| Why the purchase waits | What to do |
| - | - |
| The checkout authorization carried no `checkout_origin`, and it reads `execution_reason: "unverified_origin"`. | Send `checkout_origin`, or update the SDK to 0.7.0 or later. |
| Agentcard doesn't know the amount, or the purchase isn't in US dollars. | Send `amount` and `currency`. A purchase in another currency always needs the `approvalUrl`. |
| More than one of the user's cards carries a permission for your app, and the checkout authorization names no `card_id`. | Pass `card_id` with the card that should pay. |
| The user has no active permission for your app on the paying card. | Send the user the `approvalUrl`, and [ask for permission again](#ask-for-permission-again). |
| The checkout sends the card in a step the table above marks No. | Send the user the `approvalUrl`. |
| The purchase runs in test mode. | Send the user the `approvalUrl`. |

## Follow an unfinished payment

`autopilot_status` follows Agentcard's attempt to pay. Agentcard sends `checkout_authorization.approved` when the attempt succeeds and `checkout_authorization.declined` when the processor declines the card, so read the authorization to learn every other result. Branch on `execution_mode` as well as `status`: an authorization can read `awaiting_approval` while Agentcard is still paying, and only `user_approval` carries an `approvalUrl`.

| `autopilot_status` | What happened | What to do |
| - | - | - |
| `dispatching` | Agentcard is sending the card. | Keep reading the authorization. |
| `succeeded` | The processor answered, and the authorization is `approved`. | Replay `response`. The SDK does this for you. |
| `declined` | The processor refused the card. The authorization is `declined` with `reason: "processor_declined"`, and Agentcard sends `checkout_authorization.declined`. | Tell the user, and offer to pay with another card. |
| `not_submitted` | Agentcard didn't send the card. `execution_mode` turns `user_approval`, and `approvalUrl` appears. | Send the user the `approvalUrl`. |
| `outcome_unknown` | Agentcard can't tell yet whether the processor received the card, and keeps checking. | Keep reading. Don't start a second purchase for the same order. |
| `action_required` | The user's bank asked for 3D Secure on a one-step Stripe payment. | Tell the user the purchase hasn't gone through, and keep reading. Don't start a second purchase for the same order. |

When the user's bank asks for 3D Secure, the payment stops at `action_required`, and the user never sees the bank's request: Agentcard can't answer it for them, and the Vault doesn't show it. The payment stays open until Stripe reports it finished or canceled, and the authorization stays `awaiting_approval` until then. The current SDK raises `PaymentOutcomeUnknownError` with the reason `autopilot_action_required`.

## Report what the merchant did

When an auto-approved purchase finishes with `charged_kind: "none"`, report what the merchant charged. Send the report with the platform access token of the app that created the purchase:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/checkout/authorizations/cauth_2q9d1x8f3k2m4t7w/outcome \
  -H "Authorization: Bearer $ORG_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"outcome": "charged", "amount": 2306, "currency": "usd", "processor_reference": "ch_3Qxample"}'
```

Agentcard answers `201` with the entry, and every later read of the authorization carries `outcome` with the net amount you reported. Send `declined` instead when the merchant's charge failed, with an optional `reason`.

Report each refund after the charge, one call per refund:

```bash theme={null}
curl -X POST https://api.agentcard.sh/api/v2/checkout/authorizations/cauth_2q9d1x8f3k2m4t7w/outcome \
  -H "Authorization: Bearer $ORG_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"outcome": "refunded", "amount": 1000, "processor_reference": "re_3Qxample"}'
```

Name every refund with the id the merchant's payment processor gave it, in `processor_reference`. When the processor gave you none, send your own id for that refund in `partner_reference`, the same id on every retry. A report sent again answers `200` with `replayed: true` and adds nothing, so a retry is safe.

Agentcard records your report as you sent it and does not verify it. [Report a checkout outcome](/api-reference/vault/authorizations-outcome) lists every rule and error.

## Ask for permission again

A permission that reads `revoked` comes back from a new link. Create a vault session with `approval_mode: "partner"` and send it to the user. When they pick the same card on Select your card, the Vault opens the Restore Acme permission screen, and one tap on the card turns the permission back on. The permission keeps its `grant_id`, its `revision` goes up, and `vault.payment_permission.updated` reports `active` again.

A permission that reads `retired` doesn't come back, because the user removed the card. Send a new link so the user adds a card, and your app gets a new permission for it.

## Give your agent rules

* Get the user's approval before each purchase. Agentcard pays without asking them and never checks that your app asked.
* Send the checkout page's own origin as `checkout_origin`, never the payment processor's frame, such as `https://js.stripe.com`. Agentcard names the merchant from it.
* Pass `amount` and `currency` on every purchase. A request that only turns the card into a token carries no amount, and without one the purchase waits for the user.
* Pass `card_id` once the user has given your app more than one card. Without it, every purchase waits for the user.
* Never start a second purchase for an order whose first purchase reads `outcome_unknown` or `action_required`. The first payment is still open, and Agentcard doesn't know yet whether it will charge the card.
* Report every charge, decline and refund of a purchase that ends with `charged_kind: "none"`, and give each refund its own reference, so a retry never counts twice.


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