Skip to content
Logo

Python SDK

The Link Python SDK provides typed synchronous and asynchronous clients for agents and applications that use Link. It accepts an access token from your application and requires Python 3.11 or newer.

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.

Python SDK source

Install

Use Python 3.11 or newer. Install the stripe-link-sdk package from PyPI:

uv add stripe-link-sdk

The distribution is named stripe-link-sdk, and the import name is link.

Create a client

Pass an access token when creating the client. The SDK reads no environment variables itself, so pass credentials explicitly.

import os
 
from link import Client
 
with Client(access_token=os.environ["LINK_ACCESS_TOKEN"]) as client:
    payment_methods = client.payment_methods.list()
    for method in payment_methods:
        print(method.id, method.name, method.is_default)

For async applications, use AsyncClient with the same resource methods and await:

import asyncio
import os
 
from link import AsyncClient
 
 
async def main() -> None:
    async with AsyncClient(access_token=os.environ["LINK_ACCESS_TOKEN"]) as client:
        user = await client.user_info.retrieve()
        print(user.name)
 
 
asyncio.run(main())

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

Manage dynamic access tokens

Provide exactly one of access_token or get_access_token. When your application manages expiring credentials, pass a token callback. Here, credential_manager is your application’s token store:

from link import Client, GetAccessTokenOptions
 
 
def get_token(options: GetAccessTokenOptions) -> str:
    return credential_manager.link_access_token(force_refresh=options.force_refresh)
 
 
with Client(get_access_token=get_token) as client:
    methods = client.payment_methods.list()

The SDK calls the callback for every request with force_refresh=False. After a 401 response, it calls the callback once more with force_refresh=True and retries the request once. A client with a fixed access_token doesn’t retry. AsyncClient accepts either a synchronous callback or an async callback. Use an async callback when getting a token involves I/O.

Your credential manager should coalesce concurrent refreshes. An empty or non-string callback result raises LinkConfigurationError.

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. context must contain at least 100 characters and should tell the customer what the agent is buying and why.

methods = client.payment_methods.list()
if not methods:
    raise RuntimeError("The user needs to add a Link payment method")
method = next((method for method in methods if method.is_default), methods[0])
 
request = client.spend_requests.create(
    idempotency_key=purchase_id,
    payment_details=method.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."
    ),
)
approval = client.spend_requests.request_approval(request.id)
print(f"Approve this purchase: {approval.approval_url}")
# Persist request.id and the approval deadline (time.time() + 30 * 60)
# so a later run can continue.

purchase_id is a stable, non-sensitive identifier for this purchase attempt, such as a UUID you store with the order, so a retried create returns the original request instead of a second one. The synchronous Client is shown here. The polling loop and web service that follow use AsyncClient, and every resource method works the same way with await.

Poll for the result

The following asynchronous example uses AsyncClient. Supply surface_action_to_user as an asynchronous application helper that displays the message and action URL to the customer.

The SDK doesn’t poll automatically. Poll created and pending_approval until the saved 30-minute approval deadline, and keep polling requires_action only for auto_resume. Show an action to the customer once, when its action_url or expires_at changes, instead of on every poll.

import asyncio
import time
 
from link import AsyncClient
 
 
async def wait_for_approval(
    client: AsyncClient, spend_request_id: str, deadline: float
):
    shown_action = None
    while time.time() < deadline:
        request = await client.spend_requests.retrieve(spend_request_id)
        if request is None:
            raise RuntimeError("Spend request not found")
 
        if request.status in ("created", "pending_approval"):
            await asyncio.sleep(2)
            continue
 
        if request.status == "requires_action":
            details = request.status_details
            if details is None or details.requires_action is None:
                raise RuntimeError("Spend request requires action without instructions")
            action = details.requires_action.next_action
 
            key = (action.action_url, action.expires_at)
            if key != shown_action:
                await surface_action_to_user(action.display_message, action.action_url)
                shown_action = key
 
            if action.resolution == "auto_resume":
                await asyncio.sleep(2)
                continue
 
        return request
 
    raise TimeoutError(
        "The approval window ended. Retrieve this request once more before creating another."
    )

