Skip to content
Spend requests

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

  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:

Shell
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:

Shell
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:

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

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