Skip to content
Logo

Add payments to your agent

Use a Link SDK to let your agent request payment credentials from a customer’s Link wallet. Your application connects the customer’s Link account, creates a spend request describing the proposed purchase, asks the customer to approve it, and then uses the scoped credential to complete checkout.

This hosted integration is available for US and Canadian customers. Your business, agent, and the sellers your customers buy from can be outside these countries.

Link’s API lives at api.link.com and authorizes against login.link.com, separate from api.stripe.com. You authenticate with an OAuth access token that represents one customer. Stripe secret keys and restricted API keys don’t work, and the Stripe server-side SDKs don’t cover these endpoints.

Before you begin

This guide covers a hosted application. For an agent running locally, use the Link CLI quickstart.

Hosted authorization requirements

Prepare the following before connecting a customer’s Link account:

  • A supported runtime: Node.js 20 or newer for TypeScript, Go 1.23 or newer, or Python 3.11 or newer. TypeScript requires an ESM project.
  • A registered OAuth client, including a client_id, client_secret, and exact redirect URIs. Follow OAuth client registration if you don’t have these yet.
  • The Stripe publishable key from the API keys in your Dashboard.
  • A server-side callback at a registered redirect URI, a session store for authorization state, and secure storage for access and refresh tokens.

For the payment examples in this guide, you also need a customer with a US or Canadian Link account and an eligible payment method.

The code blocks are integration excerpts. Your application supplies the credential storage, customer messaging, saved run state, and checkout execution described in each step.

Install the SDK

Install the SDK for your language.

npm install @stripe/link-sdk

The TypeScript package exports Link as both a default and a named export. Each SDK provides an authenticated client for the steps that follow.

Register a hosted OAuth client

Skip this step if you already have a registered client. A hosted agent uses the OAuth 2.0 authorization-code flow with a confidential client. Create a Stripe account, then apply for an OAuth client through the registration form. Provide your app name, a description, and the exact redirect URIs you need. Matching is exact, so register every URI. Stripe sends you a client_id and client_secret.

Client registration is part of this hosted integration. Customers using Link CLI’s local authorization flow don’t need to register a confidential OAuth client.

Generate a Proof Key for Code Exchange (PKCE) verifier and challenge, store the verifier and a random state value in the customer’s server-side session, then redirect them to:

https://login.link.com/auth?key=pk_live_YOUR_PUBLISHABLE_KEY&client_id=YOUR_CLIENT_ID&redirect_uri=https://yourapp.com/callback&response_type=code&scope=payment_methods.agentic%20userinfo:read&state=RANDOM_STATE_VALUE&code_challenge=YOUR_PKCE_CODE_CHALLENGE&code_challenge_method=S256

URL-encode every parameter value. The parameters are:

ParameterValue
keyYour Stripe publishable key.
client_idThe OAuth client ID from Stripe.
redirect_uriAn exact match for a URI registered with your OAuth client.
response_typecode
scopeSpace-separated (%20) or comma-separated scopes. See the following table.
stateA random value that you verify on callback to prevent cross-site request forgery (CSRF).
code_challengeOptional for confidential clients, but recommended. BASE64URL(SHA256(code_verifier)), using a random, URL-safe verifier 43–128 characters long.
code_challenge_methodS256. Required if you send code_challenge; any other value is rejected.

Request the scopes your feature needs:

ScopeUse it to
payment_methods.agenticCreate spend requests and retrieve credentials. Required for every payment integration.
userinfo:readRead the customer’s name and email to prefill forms and check eligibility.
financial_insights.sources.read, financial_insights.balances.read, financial_insights.transactions.readRead financial data. These pair with the authorization_details parameter described in Financial insights.

After the customer authenticates and approves access, Link redirects to your callback with code and state. Verify state before doing anything else. The authorization code is single-use and must be exchanged within 10 minutes. If the customer declines, or isn’t eligible for a scope, Link redirects with an error instead of a code.

Exchange the code for tokens

Exchange the code from your server. Send your publishable key in the Authorization header, and include the original PKCE verifier:

curl -X POST https://login.link.com/auth/token \
  -H "Authorization: Bearer pk_live_YOUR_PUBLISHABLE_KEY" \
  -d "grant_type=authorization_code" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "redirect_uri=https://yourapp.com/callback" \
  -d "code=AUTHORIZATION_CODE" \
  -d "code_verifier=YOUR_PKCE_CODE_VERIFIER"

Link returns both tokens:

{
  "access_token": "liwltoken_...",
  "refresh_token": "liwlrefresh_...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "payment_methods.agentic userinfo:read"
}

The response echoes the scope (and authorization_details) the customer actually granted. A customer can approve only a subset, so read it instead of assuming you received everything you asked for.

Store both tokens securely on your server. Access tokens last one hour. Refresh tokens last one year and rotate every time they’re used. Atomically replace the stored refresh token with the new one from each refresh response.

Create the SDK client

The SDK accepts an OAuth access token, but it doesn’t perform login, persist credentials, or own refresh tokens. Provide a function that returns a current access token. The SDK calls it before every request and, as a fallback after a 401, calls it once more with a forced-refresh signal.

In these examples, credentialManager (credential_manager in Python) is your application’s token store, not a Link SDK export. It returns the customer’s current token and refreshes it when requested.

import Link from '@stripe/link-sdk'
 
const link = new Link({
  getAccessToken: async ({ forceRefresh } = {}) =>
    credentialManager.getLinkAccessToken({ forceRefresh }),
})

Your credential manager should track token expiry, coalesce concurrent refreshes, persist rotated refresh tokens safely, and keep all tokens out of agent context, logs, and client-side code.

Choose a payment method

