Skip to content
Logo

Spend requests

A spend request is Link’s record of a proposed purchase. It identifies the seller, amount, reason for the purchase, selected payment method, and type of credential the agent needs. It also tracks the customer’s approval and the purchase outcome. Start with the CLI quickstart or hosted integration guide to create your first request. Use this page to look up shared behavior while building your integration.

A spend request moves through three main phases:

  1. Create the request with the seller, context, and amount.
  2. Request approval from the Link account owner.
  3. Use the credential returned after approval.

Statuses

Handle all ten spend-request statuses explicitly:

StatusMeaningWhat to do
createdThe request exists, but approval isn’t pending yet.Request approval. If you just requested it, continue polling the same request.
pending_approvalThe customer hasn’t approved or denied the request.Continue polling.
requires_actionThe customer must complete another action.Follow status_details.requires_action.next_action.
approvedThe credential is available.Complete the purchase.
deniedThe customer denied the request.Stop. Create a new request only if the customer asks to retry.
expiredThe 30-minute approval window elapsed, or the approved credential wasn’t used in time.Create a new request if the customer still wants to proceed.
canceledThe request was canceled.Create a new request if the customer still wants to proceed.
submittedThe payment was submitted and is processing.No further action is required. Optionally report the outcome.
succeededThe payment completed.No further action is required.
failedThe payment failed and the request is terminal.Inspect the latest payment's payment_status_details.code and payment_status_details.decline_code.

Poll for approval

Poll for a decision during the 30-minute approval window:

link-cli spend-request retrieve lsrq_001 \
  --interval 2 \
  --max-attempts 300

In an interactive terminal, approval commands poll internally. In non-interactive or structured-output mode, create and request-approval commands return immediately with _next. Run the previous spend-request retrieve command to poll.

Polling can exit successfully at approved, denied, expired, submitted, succeeded, failed, or canceled. It returns a non-auto_resume requires_action so you can involve the customer. An auto_resume action continues polling the same request. Exit code zero means the command completed, not that the customer approved or the payment succeeded. Always inspect status.

If --timeout elapses or --max-attempts is exhausted, Link CLI exits non-zero with code: "POLLING_TIMEOUT". The request is still pending. Retrieve the same request again to learn its current status. Don’t create a duplicate or report a failed purchase based on the timeout alone.

The SDKs don’t poll for you. When you write your own loop, keep polling created and pending_approval, and keep polling requires_action only when the resolution is auto_resume. Return on every other status. Show an auto_resume action to the customer once, when its action_url or expires_at changes, instead of on every poll.

Purchase details

Use these parameters to describe a virtual-card or Shared Payment Token purchase:

ParameterApplies toRequirements
merchant_nameVirtual cardRequired. The seller shown to the customer.
merchant_urlVirtual cardRequired. The seller’s URL.
network_idShared Payment TokenRequired. Use the network ID from the seller’s HTTP 402 challenge. Omit the merchant fields.
contextBothRequired. At least 100 characters explaining what the agent is buying and why.
amountBothRequired. Uses the currency’s smallest unit and can’t exceed 50,000 cents for USD.
currencyBothA three-letter ISO code. If omitted, Link uses the consumer’s default spend-request currency. See Currency.
payment_detailsBothThe ID of an eligible payment method. Omit it to use the customer’s default, or the first eligible method.
line_itemsBothEach item requires name. Optional fields are quantity, unit_amount, description, sku, url, image_url, and product_url.
totalsBothEach total requires type, display_text, and amount. Types are subtotal, tax, total, items_base_amount, items_discount, discount, fulfillment, shipping, fee, gift_wrap, tip, and store_credit.
metadataBothUp to 50 string keys. Keys can contain 40 characters and values 500 characters.
idempotency_keyBothOptional. 1–255 UTF-8 bytes with no personal data. Repeating a create with the same key returns the original response.
recurringCard, Shared Payment Token, and Link Pay TokenOptional, create-only. Sets a repeating spending-limit period with interval (day, week, month, or year) and an optional positive interval_count that defaults to 1. Omit it for a one-time request. See Recurring payments.
completion_callback_url and nonceAll credential typesOptional, create-only, and supplied together. See Completion callbacks.

Link Pay Token requests use a different set of fields. See Link Pay Token.

Currency

If currency is omitted, Link uses the consumer’s default spend-request currency, reported by user-info retrieve as default_spend_request_currency. For an amount taken from a checkout, specify that checkout’s currency. Amounts, line-item unit_amount, and total amount use that currency’s minor units. The client performs no currency conversion. Existing requests retain their stored currency.

Check for blockers before you create a request

Retrieve the customer’s spend limits and verification requirement first, so you can avoid a request that can’t be approved:

link-cli user-info retrieve --format json

