# Approve a purchase outside Shopify
Have the shopper approve the store, the purchase, a saved card and a spending limit in Shop, so an agent can pay at a store that doesn't check out through Shopify.
Some purchases can't go through a Shop checkout: the store doesn't run on Shopify, so the agent has to pay on the store's own checkout page. The spend request tools cover that case. The shopper reviews the store, the purchase, one of their saved cards and a spending limit in Shop. Once they approve, Shop gives the agent card details for that purchase, paid for by the saved card.
| Tool | What it does |
| --- | --- |
| `create_spend_request` | Asks the shopper to approve a purchase. It doesn't pay or place an order. |
| `get_spend_request` | Reads status and terms; explicit `include: "card"` requests the card after approval |
| `update_spend_request` | Changes the limit, items, totals or shipping address |
| `cancel_spend_request` | Cancels the spend request |
| `complete_spend_request` | Records how the purchase went |
All five need [sign-in](sign-in.md). They also depend on permissions Shop grants to the client the shopper signed in with, so they can be refused even when `get_account` works.
## When to use it
Use a Shop checkout whenever the store supports one. Products from Shop search come with a `seller.domain`, so they always go through the [checkout tools](checkout.md).
For a product page found somewhere else, check whether the store runs on Shopify before you create a spend request. Either of these on the product page means it does:
- A `powered-by` HTTP header that names Shopify.
- A `rel="buy"` link whose `payment` tokens include `dev.shopify.shop_pay`, in an HTTP `Link` header or an HTML `` or `` element.
If the product page has neither, check the store's cart or checkout page as well. When the store does run on Shopify, create a Shop checkout with it.
A spend request isn't a fallback. Don't create one because a Shop checkout failed or needs the shopper to finish on the merchant's page.
## How it works
1. Go through the store's checkout until you can see the real payee and terms: the items, prices, shipping, tax and total.
2. Choose the card. Call `get_account` and send the shopper's chosen card's `id` from `selectable_instruments` as `instrument_id`. To select the displayed preferred card, pass the ID named by `preferred_instrument_id` explicitly. If you omit the choice, Shop selects the saved card shown on the review page; don't infer that it matches the account's preference. If no cards come back, follow the result's `messages` and stop.
3. Call `create_spend_request` once, with the terms you saw.
4. Give the shopper the returned `continue_url`, where they review and approve in Shop.
5. Read status with `get_spend_request`. After the shopper approves, explicitly request the card with `include: "card"`.
6. Pay on the approved store's checkout with the returned root `card`.
7. Report what happened with `complete_spend_request`.
## Create a spend request
```json
{
"spend_request": {
"business": { "name": "Example Merchant", "url": "https://merchant.example" },
"currency": "USD",
"line_items": [{ "item": { "title": "Ceramic pour-over set", "price": 10500 }, "quantity": 1 }],
"totals": [
{ "type": "subtotal", "amount": 10500 },
{ "type": "fulfillment", "amount": 800 },
{ "type": "tax", "amount": 700 },
{ "type": "total", "amount": 12000 }
],
"instrument_id": "3f1e2d4c-5b6a-4789-8abc-def012345678",
"approval_limit": 12000,
"context": {
"intent": "A ceramic pour-over coffee set as a birthday present for my sister, from the small roaster she likes, shipped to me so I can wrap it first."
}
}
}
```
| Field | Value |
| --- | --- |
| `business` | The payee's `name` and `url`, as the store presents them. Required. Add `country` (a two-letter code) only when the store states it; don't work it out from the domain, currency or shipping address. |
| `currency` | The purchase currency (ISO 4217). Required. |
| `totals` | The totals in minor units, including exactly one `total`. Discounts are negative. Required. |
| `instrument_id` | The `id` of the shopper's chosen saved card from `get_account`'s `selectable_instruments`. Pass it explicitly to select that card, including the one named by `preferred_instrument_id`; otherwise Shop chooses the funding card for review. |
| `approval_limit` | The most the card can be charged, in minor units. Required. |
| `line_items` | Each item's `title`, unit `price` and `quantity` |
| `shipping_address` | Where the order ships, in the same fields as a checkout address |
| `context.intent` | What the shopper asked for, in their words: 100 to 500 characters with no links, card details, email addresses or postal addresses. The shopper reads it when they approve, and it can't be changed later. Required. |
Send the items and the shipping address whenever you know them, and never make up a term you don't have.
From the CLI, put the arguments in a file:
```sh
shop spend-request create --arguments @spend-request.json
```
Without an idempotency key, each `create_spend_request` is a new request. If a create fails before returning an `id`, you can't tell whether the spend request exists, so tell the shopper and don't create another. To make a create safe to repeat, send a UUID as `meta.idempotency-key` and repeat it with the same key.
## Shopper review
The result is the spend request, with `status: "pending"`:
| Field | What it holds |
| --- | --- |
| `id` | The spend request's ID, a UUID. The other spend request tools take it as `id`. |
| `status` | Where the spend request stands |
| `continue_url` | The page in Shop where the shopper reviews and approves |
| `expires_at` | The deadline for the next step |
| `messages` | Warnings, such as `review_link_unavailable` when Shop returned no review link |
Give the shopper `continue_url` unchanged. If it's missing, tell them the spend request is waiting in Shop, and read it again later for a new link. Never put together a review link yourself.
`expires_at` isn't the card's expiry date. While the spend request is `pending`, it's when the shopper has to approve by. Once it's `approved`, it's when the agent has to collect the card by.
## Read status
```json
{ "id": "0198e63b-1234-7abc-8def-123456789abc" }
```
Without `include`, `get_spend_request` is a passive status and terms read in every state. It never issues or replays a card.
The CLI can wait for approval. This checks every 5 seconds for up to 90 seconds, and stops at the first result that isn't `pending`. `--wait` alone doesn't request credentials:
```sh
shop spend-request get 0198e63b-1234-7abc-8def-123456789abc --wait 90
```
| `status` | Meaning |
| --- | --- |
| `pending` | The shopper hasn't approved yet |
| `approved` | The shopper approved, and the card hasn't been issued |
| `consumed` | Shop issued the card; this alone proves neither card delivery nor merchant payment |
| `cancelled` | The spend request was canceled |
| `expired` | The deadline passed |
## Get the card
After approval, select the card explicitly with the scalar string `include: "card"`, not an array:
```json
{ "id": "0198e63b-1234-7abc-8def-123456789abc", "include": "card" }
```
This asks Shop to issue the card, or replay the same card if already issued. The result carries it in root `card`, not `instrument`. The tool remains potentially mutating because of this opt-in. Never run concurrent issuance calls or automatically retry issuance after an error. A `materialization_in_progress` error means Shop is preparing the card; follow the returned guidance rather than creating a new spend request.
For a private CLI handoff:
```sh
shop spend-request get 0198e63b-1234-7abc-8def-123456789abc --include card --wait 90 --output-file payment.json
```
`--output-file` requires `--include card`, or scalar `include: "card"` inside complete `--arguments`. When a card is returned, it writes a new `{spend_request_id,card}` file and removes the card from stdout, replacing it with `credential_output_file`. It refuses existing files. POSIX files use mode 0600; Windows uses private directory ACLs. Keep the file private, don't print it, and delete it when the handoff is finished. The browser host can't export a file. Direct MCP consumers don't need a handoff file.
### Use the card
`card` has the issued card's `credential` (the number and other details a checkout form asks for), a `billing_address`, and a `display` object:
- `display` describes the issued card, including its `last_digits`.
- `display.funding_instrument` describes the shopper's saved card, which pays for the issued one.
Use the card only on the checkout the shopper approved, for that purchase. Never repeat its number, expiry date, security code or cardholder name in chat, logs or error messages.
When you tell the shopper which card paid, name the saved card: its `description` exactly as given, or else its brand and last digits. If the store needs to find the payment, give the issued card's last digits.
## Report the outcome
After you try to pay, record the result with `complete_spend_request`. Report `success` only when the store confirms the transaction, such as with an order confirmation, and include the order details you have:
```json
{
"id": "0198e63b-1234-7abc-8def-123456789abc",
"result": { "status": "success", "order": { "id": "1042", "total": 12000, "currency": "USD" } }
}
```
Otherwise, report `error` with a `failure_code` and a short `message`:
```json
{
"id": "0198e63b-1234-7abc-8def-123456789abc",
"result": { "status": "error", "failure_code": "payment_declined", "message": "The payment was declined by the processor." }
}
```
Example codes are `captcha`, `anti_bot_script`, `cdn_block`, `waf_block`, `dns_block`, `rate_limited`, `login_required`, `3ds_challenge`, `page_inaccessible`, `timeout`, `site_error`, `payment_declined` and `other`. Other codes are allowed; with `other`, say what happened in `message`. If you can't tell whether the card was charged, for example after a timeout, report `error` and say in `message` that the charge is unknown.
`complete_spend_request` only records your report. It doesn't pay, prove a payment, close the spend request or cancel the card. Its result's `result.status` and `result.recorded_at` say Shop received the report. `get_spend_request` doesn't show the report afterwards.
## Change or cancel a spend request
`update_spend_request` takes the spend request's `id` and a `spend_request` object with any of `approval_limit`, `line_items`, `shipping_address` and `totals`. The payee, currency, card and intent stay as they were. Shop refuses the update with `approval_terms_immutable` once the spend request's state doesn't allow changes.
```sh
shop spend-request update --arguments '{"id":"0198e63b-1234-7abc-8def-123456789abc","spend_request":{"approval_limit":13000}}'
```
`cancel_spend_request` takes the `id`. It cancels the spend request only: it doesn't cancel or refund a purchase made with it, or revoke a card that was already issued.
Both accept a UUID in `meta.idempotency-key`, and make one from the request when you leave it out. After an uncertain update or cancel, you can repeat the exact same request, with the same key if you sent one.
## When spend requests are turned off
Shop can turn spend requests off. While they're off, every spend request tool returns `isError: true` with this text and no error code, and nothing runs:
```text
This operation is currently unavailable and was not executed. Do not retry automatically.
```
Tell the shopper, and don't retry automatically.