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.
Install
Use Python 3.11 or newer. Install the stripe-link-sdk package from PyPI:
uv add stripe-link-sdkpip install stripe-link-sdkThe 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 1interval 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"andnetwork_id. Retrieve it withinclude=["shared_payment_token"]and readshared_payment_token. - For a Link Pay Token, pass
credential_type="link_pay_token"andmerchant_account_id, then retrieve withinclude=["link_pay_token"]and readlink_pay_token. See Link Pay Tokens. - Pass
test=Trueto 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:
| Option | Behavior |
|---|---|
api_base_url | Defaults to https://api.link.com. |
spend_request_base_url | Defaults to api_base_url. Used only for spend requests. |
default_headers | Copied at construction. Resource content types and bearer authorization take precedence. |
http_client | An httpx.Client or httpx.AsyncClient for custom timeouts, transports, proxies, and tracing. |
timeout | Seconds, an httpx.Timeout, or None to disable timeouts. Defaults to 30 seconds for SDK-owned HTTP clients. |
verbose, logger | Send 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:
LinkConfigurationErrorfor invalid credentials or configurationLinkTransportErrorwhen a request fails or a response can’t be readLinkAPIErrorfor non-success API responses, withstatus,code,raw_body, anddetailsLinkResponseErrorwhen a successful response can’t be decodedLinkSDKErrorfor 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 = duplicateManage 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.
| Resource | Operations |
|---|---|
spend_requests | create, update, retrieve, cancel, list, and request_approval |
payment_methods | list and update (nickname and default status) |
shipping_addresses | list |
user_info | retrieve |
transactions | list |
sources | list |
balances | list |
reports | create |
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.