agent_wallet_spend_limits has per_transaction, daily, and thirty_day entries, each with limit, used, and remaining in cents. A null value means unlimited. agent_wallet_verification_requirement has a status and a nullable action_url that the customer must visit if verification is required.

Manage a spend request

Retrieve a request to inspect its current state, payment outcome, and any refunds. List requests to recover work across agent runs. You can cancel requests in created, pending_approval, or approved.

The command examples assume you have completed Link CLI setup. Each command demonstrates a separate operation; don’t run them all as one purchase flow. For SDK setup and language-specific examples, use the TypeScript, Go, or Python page.

# Update before approval
link-cli spend-request update lsrq_001 \
  --merchant-url https://press.stripe.com/working-in-public
 
# Request approval separately
link-cli spend-request request-approval lsrq_001
 
# Retrieve at any point
link-cli spend-request retrieve lsrq_001
 
# List the user's spend requests
link-cli spend-request list
 
# Cancel a created, pending, or approved request
link-cli spend-request cancel lsrq_001

See Limits for approval windows and credential validity.

Increase the amount of an approved request

If the final total turns out to be higher than the approved amount, and you haven’t used the card yet, update the request with the new total, not the difference, and ask for approval again:

link-cli spend-request update lsrq_abc123 --amount 7500
link-cli spend-request request-approval lsrq_abc123
link-cli spend-request retrieve lsrq_abc123 --interval 2 --max-attempts 300

Then poll the same request until it returns to approved. After the customer approves, Link increments the authorization. Increases aren’t guaranteed. If one fails, the request stays usable at its original amount. Cancel it and create a new request if you need the higher total. A one-time card can’t be used twice, so don’t request an increase after you’ve used the credential. Recurring terms can’t be changed with update; see Recurring payments.

Test mode

Pass --test to receive test credentials instead of a real payment credential:

link-cli spend-request create \
  --test \
  --merchant-name "Example Store" \
  --merchant-url "https://example.com" \
  --context "Testing the agent purchase flow for Example Store without charging a real payment method in the connected wallet." \
  --amount 100

Test-mode requests don’t charge the underlying payment method. With an SDK, set test: true in TypeScript, Test to a pointer to true in Go, or test=True in Python. Test mode supports virtual cards and Shared Payment Tokens, but not Link Pay Tokens.

Virtual card

The default request returns a virtual card that can be entered into a standard checkout form. It works beyond Link-enabled or Stripe-powered sellers.

After approval, request the full credential only at the point of use:

link-cli spend-request retrieve lsrq_001 \
  --include card \
  --output-file "$(mktemp -d)/link-card.json" \
  --format json

The expanded card object contains these properties:

{
  "id": "card_...",
  "brand": "visa",
  "number": "4242...",
  "cvc": "123",
  "exp_month": 12,
  "exp_year": 2028,
  "billing_address": {
    "name": "Jenny Rosen",
    "line1": "510 Townsend St",
    "city": "San Francisco",
    "state": "CA",
    "postal_code": "94103",
    "country": "US"
  },
  "valid_until": "2026-06-13T03:40:10Z"
}

The cvc, billing_address, and valid_until fields are optional, as are line2, city, state, and postal_code within billing_address. Keep returned card credentials out of model context, logs, and customer-visible messages.

Read a failed payment

When a request has status: "failed", inspect payment_status_details before you retry. It contains outcome, code, decline_code, amount, currency, and refund_details (amount, currency, state, and created) when Link refunded part of the payment.

Shared Payment Token

Use a Shared Payment Token (SPT) for a seller that accepts Stripe payments through Machine Payment Protocol and returns an HTTP 402 challenge.

Decode the raw WWW-Authenticate challenge to extract its network_id:

link-cli mpp decode \
  --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."'

Create the spend request with --credential-type shared_payment_token. The network ID, amount, and currency must all come from the same seller challenge. The values below are illustrative; use the currency from your challenge, not necessarily usd:

link-cli spend-request create \
  --credential-type shared_payment_token \
  --network-id network_xxx \
  --context "Purchasing a contribution from the MPP endpoint the user selected, at the amount in the seller's HTTP 402 challenge." \
  --amount 100 \
  --currency usd

After approval, complete the purchase:

link-cli mpp pay https://climate.stripe.dev/api/contribute \
  --spend-request-id lsrq_001 \
  --method POST \
  --data '{"amount":100}' \
  --header "X-Custom: value"

For a one-time request, an SPT is one-time-use. Create a new spend request if the payment fails. Reusing a consumed token returns a verification-failed error.

The Link SDK creates and retrieves the SPT. Your application’s MPP client is responsible for decoding the seller’s challenge and sending the credential back to the endpoint. See Machine payments for the complete flow.

Some Stripe-hosted checkouts accept a Link Pay Token (LPT) in place of card details. The token also fills in billing and shipping details.

Request it by setting credential_type to link_pay_token; it is a distinct credential type, not a value requested alongside a virtual card. Provide merchant_account_id and omit the merchant name and URL. After approval, retrieve with include set to link_pay_token.

Link Pay Tokens don’t support test mode or Shared Payment Tokens. See Link Pay Tokens to check checkout support, request the credential, and submit it. See Limits for credential validity.

Recurring payments

Add recurring when the customer approves a repeating spending limit, such as a subscription. Omit it for a one-time purchase.

link-cli spend-request create \
  --merchant-name "Example Pro" \
  --merchant-url "https://example.com/subscribe" \
  --context "Monthly access to Example Pro requested by the user, with a spending limit of 20 USD per month including all taxes." \
  --amount 2000 \
  --currency usd \
  --recurring-interval month
InputCLI flagSDK fieldRequirements
Interval--recurring-intervalrecurring.intervalday, week, month, or year.
Interval count--recurring-interval-countrecurring.interval_countOptional positive integer. It requires an interval and defaults to 1 on the server. week with a count of 2 resets the limit every two weeks.

Keep these rules in mind:

  • Amount is a per-period limit. amount is the spending limit for each period in the selected currency’s minor units, not a lifetime subscription budget. Confirm the limit, currency, and cadence with the customer, and state them in context so they appear in approval.
  • Credential types. Card, Shared Payment Token, and Link Pay Token creation accept recurring inputs. Each keeps its normal targeting and checkout requirements, and Link enforces availability and limits. Machine payments started with mpp pay create one-time requests; see Machine payments.
  • The merchant schedules billing. Link CLI doesn’t schedule charges. A recurring card request stays active after individual charges, so don’t create a new request each period or wait for succeeded after each charge. Confirm payments through the merchant’s results or transaction records, and don’t treat the request status alone as proof that a payment succeeded.
  • Terms are create-only. update can’t change the interval or count. Cancel the request and create a new one to change them.
  • Respect credential expiry. Use the expiry returned with the credential, such as valid_until, rather than assuming a fixed lifetime.

Recurring card spending periods

For a recurring virtual card:

  • Spending periods start when the underlying credential is created, normally during approval, and use UTC.
  • Retrieving the existing card doesn’t reset its spending period.
  • Reuse the approved card across periods, within its spending limit and expiry. Don’t create a new request each period.
  • A recurring card still expires. Check the request’s expires_at and the card’s valid_until.

This timing is documented for recurring virtual cards. Don’t assume Shared Payment Tokens or Link Pay Tokens share it, even though they accept the same recurring inputs.

Cancel a recurring payment

Two separate steps stop a subscription:

  1. Cancel the subscription with the merchant so it stops billing.
  2. Run link-cli spend-request cancel to revoke the Link credential.

cancel revokes the credential only. It doesn’t cancel the merchant’s subscription, and the merchant can still try to bill until you cancel there.

Handle required actions

If a request returns status: "requires_action", inspect status_details.requires_action.next_action. It contains the action type, resolution, user-facing display_message, optional action_url, and optional expires_at.

link-cli spend-request retrieve lsrq_001 --format json

Branch on resolution, not on type. New action types can appear over time, but each one maps to one of three resolutions. The table shows how each current type resolves:

Action typeResolutionWhat to do
ssn_verificationcreate_new_spend_request_after_completionSend the customer to action_url, then create a new request after verification.
identity_verificationcreate_new_spend_request_after_completionSend the customer to action_url, then create a new request after verification.
contact_supportcreate_new_spend_request_after_completionDirect the customer to Link support, then create a new request when resolved.
select_payment_methodcreate_new_spend_requestAsk the customer to choose another payment method, then create a new request.
add_payment_methodcreate_new_spend_request_after_completionSend the customer to action_url to add a method, then create a new request.
update_payment_methodcreate_new_spend_requestSend the customer to action_url, then create a new request.
re_authorizecreate_new_spend_requestCreate a new request for the correct amount.
three_d_secureauto_resumeSend the customer to action_url and keep polling the existing request.
three_d_secure_retrycreate_new_spend_requestCreate a new request so the customer can retry the challenge.
partner_auth_challengeauto_resumeShow the returned display_message. The customer completes verification in the Link app or through action_url when it is present. Keep polling the same request.
partner_auth_challenge_retrycreate_new_spend_requestFollow the returned instructions and create a new request to retry verification. Stop polling the unusable old request.

If you supplied a completion callback at creation, use action_url as returned and don’t append callback parameters. A callback arriving doesn’t resolve the action; retrieve the request and inspect its status.

Only auto_resume keeps the existing request alive. Every other resolution requires a new spend request after the customer completes the requested action. The old request expires on its own, so don’t cancel it or keep polling it.

Partner verification