Use the result

Your application supplies the asynchronous checkout_worker.complete and notify_user helpers. Pass deadline as a wall-clock time (time.time() + 30 * 60) that you saved when you requested approval, so a later run can continue. When the loop returns, branch on the status, and request card only after approval:

result = await wait_for_approval(client, spend_request_id, deadline)
 
if result.status == "approved":
    approved = await client.spend_requests.retrieve(result.id, include=["card"])
    if approved is None or approved.card is None:
        raise RuntimeError("Approved spend request did not include a card")
    await checkout_worker.complete(approved.card)
elif result.status == "requires_action":
    # Only auto_resume keeps the same request alive. Create a new request
    # once the user has completed the action.
    await notify_user("Finish the requested action, then start the purchase again.")
elif result.status in ("submitted", "succeeded"):
    await notify_user("The payment is on its way.")
else:
    await notify_user(f"Purchase not approved: {result.status}")

Keep returned card credentials out of model context, logs, and customer-visible messages. Credential fields are omitted from a model’s repr, but model_dump() and model_dump_json() include them. The card has id, brand, number, cvc, exp_month, exp_year, billing_address, and valid_until. See Virtual card for the complete object structure.

Add recurring terms or a completion callback

Both inputs are create-only and optional, and the sync and async clients accept them. For a $20 monthly spending limit, pass amount=2000, currency="usd", and recurring. 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=nonce,  # Opaque, unpredictable, and saved unchanged for exact comparison.

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

Request other credential types

spend_requests.create takes keyword-only arguments. Besides context, which is required, you can pass credential_type, network_id, merchant_account_id, test, idempotency_key, line_items, totals, metadata, and the other fields described in Spend requests.

  • For a Shared Payment Token, pass credential_type="shared_payment_token" and network_id. Retrieve it with include=["shared_payment_token"] and read shared_payment_token.
  • For a Link Pay Token, pass credential_type="link_pay_token" and merchant_account_id, then retrieve with include=["link_pay_token"] and read link_pay_token. See Link Pay Tokens.
  • Pass test=True to receive test credentials. Test mode isn’t available for Link Pay Tokens.

The SDK passes include values through as given.

Use the client in a web service

This FastAPI excerpt assumes your application provides the current_user authentication dependency and a token_store that returns the customer’s current access token.

Create one httpx.AsyncClient when your service starts and share it across requests. Create a Link AsyncClient per customer request with that customer’s current access token, and pass the shared HTTP client in. Exiting the Link client doesn’t close an HTTP client you provide.

from contextlib import asynccontextmanager
 
import httpx
from fastapi import Depends, FastAPI
from link import AsyncClient
 
 
@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.http = httpx.AsyncClient(timeout=httpx.Timeout(30, connect=5))
    yield
    await app.state.http.aclose()
 
 
app = FastAPI(lifespan=lifespan)
 
 
@app.post("/purchases/{spend_request_id}/approval")
async def request_approval(spend_request_id: str, user=Depends(current_user)):
    # Your token store returns a current access token for this user and
    # coalesces concurrent refreshes.
    token = await token_store.fresh_access_token(user.id)
 
    async with AsyncClient(
        access_token=token, http_client=app.state.http
    ) as client:
        approval = await client.spend_requests.request_approval(spend_request_id)
    return {"approval_url": approval.approval_url}

Configure the client

Both constructors accept the following options:

