Skip to content
Logo

TypeScript SDK

@stripe/link-sdk is the Node.js SDK for agents and applications that use Link. It provides typed resources for Link APIs and accepts an access token from your application.

The SDK doesn’t perform login, persist credentials, or own refresh tokens. Authentication state and user-facing authorization flows belong to Link CLI or the application embedding the SDK.

TypeScript SDK source

Install

Use an ESM project with Node.js 20 or newer. Install the package with npm:

npm install @stripe/link-sdk

Create a client

Pass an access token when creating the client:

import Link from '@stripe/link-sdk'
 
const link = new Link({
  accessToken: process.env.LINK_ACCESS_TOKEN!,
})
 
const paymentMethods = await link.paymentMethods.list()

A fixed token is appropriate for a short-lived job or when the caller replaces the entire client whenever credentials change.

The package exports Link both as the default export and as a named export, so import Link from '@stripe/link-sdk' and import { Link } from '@stripe/link-sdk' are equivalent.

Manage dynamic access tokens

When your application manages expiring credentials, provide getAccessToken. The SDK calls the callback for every request. After a 401 response, it calls the callback once with forceRefresh: true and retries with the returned token.

In this example, credentialManager is your application’s token store.

const link = new Link({
  getAccessToken: async ({ forceRefresh } = {}) =>
    credentialManager.getLinkAccessToken({ forceRefresh }),
})

Your credential manager should coalesce concurrent refreshes when several requests can receive a 401 at the same time. A client configured with a fixed accessToken doesn’t retry a 401 because it can’t obtain a different token.

You must provide exactly one credential option: accessToken or getAccessToken.

Create a customer-approved purchase

The following excerpts assume an authenticated client that remains open for the purchase flow. Your application supplies messaging, persistent run state, and checkout execution. These examples create live requests. Use test mode while developing.

Amounts use the currency’s minor unit, such as cents for USD. If you omit currency, Link uses the consumer’s default; see Currency. context must contain at least 100 characters and should tell the customer what the agent is buying and why.

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.')
}
 
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.',
})
 
const approval = await link.spendRequests.requestApproval(spendRequest.id)
await sendToUser(`Approve this purchase: ${approval.approval_url}`)
await saveRunState({
  spendRequestId: spendRequest.id,
  approvalDeadline: Date.now() + 30 * 60 * 1000,
})

payment_details is the selected payment method ID. Ineligible methods include a capabilities.agentic_payments object with eligible: false and ineligibility_reasons.

The sendToUser and saveRunState helpers represent your application’s messaging and persistent storage. Persist the spend request ID and deadline so a later agent run can continue after the customer acts.

Add recurring terms or a completion callback

Both inputs are create-only and optional. For a $20 monthly spending limit, add recurring to a create call that also sets amount: 2000 and currency: 'usd'. amount is the limit for each period:

recurring: { interval: 'month' }, // interval_count is optional and defaults to 1

interval is day, week, month, or year. update can’t change recurring terms, and cancel revokes the credential without canceling the merchant’s subscription. See Recurring payments.

To return the customer to your app after approval, pass completion_callback_url and nonce together:

completion_callback_url: 'https://agent.example.com/link/callback',
nonce, // Opaque, unpredictable, and saved unchanged for exact comparison.

The API validates these values. See Return the customer to your app.

Handle the result

Load state from your saved run state. Supply surfaceActionToUser and notifyUser to show messages to the customer, and checkoutWorker.complete to use the card outside model context. These are application helpers, not SDK exports.

The SDK doesn’t poll or register a webhook. Poll created and pending_approval yourself until the saved 30-minute approval deadline. Always show a required action’s message and any action_url to the customer. Continue the same request only for auto_resume.

const delay = (milliseconds: number) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds))
 
async function waitForApproval(spendRequestId: string, deadline: number) {
  let shownAction: string | undefined
 
  while (Date.now() < deadline) {
    const request = await link.spendRequests.retrieve(spendRequestId)
    if (!request) throw new Error('Spend request not found.')
 
    if (request.status === 'created' || request.status === 'pending_approval') {
      await delay(2_000)
      continue
    }
 
    if (request.status === 'requires_action') {
      const action = request.status_details?.requires_action?.next_action
      if (!action) {
        throw new Error('Spend request requires action without instructions.')
      }
 
      // Show the action once, not on every poll.
      const key = `${action.action_url ?? ''}|${action.expires_at ?? ''}`
      if (key !== shownAction) {
        await surfaceActionToUser(action.display_message, action.action_url)
        shownAction = key
      }
      if (action.resolution === 'auto_resume') {
        await delay(2_000)
        continue
      }
    }
 
    return request
  }
 
  throw new Error(
    'The approval window ended. Retrieve this request once more before creating another.',
  )
}
 
const result = await waitForApproval(
  state.spendRequestId,
  state.approvalDeadline,
)
 
if (result.status === 'approved') {
  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)
} else {
  await notifyUser(`Purchase not approved: ${result.status}`)
}

The second retrieve deliberately requests card only after approval. Keep returned card credentials out of model context, logs, and customer-visible messages. See Virtual card for the complete object structure.

Configure the client

Override transport and logging options as needed. Here, diagnostics represents your application’s logger:

const link = new Link({
  accessToken,
  fetch,
  defaultHeaders: { 'X-Agent-Version': 'acme-agent/1.0' },
  verbose: true,
  logger: { debug: (message) => diagnostics.debug(message) },
  apiBaseUrl: 'https://api.link.com',
  spendRequestBaseUrl: 'https://api.link.com',
})

Verbose logging includes request methods, URLs, and response status codes. It doesn’t include authorization headers or request and response bodies.

Handle errors

The SDK exports typed errors:

  • LinkConfigurationError for invalid or missing client configuration
  • LinkTransportError when a request can’t reach Link
  • LinkApiError for non-success API responses
  • LinkResponseError when a successful response has an invalid shape

LinkApiError includes status, code, and structured details fields.

import { LinkApiError } from '@stripe/link-sdk'
 
try {
  await link.spendRequests.retrieve('spend_request_id')
} catch (error) {
  if (error instanceof LinkApiError) {
    diagnostics.error({
      status: error.status,
      code: error.code,
      details: error.details,
    })
  }
  throw error
}

spendRequests.retrieve is the exception to normal API-error handling: it returns null for HTTP 404. The SDK makes no automatic retry for transport errors, 429, or 5xx. Its only built-in retry is one 401 retry when a dynamic token provider returns a refreshed token.

Pass an idempotency_key (1–255 UTF-8 bytes, with no personal data) to spendRequests.create so a retried create returns the original response instead of a second request. Link rejects a duplicate create with HTTP 429 and the code spend_request_rate_limited, and the error details include the existing request. getDuplicateSpendRequest returns that request, or undefined if the error isn’t a duplicate rejection:

import { getDuplicateSpendRequest } from '@stripe/link-sdk'
 
try {
  await link.spendRequests.create(params)
} catch (error) {
  const duplicate = getDuplicateSpendRequest(error)
  if (duplicate) {
    await saveRunState({ spendRequestId: duplicate.id })
    return
  }
  throw error
}

For other transient failures, use bounded backoff and your own operation tracking. See Error handling. After an uncertain create outcome, recover an existing request where possible instead of retrying blindly.

Manage spend requests

Use an authenticated client to update, request approval for, retrieve, list, or cancel a request. These examples show separate operations. Follow the spend request lifecycle to determine which operation applies, including how to increase the amount.

await link.spendRequests.update('lsrq_001', {
  merchant_url: 'https://press.stripe.com/working-in-public',
})
 
const approval = await link.spendRequests.requestApproval('lsrq_001')
const request = await link.spendRequests.retrieve('lsrq_001')
const requests = await link.spendRequests.list()
await link.spendRequests.cancel('lsrq_001')

Update a payment method

Pass nickname, is_default, or both. Omitted fields stay unchanged.

// Set the wallet default
await link.paymentMethods.update('pd_123', { is_default: true })
 
// Clear its default status without changing its nickname
await link.paymentMethods.update('pd_123', { is_default: false })

Setting is_default: true replaces the previous wallet default. This changes the customer’s wallet-level default, not just the payment method for one spend request, so do it only when the customer asks. To pick a method for one purchase, pass its ID as payment_details when you create the spend request.

Report purchase outcomes

Use the reports resource to record a successful or blocked purchase attempt. Reporting is optional. See Outcome reporting for supported outcomes, tags, and personal-data handling.

await link.reports.create({
  domain: 'shop.example.com',
  outcome: 'success',
  spend_request_id: 'lsrq_abc123',
})
 
await link.reports.create({
  domain: 'shop.example.com',
  outcome: 'blocked',
  spend_request_id: 'lsrq_abc123',
  tags: ['captcha'],
  step: 'checkout page',
  freeform_context: 'Turnstile challenge appeared',
})

Client resources

Use these resources on an authenticated client:

ResourceOperations
spendRequestsCreate, request approval, retrieve, update, cancel, and list
paymentMethodsList, retrieve, and update (nickname and default status) Link payment methods
shippingAddressesList shipping addresses
userInfoRetrieve Link user information, spend limits, and verification requirements
transactionsList transactions
insightsList available insight types and retrieve insights
sourcesList connected sources
balancesList balances
reportsReport agent outcomes

Financial-data resources (transactions, sources, balances, and insights) need the source actions described in Financial insights.

To get a credential, retrieve the request after approval with include set to the credential you need: ['card'], ['shared_payment_token'], or ['link_pay_token']. The matching field (card, shared_payment_token, or link_pay_token) is set on the returned request.

Use the SDK as agent tools

The @stripe/link-sdk/tools entry point wraps a client as a set of ready-made agent tools, with schemas you can hand to your agent framework:

import { Link } from '@stripe/link-sdk'
import { createLinkTools } from '@stripe/link-sdk/tools'
 
const tools = createLinkTools(new Link({ accessToken }))
const methods = await tools.list_payment_methods.execute({}, undefined)

createLinkTools also accepts a function that resolves a client per call, which is useful when each request runs as a different customer. The tool names match the resources in the previous table, for example list_payment_methods, create_spend_request, request_spend_approval, list_transactions, and list_insights. Require your own customer confirmation before you let an agent call create_spend_request.