List the payment methods in the connected Link wallet and select the default eligible method:

const methods = await link.paymentMethods.list()
const eligibleMethods = methods.filter(
  (method) => method.capabilities?.agentic_payments?.eligible !== false,
)
const paymentMethod =
  eligibleMethods.find((method) => method.is_default) ?? eligibleMethods[0]
 
if (!paymentMethod) {
  throw new Error('The user needs to add an eligible Link payment method.')
}

Ineligible methods include a capabilities.agentic_payments object with eligible: false and ineligibility_reasons.

Link CLI calls this selector payment_method_id, while the SDKs send the selected method ID as payment_details.

Create a spend request

Describe what the agent is buying and why. Amounts use the currency’s minor unit, such as cents for USD, and context must contain at least 100 characters.

const spendRequest = await link.spendRequests.create({
  payment_details: paymentMethod.id,
  credential_type: 'card',
  amount: 2599,
  currency: 'usd',
  merchant_name: 'Acme',
  merchant_url: 'https://acme.example',
  context:
    'The user asked the agent to buy the selected item from Acme for $25.99, including the displayed shipping cost.',
})

Pass an idempotency_key (IdempotencyKey in Go) with a stable, non-sensitive value for the purchase so that a retried create returns the original request. See Error handling.

Ask for approval

Request approval, send the returned URL to the customer, and save the spend request ID so a later agent run can resume the purchase. The sendToUser and saveRunState helpers (and their Python equivalents) represent your application’s messaging and persistent storage.

const approval = await link.spendRequests.requestApproval(spendRequest.id)
const approvalDeadline = Date.now() + 30 * 60 * 1000
 
await sendToUser(`Approve this purchase: ${approval.approval_url}`)
await saveRunState({ spendRequestId: spendRequest.id, approvalDeadline })

The customer has 30 minutes to approve the request.

Return the customer to your app

Optionally, send the customer back to your application after they finish an approval or required action. Supply a completion callback when you create the request. This is separate from the OAuth redirect_uri that you registered for authorization.

InputCLI flagSDK fieldRequirements
Callback URL--completion-callback-urlcompletion_callback_url (TypeScript, Python), CompletionCallbackURL (Go)A valid URL of at most 2,048 characters. In live mode, the API requires HTTPS and enforces URL security rules.
Nonce--noncenonce (TypeScript, Python), Nonce (Go)Nonblank, at most 512 JavaScript/UTF-16 code units.

Both inputs are create-only. Supply both or neither. Link CLI and the SDK agent-tool schemas validate these constraints locally. The language SDKs pass the values through, and the API performs the server-side validation, so don’t rely on identical client-side errors across SDKs.

const nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url')
await saveRunState({ nonce })
 
const spendRequest = await link.spendRequests.create({
  // ...the purchase fields from the previous example
  completion_callback_url: 'https://agent.example.com/link/callback',
  nonce,
})

The CLI equivalent adds --completion-callback-url "https://agent.example.com/link/callback" --nonce "$NONCE" to spend-request create.

Follow these rules:

  • Generate an opaque, cryptographically unpredictable nonce. Save it unchanged and verify an exact match when the callback arrives. Don’t trim or normalize it.
  • Use the returned approval_url and status_details.requires_action.next_action.action_url as-is. Don’t append callback parameters yourself, because not every action URL carries the callback.
  • A callback arriving doesn’t mean the customer approved or the payment succeeded. Retrieve the request and inspect its current status before you continue. The polling and required-action rules still apply.
  • The completion callback isn’t the OAuth redirect and isn’t a webhook registration. Keep your existing polling or retrieval logic as the source of truth.

Wait for the approval result

The SDKs don’t poll automatically. Follow the approval polling rules, including how to show required actions and recover from a timeout.

Use the polling example for your language with the request ID and deadline you saved in the previous step:

Save the returned spend request as result for the next step. Load the request ID and deadline from your application’s saved state when resuming a purchase.

Retrieve and use the credential

After the request is approved, retrieve again with the card expansion. This keeps the full credential out of routine polling responses. The checkoutWorker and notifyUser helpers (and their Python equivalents) represent your application’s checkout executor and customer messaging. Keep the checkout executor outside model context.

if (result.status !== 'approved') {
  await notifyUser(`Purchase not approved: ${result.status}`)
  return
}
 
const approved = await link.spendRequests.retrieve(result.id, {
  include: ['card'],
})
 
if (!approved?.card) {
  throw new Error('Approved spend request did not include a card.')
}
 
await checkoutWorker.complete(approved.card)

The returned card contains id, number, brand, exp_month, exp_year, and optional cvc, billing_address, and valid_until. Keep returned card credentials out of model context, logs, and customer-visible messages.

Refresh and revoke access

Refresh before the access token expires instead of waiting for a 401:

curl -X POST https://login.link.com/auth/token \
  -H "Authorization: Bearer pk_live_YOUR_PUBLISHABLE_KEY" \
  -d "grant_type=refresh_token" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "refresh_token=CURRENT_REFRESH_TOKEN"

When a customer disconnects your agent, revoke their refresh token. Revoking it ends the grant and invalidates the access tokens issued from it:

curl -X POST https://login.link.com/auth/revoke \
  -H "Authorization: Bearer pk_live_YOUR_PUBLISHABLE_KEY" \
  -d "client_id=YOUR_CLIENT_ID" \
  -d "client_secret=YOUR_CLIENT_SECRET" \
  -d "token=TOKEN_TO_REVOKE" \
  -d "token_type_hint=refresh_token"

Know the limits

Review the limits table for purchase amounts, request rates, approval windows, and credential validity. Use Error handling to plan recovery when a call fails.

Next steps

Extend your integration with another payment option or financial data: