Guides
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. 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.
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-byHTTP header that names Shopify. - A
rel="buy"link whosepaymenttokens includedev.shopify.shop_pay, in an HTTPLinkheader or an HTML<link>or<a>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
- Go through the store’s checkout until you can see the real payee and terms: the items, prices, shipping, tax and total.
- Choose the card. Call
get_accountand send the shopper’s chosen card’sidfromselectable_instrumentsasinstrument_id. To select the displayed preferred card, pass the ID named bypreferred_instrument_idexplicitly. 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’smessagesand stop. - Call
create_spend_requestonce, with the terms you saw. - Give the shopper the returned
continue_url, where they review and approve in Shop. - Read status with
get_spend_request. After the shopper approves, explicitly request the card withinclude: "card". - Pay on the approved store’s checkout with the returned root
card. - Report what happened with
complete_spend_request.
Create a spend request
{
"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:
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
{ "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:
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:
{ "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:
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:
displaydescribes the issued card, including itslast_digits.display.funding_instrumentdescribes 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:
{
"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:
{
"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.
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:
This operation is currently unavailable and was not executed. Do not retry automatically.
Tell the shopper, and don’t retry automatically.
