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

# Approving a purchase

> Ask for the shopper's yes, send the approval link, and let Agentcard place the order the moment they approve.

The shopper reads the cart and says yes. Your agent sends an approval link, and the shopper approves the payment on their phone with a passkey, which is Face ID or Touch ID on an iPhone. Agentcard places the order the moment they approve, and the chat reads "order placed" with nothing else to type.

Two steps are the shopper's own: the yes to the cart, and the approval. Your code never approves for them, and the model never places an order without the yes.

## Confirm the cart

The model sets `confirm` to true only when the shopper said yes to the cart in the model's last result, asks to check out or pay, says they added the card for a cart that waits, says they approved, or asks for the approval link again. The tool then confirms the cart Agentcard last showed, so you handle no cart hash and no conversation id.

Show the shopper the cart before the model confirms it. "Showing the cart" renders it.

When the confirm needs the shopper to approve, the answer carries these fields:

| Field | Value |
| - | - |
| `status` | `approval_sent` |
| `next_action` | `approve` |
| `links` | One `approval` link, handed to `sendLink` before the call returns. |
| `approval_expires_at` | When the approval link stops working, as an ISO 8601 timestamp. |

The approval link works for about 15 minutes. The model asks the shopper for one thing, the approval, in one short line, such as "approve the payment to place your order (up to \$19.02)". The dollar amount in the parentheses is the cart's `max_charge`, the most the shopper's card can be held for. A store that prices its final total at checkout asks for more than the cart's `total`, and a flight asks for exactly its total.

```ts chat.ts theme={null}
const turn = buyFor(shopper.id).turn(message, {
  sendLink: (link) => {
    if (link.kind === 'approval') return sendStep(shopper.id, 'Approve the payment', link); // your code
    return deliverLink(shopper.id, link);
  },
  onResult: (result) => {
    // result.carts holds each cart's total and the most it can be held for: approvedCeilingCents
    showCart(shopper.id, result.carts, result.next_action); // your code
  },
});
```

Show the cart's `approvedCeilingCents` beside its total whenever the cart carries one. A shopper who sees only the total approves less than the card can be held for.

## Place the order

The shopper taps the link, sees the store and the amount, and approves with their passkey. Agentcard places the order against that approval. Your server hears about it as a `purchase.message`, and the SDK hands you `text` ready to post:

| `kind` | When it arrives |
| - | - |
| `order_placed` | The shopper approved, and Agentcard placed the order. |
| `order_not_placed_yet` | The shopper approved, and the order is not placed yet. |
| `order_unconfirmed` | The store's reply to the order was lost, so the order may exist, and Agentcard is confirming it. |
| `order_confirmed` | The retailer accepted the order on its side, with its own order number and delivery window. |
| `order_failed` | The order failed after the shopper approved. |

An order takes a few seconds to go through after the approval. A call that lands while Agentcard is still placing the order pauses once for 12 seconds and reads the conversation. The model then reads `order_placed` with the order number, or `placing`. A `placing` status means the order is going through, so the model never asks for another approval. A call that finds the order already placed reads it at once.

When `shopper_need_not_reply` is true, a `purchase.message` will tell the shopper how the order went, and the model tells them they need not reply. Post that message into the chat when it arrives. The order events your company receives are documented in [Order events](https://docs.agentcard.com/webhooks/orders/overview.md).

## Handle a refusal where it happens

### The shopper declines or lets the approval expire

| `kind` | When it arrives |
| - | - |
| `approval_declined` | The shopper declined the approval, and no charge went through. |
| `approval_expired` | The approval expired before the shopper answered, and no charge went through. |
| `approval_expiring` | The approval link expires soon. `text` carries the link, so send `text` as it is and the link arrives exactly as written. |
| `payment_declined` | The payment was refused before the order went in, such as a declined card. `facts.reason` says why, and no charge went through. |

After a decline, the cart stays on the table. The shopper can say yes for a new approval link, or tell the agent what to change.

`approval_declined` has `advances` set to true. Open `buy.nextStep(message)` as "Taking the next step once the shopper finishes outside the chat" shows, and the agent offers the next step without waiting for the shopper.

Drop a message after its `facts.validUntil`: the link or window it names has closed. `isStale(message)` checks it, and `listen` and the webhook handler skip stale messages for you.

### The cart changed

A cart changes when its price or contents drift after the shopper saw it. Agentcard refuses the confirm, and the tool answers `needs_input` with `error_code: cart_changed` and the cart as it stands now. The model shows the shopper the new total and asks again. Nothing is charged.

The other refusals that still answer the shopper come back as `needs_input` with the reason in `reply` and the code in `error_code`:

| `error_code` | What it means |
| - | - |
| `nothing_to_confirm` | The shopper confirmed with no cart. The model asks what they want. |
| `conversation_closed` | The shopper confirmed on a closed conversation. The model asks what they want. |
| `cart_not_ready` | The store is still building the cart, so it has no total yet. Nothing was confirmed or charged. |
| `turn_in_progress` | Agentcard is still working on the shopper's last request. The tool asks the model to call again in a moment. |
| A code that starts with `delivery_address` | The delivery address changed since the cart was built. The model asks for the cart again. |

### A confirm that takes too long

A request that takes Agentcard more than 110 seconds comes back before its cart is ready. A confirm that takes that long returns the order it placed, sends the approval link it paused on, or reads `placing` while the order is still going through. A call that times out throws an `AgentcardApiError` with `code: timeout`. Whether the turn ran is then unknown, so read the conversation before you call again:

```ts recover.ts theme={null}
const conversation = await agentcard.conversation(conversationId, { externalUserId: shopper.id });
// conversation.orders lists the orders placed, and conversation.turn_in_progress says whether a turn still runs.
```

Call the tool again only once that read shows the turn did not finish, because a second call sends a second turn.

## Check it with live credentials

The sandbox stops before payment. A confirm there ends `declined`: a paid purchase answers `decline_code: sandbox_mode`, and a table booking answers `decline_code: sandbox_unavailable`. Both mean the sandbox worked, and everything before the confirm ran for real. The approval link, the passkey tap, and the placed order exist only with live credentials.

To check them end to end, switch to live credentials as the Quickstart describes, and buy one inexpensive item with your own card.

1. Ask for the item, pick a product, and say yes to the cart.
2. Check that the approval link arrives alone, with one short line that names the amount, and that the model's reply holds no link.
3. Approve with your passkey. Check that "order placed" arrives in the chat with nothing typed after the tap.
4. Ask for the item again, say yes, and decline the approval. Check that no charge appears and the cart is still there.


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