OptionBehavior
api_base_urlDefaults to https://api.link.com.
spend_request_base_urlDefaults to api_base_url. Used only for spend requests.
default_headersCopied at construction. Resource content types and bearer authorization take precedence.
http_clientAn httpx.Client or httpx.AsyncClient for custom timeouts, transports, proxies, and tracing.
timeoutSeconds, an httpx.Timeout, or None to disable timeouts. Defaults to 30 seconds for SDK-owned HTTP clients.
verbose, loggerSend method, URL, and status diagnostics to a Python logger. Authorization headers and bodies are never logged.
import httpx
from link import Client
 
with Client(access_token=access_token, timeout=httpx.Timeout(30, connect=5)) as client:
    balances = client.balances.list(limit=20)

Base URLs must be absolute HTTP(S) URLs without a query or fragment. Exiting a client’s context manager, or calling close() or aclose(), closes only HTTP clients the SDK created. You close any client you inject. Reuse clients for connection pooling.

Handle errors

All SDK-created exceptions inherit LinkError:

  • LinkConfigurationError for invalid credentials or configuration
  • LinkTransportError when a request fails or a response can’t be read
  • LinkAPIError for non-success API responses, with status, code, raw_body, and details
  • LinkResponseError when a successful response can’t be decoded
  • LinkSDKError for other SDK errors

spend_requests.retrieve is the exception to normal API-error handling: it returns None 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 token callback returns a refreshed token.

Pass an idempotency_key to spend_requests.create so a retried create returns the original response. A duplicate is rejected with HTTP 429 and the code spend_request_rate_limited. See Error handling. If a create error includes a duplicate request, recover it instead of creating another:

from link import LinkAPIError, get_duplicate_spend_request
 
try:
    request = client.spend_requests.create(
        context=purchase_context, idempotency_key=stable_purchase_id
    )
except LinkAPIError as error:
    duplicate = get_duplicate_spend_request(error)
    if duplicate is None:
        raise
    request = duplicate

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.

client.spend_requests.update(
    "lsrq_001", merchant_url="https://press.stripe.com/working-in-public"
)
 
approval = client.spend_requests.request_approval("lsrq_001")
request = client.spend_requests.retrieve("lsrq_001")
requests = client.spend_requests.list()
client.spend_requests.cancel("lsrq_001")

Update a payment method

Pass nickname, is_default, or both to payment_methods.update. Omitted fields stay unchanged, and an empty nickname clears it.

# Set the wallet default
client.payment_methods.update("pd_123", is_default=True)
 
# Clear its default status without changing its nickname
client.payment_methods.update("pd_123", is_default=False)
 
# Change both
client.payment_methods.update("pd_123", nickname="Work card", is_default=True)

AsyncClient accepts the same arguments with await. Setting a default 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.

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.

client.reports.create(
    domain="shop.example.com",
    outcome="success",
    spend_request_id="lsrq_abc123",
)
 
client.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

Both client styles expose these resources. Arguments after a resource ID are keyword-only, and list methods return one page of results without auto-pagination.

ResourceOperations
spend_requestscreate, update, retrieve, cancel, list, and request_approval
payment_methodslist and update (nickname and default status)
shipping_addresseslist
user_inforetrieve
transactionslist
sourceslist
balanceslist
reportscreate
history = client.spend_requests.list(include_history=True)
page = client.transactions.list(
    limit=20, starting_after="lbctxn_abc123", start_date="2026-01-01", sources=["csmrpd_abc123"]
)
report = client.reports.create(
    domain="merchant.example", outcome="success", spend_request_id="sr_123", tags=[]
)

Use Link CLI or the TypeScript SDK for anything the Python SDK doesn’t cover.

Work with types

Responses are Pydantic models with attribute access. Request methods have typed keyword arguments, and nested request objects use dictionaries. Optional request values set to None are omitted. Explicit empty collections, False, and 0 are sent. Pass include and sources as sequences such as ["card"], because bare strings are rejected.

Response fields accept future string values, including new spend request statuses. Required response fields must be present and correctly typed, or the SDK raises LinkResponseError. Unknown properties on TransactionsPage, Source, SourcesPage, Balance, and BalancesPage are kept in model_extra.