A card network can ask the customer to verify themselves before it issues a credential. Link reports this as partner_auth_challenge (auto_resume) or partner_auth_challenge_retry (create_new_spend_request). Keep branching on resolution, not on a fixed list of action types.

  • action_url can be null. Don’t assume verification has a browser URL; always show display_message, and show the link only when action_url is present.
  • Don’t create a replacement request just because a partner_auth_challenge is pending. Keep polling the same request until its status changes.
  • If verification times out, retrieve the same request to learn its current status. Don’t automatically create another. Create a new request only when the action is partner_auth_challenge_retry or another action with a create_new_spend_request resolution.

In an interactive terminal, spend-request create --request-approval and spend-request request-approval keep polling through an auto_resume action reached after approval. In agent or other non-interactive mode, the command returns instead of polling, so follow the returned continuation for the same request. A successful exit doesn’t prove approval or payment; inspect the returned status.

Handle errors and retries

Plan how your integration handles authentication failures, rate limits, and uncertain request outcomes:

  • Expired access tokens: When a request returns 401, an SDK configured with a token provider calls it once with a forced refresh and retries the request once. An SDK configured with a fixed token doesn’t retry. If a refresh request fails with invalid_grant, the grant is no longer valid. Send the customer through authorization again.
  • Rate limits and server errors: The SDKs don’t retry 429 or 5xx responses. Use bounded exponential backoff with jitter and your own operation tracking.
  • Timeouts: The Python SDK uses a 30-second timeout by default. The Go SDK uses http.DefaultClient unless you pass HTTPClient, which has no timeout, so set one.
  • Link CLI: Errors are JSON objects with code and message fields, and the exit code is 1. A POLLING_TIMEOUT error is marked retryable. Retrieve the same request again instead of creating another.

Recover from unavailable payment details

When selected payment details are unavailable to the authenticated account, Link returns HTTP 404:

{
  "error": {
    "code": "payment_details_not_found",
    "message": "The specified payment details were not found or are not available. Fetch and use current payment details before resubmitting the request."
  }
}

Handle this failure by error.code, not by matching the message. Link intentionally does not reveal whether the ID is missing, inactive, or belongs to another account. The response applies to these operations:

  • POST /spend_requests with selected payment details
  • POST /spend_requests/create_delegated with selected payment details
  • POST /spend_requests/:id when replacing payment details through the regular update operation
  • POST /spend_requests/:id/request_approval when the request’s stored payment details are unavailable

Requesting approval starts the approval process; it is distinct from the customer actually approving the request.

Clear the stale selection, fetch GET /payment-details, obtain appropriate purchase authorization for a currently available eligible method, and then resubmit the intended operation. Deleting and re-adding the same underlying card can produce a new payment-details ID. The unavailable ID continues to fail while it remains unavailable, so do not retry it unchanged or silently substitute a default or different card.

A rejected create makes no new spend request and starts no approval. It is not a failed payment or a requires_action spend request. For an existing request, use the regular update operation to replace the stale selection when the request state permits, and then request approval again. If the state does not permit editing, follow the existing lifecycle rules rather than inventing a transition or unconditionally recreating the request.

Resubmission remains subject to existing purchase authorization and idempotency rules. Do not automatically omit an explicit payment-details ID, choose a new idempotency key, or repurchase something that was already completed or canceled. Handle service errors through your existing error and retry policy instead of clearing the payment-method selection.

Recover from an uncertain create

If a create call fails and you don’t know whether Link received it, don’t create a second request blindly.

  1. Create requests with an idempotency_key so that repeating the call returns the original response.
  2. If Link rejects a create as a duplicate, it responds with HTTP 429 and the code spend_request_rate_limited. The error details include the existing request. Use getDuplicateSpendRequest (TypeScript), GetDuplicateSpendRequest (Go), or get_duplicate_spend_request (Python) to read it, then continue with that request instead of creating a new one.
  3. Otherwise, list requests with includeHistory enabled to find the one you created.

Use the recovery example for TypeScript, Go, or Python.

Report purchase outcomes

Reporting is optional, but it helps improve agent checkout reliability.

link-cli report \
  --domain shop.example.com \
  --outcome success \
  --spend-request-id lsrq_abc123
 
link-cli report \
  --domain shop.example.com \
  --outcome blocked \
  --spend-request-id lsrq_abc123 \
  --tag captcha \
  --step "checkout page" \
  --freeform-context "Turnstile challenge appeared"

Supported outcomes are success, blocked, and abandoned. Add one or more --tag values to explain the result: stripe_checkout, captcha, anti_bot_script, cdn_block, waf_block, dns_block, rate_limited, login_required, 3ds_challenge, page_inaccessible, timeout, site_error, payment_declined, or other.

Use --step (up to 500 characters) to say where the agent was, and --attempt-trace for a numbered, step-by-step record of the attempt, one line per step. Replace personal data with placeholders such as [email] and [address]. The server truncates traces longer than 8,000 characters instead of rejecting them.