> ## Documentation Index
> Fetch the complete documentation index at: https://tbd-6fc993ce-hypeship-link-checkout-bound-cards.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# link by stripe

> use link by stripe to approve a one-use payment credential for a browser checkout

[link by stripe](https://stripe.com/payments/link) connects a user's wallet through oauth and issues a one-use payment credential for an approved purchase. KERNEL's native integration handles wallet connection, token refresh, approval actions, and credential storage through vault items. oauth credentials and payment material use kms-backed envelope encryption.

you create a link card item at the final checkout page, and KERNEL decides how link pays for that checkout. KERNEL's [`fill` api](/vaults/fill) then puts the approved credential into the attached browser without passing it through your application's code or model context. see [how KERNEL pays](#how-kernel-pays) for details.

link is the wallet provider, not the merchant's payment processor. for the overall offering, see [payments on KERNEL](/browsers/payments).

## Before you start

<Note>
  link currently supports only users with US phone numbers.
</Note>

create a project-scoped client and vault. the examples below use these `kernel` and `vault` variables.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import Kernel from "@onkernel/sdk";

  const kernel = new Kernel({
    projectID: process.env.KERNEL_PROJECT_ID!,
    maxRetries: 0,
  });
  const vault = await kernel.vaults.upsert({ name: "user-12345" });
  ```

  ```python Python theme={null}
  import os

  from kernel import Kernel

  kernel = Kernel(project_id=os.environ["KERNEL_PROJECT_ID"], max_retries=0)
  vault = kernel.vaults.upsert(name="user-12345")
  ```

  ```bash CLI theme={null}
  kernel vaults create --name user-12345
  ```
</CodeGroup>

## Choose an oauth client

use KERNEL's oauth client by default. if you bring your own client, complete the
setup below before following the shared payment lifecycle.

### bring your own link oauth client

register a [provider configuration](/integrations/wallets/overview#provider-configurations) once for
your organization. configurations are shared across projects, so authenticate
this step with an [organization-scoped api key](/info/api-keys). project-scoped
credentials can reference an existing configuration when creating a wallet, but
can't create, update, or delete configurations.

keep the client credentials outside the agent's accessible files. the SDK
examples read them from backend environment variables. for the CLI, create a
protected file readable only by its owner (for example, mode `0600`):

```json link-client.json theme={null}
{
  "client_id": "example-client-id",
  "client_secret": "example-client-secret"
}
```

<CodeGroup>
  ```typescript TypeScript theme={null}
  const orgKernel = new Kernel({
    apiKey: process.env.KERNEL_ORG_API_KEY!,
  });
  const linkConfig = await orgKernel.vaultProviderConfigs.create({
    name: "checkout-link",
    provider: "link",
    credentials: {
      client_id: process.env.LINK_CLIENT_ID!,
      client_secret: process.env.LINK_CLIENT_SECRET!,
    },
  });
  ```

  ```python Python theme={null}
  org_kernel = Kernel(api_key=os.environ["KERNEL_ORG_API_KEY"])
  link_config = org_kernel.vault_provider_configs.create(
      name="checkout-link",
      provider="link",
      credentials={
          "client_id": os.environ["LINK_CLIENT_ID"],
          "client_secret": os.environ["LINK_CLIENT_SECRET"],
      },
  )
  ```

  ```bash CLI theme={null}
  # run with KERNEL_API_KEY set to an organization-scoped key
  kernel vault-provider-configs create --name checkout-link --provider link \
    --credentials-file "$HOME/.config/kernel/link-client.json"
  ```
</CodeGroup>

for each end user, complete your existing link oauth flow in your backend and
obtain its access and refresh tokens. keep the client secret, pkce verifier, and
tokens out of agent context, browser code, urls, and logs.

create the wallet with the project-scoped `kernel` client and `vault` from
[Before you start](#before-you-start). supply the access and refresh tokens from
the same grant, with a currently valid access token. CLI users should restore
their project-scoped `KERNEL_API_KEY` before running the wallet command and put
the grant in this protected file:

```json link-grant.json theme={null}
{
  "access_token": "example-access-token",
  "refresh_token": "example-refresh-token"
}
```

<CodeGroup>
  ```typescript TypeScript theme={null}
  const wallet = await kernel.vaults.items.upsert("link-wallet", {
    id_or_name: vault.id,
    type: "wallet",
    spec: {
      provider: "link",
      authorization: {
        method: "oauth",
        client: {
          type: "customer_managed",
          provider_config: { name: linkConfig.name },
        },
        tokens: {
          access_token: process.env.LINK_ACCESS_TOKEN!,
          refresh_token: process.env.LINK_REFRESH_TOKEN!,
        },
      },
    },
  });
  ```

  ```python Python theme={null}
  wallet = kernel.vaults.items.upsert(
      "link-wallet",
      id_or_name=vault.id,
      type="wallet",
      spec={
          "provider": "link",
          "authorization": {
              "method": "oauth",
              "client": {
                  "type": "customer_managed",
                  "provider_config": {"name": link_config.name},
              },
              "tokens": {
                  "access_token": os.environ["LINK_ACCESS_TOKEN"],
                  "refresh_token": os.environ["LINK_REFRESH_TOKEN"],
              },
          },
      },
  )
  ```

  ```bash CLI theme={null}
  kernel vaults wallets create user-12345 link-wallet --provider link \
    --provider-config-name checkout-link --spec '{}' \
    --tokens-file "$HOME/.config/kernel/link-grant.json"
  ```
</CodeGroup>

a successful import returns a `connected` wallet without a `link_oauth` action.
KERNEL takes over refresh-token rotation, so your backend must stop refreshing
that grant. continue with [Select a payment method](#select-a-payment-method).

## Lifecycle

1. connect a wallet: use KERNEL's client and present the returned `link_oauth` action, or complete oauth in your backend and import the grant with your provider configuration.
2. require the wallet to be `connected`.
3. request the advertised `payment_methods` expansion and let the user choose an eligible method.
4. create a browser with the vault attached, navigate it to the final checkout page, and verify the purchase.
5. create a `card` item with that browser's session id, the checkout's exact `page_url`, and the purchase details. creation starts link's spend request and returns a `spend_approval` or `push_approval` action.
6. present the action to the user and wait for `state.status` to become `ready`.
7. invoke the advertised `fill` operation with the inputs its description names, inspect the outcome, then have the agent click the checkout's pay button once.

creating the card item is the request for approval. card items are immutable and bound to one browser session and checkout page.

## Connect a wallet

this section shows the KERNEL-managed oauth path. if you imported a connected
wallet with your own client above, continue with
[Select a payment method](#select-a-payment-method).

before showing a link connection option, list the vault's items. if a
link wallet already exists in any state, reuse it and do not let the user add
another. show its existing action or status instead. the api makes item keys
unique but does not currently enforce one wallet per provider, so the ui must
enforce a maximum of one link wallet per vault.

the examples use `presentProviderAction`, an application-owned function that
publishes the action to an authenticated session for the end user who owns the
vault. bind the action to that user, vault, and item; apply a short application
ttl capped by `wallet.expires_at` when present; and stop serving it when the
action changes or disappears. derive `authenticatedUser` from the server-side
session, not a request field. do not log the url or put it in model context.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const items = await kernel.vaults.items.list(vault.id);
  const linkWallets = items.filter(
    (item) => item.type === "wallet" && item.spec.provider === "link",
  );
  if (linkWallets.length > 1) {
    throw new Error("vault has more than one link wallet");
  }

  let wallet = linkWallets[0];
  if (!wallet) {
    wallet = await kernel.vaults.items.upsert("link-wallet", {
      id_or_name: vault.id,
      type: "wallet",
      spec: {
        provider: "link",
        authorization: {
          method: "oauth",
          client: { type: "kernel_managed" },
        },
      },
    });
  }

  if (wallet.action?.name === "link_oauth") {
    await presentProviderAction({
      userID: authenticatedUser.id,
      vaultID: vault.id,
      item: wallet,
    });
  }
  wallet = await kernel.vaults.items.retrieve(wallet.key, {
    id_or_name: vault.id,
    wait: 60,
  });
  ```

  ```python Python theme={null}
  items = kernel.vaults.items.list(vault.id)
  link_wallets = [
      item
      for item in items
      if item.type == "wallet" and item.spec.provider == "link"
  ]
  if len(link_wallets) > 1:
      raise RuntimeError("vault has more than one link wallet")

  wallet = link_wallets[0] if link_wallets else None
  if wallet is None:
      wallet = kernel.vaults.items.upsert(
          "link-wallet",
          id_or_name=vault.id,
          type="wallet",
          spec={
              "provider": "link",
              "authorization": {
                  "method": "oauth",
                  "client": {"type": "kernel_managed"},
              },
          },
      )

  if wallet.action is not None and wallet.action.name == "link_oauth":
      present_provider_action(
          user_id=authenticated_user.id,
          vault_id=vault.id,
          item=wallet,
      )
  wallet = kernel.vaults.items.retrieve(
      wallet.key,
      id_or_name=vault.id,
      wait=60,
  )
  ```

  ```bash CLI theme={null}
  # create only when the list has no link wallet
  kernel vaults items list user-12345 -o json
  kernel vaults wallets create user-12345 link-wallet \
    --provider link \
    --spec '{"authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}' \
    --open
  kernel vaults items get user-12345 link-wallet --wait 60
  ```
</CodeGroup>

<Warning>
  open collection and approval urls in a trusted user-facing surface. don't give
  them to the agent or print full wallet responses into model context. run cli
  `--open` only from a trusted, human-operated terminal because the command
  output can contain the action url.
</Warning>

## Select a payment method

request `payment_methods` only when `available_expansions` advertises it. the expansion comes from link at request time and is not persisted in the vault item.

<CodeGroup>
  ```typescript TypeScript theme={null}
  if (
    !wallet.available_expansions.some(({ type }) => type === "payment_methods")
  ) {
    throw new Error("payment methods are unavailable");
  }

  wallet = await kernel.vaults.items.retrieve(wallet.key, {
    id_or_name: vault.id,
    expand: ["payment_methods"],
  });

  const methods = wallet.expanded?.payment_methods ?? [];
  for (const method of methods) {
    console.log(method.id, method.display, method.capabilities);
  }

  const paymentMethod = methods.find(
    ({ id }) => id === process.env.LINK_PAYMENT_METHOD_ID,
  );
  if (
    !paymentMethod ||
    paymentMethod.capabilities.single_use_card?.eligible === false
  ) {
    throw new Error("select an available payment method");
  }
  ```

  ```python Python theme={null}
  if not any(expansion.type == "payment_methods" for expansion in wallet.available_expansions):
      raise RuntimeError("payment methods are unavailable")

  wallet = kernel.vaults.items.retrieve(
      wallet.key,
      id_or_name=vault.id,
      expand=["payment_methods"],
  )

  methods = (wallet.expanded.payment_methods or []) if wallet.expanded else []
  for method in methods:
      print(method.id, method.display, method.capabilities)

  payment_method = next(
      (
          method
          for method in methods
          if method.id == os.environ["LINK_PAYMENT_METHOD_ID"]
      ),
      None,
  )
  if payment_method is None or (
      payment_method.capabilities.single_use_card is not None
      and payment_method.capabilities.single_use_card.eligible is False
  ):
      raise RuntimeError("select an available payment method")
  ```

  ```bash CLI theme={null}
  kernel vaults wallets payment-methods user-12345 link-wallet -o json
  ```
</CodeGroup>

show the returned methods in a trusted user-facing surface, let the user choose one, and set its id as `LINK_PAYMENT_METHOD_ID`. missing capability metadata means eligibility is unknown. only `eligible: false` is an explicit negative result.

## Open the checkout

create the card item only after the agent reaches the final checkout page, the page where the next step is paying. KERNEL inspects that page when you create the card, so the browser session must be running, have the vault attached, and have the checkout open at that url.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const browser = await kernel.browsers.create({
    vaults: [{ id: vault.id }],
    headless: false,
    timeout_seconds: 1800,
  });
  // navigate the agent to checkout, then read the exact current top-level url
  const checkoutURL = "https://shop.example.com/checkout?cart=cart_8472";
  ```

  ```python Python theme={null}
  browser = kernel.browsers.create(
      vaults=[{"id": vault.id}],
      headless=False,
      timeout_seconds=1800,
  )
  # navigate the agent to checkout, then read the exact current top-level url
  checkout_url = "https://shop.example.com/checkout?cart=cart_8472"
  ```

  ```bash CLI theme={null}
  kernel browsers create --vault user-12345 -o json
  ```
</CodeGroup>

`checkoutURL` / `checkout_url` is the browser's exact current top-level https url, including path, query, and fragment, obtained through your browser-control method. verify the merchant, amount, currency, and items in your controller before creating the card. see [verify and confirm the purchase](/browsers/enable-payments-in-browser-agent#2-verify-and-confirm-the-purchase).

## Create a card item at checkout

creating the card item starts link's spend request and the user's approval. use the session id from the browser above, not a browser name.

<CodeGroup>
  ```typescript TypeScript theme={null}
  let card = await kernel.vaults.items.upsert("notebook-order", {
    id_or_name: vault.id,
    type: "card",
    spec: {
      provider: "link",
      wallet: wallet.key,
      browser_id: browser.session_id,
      page_url: checkoutURL,
      payment_method_id: paymentMethod.id,
      amount: 2306,
      currency: "usd",
      merchant_name: "example shop",
      context:
        "buy one notebook from example shop for a total of 23.06 usd, including tax " +
        "and shipping. this request is for this purchase only and must not be repeated.",
    },
  });

  if (card.action && "url" in card.action) {
    await presentProviderAction({
      userID: authenticatedUser.id,
      vaultID: vault.id,
      item: card,
    });
  } else if (card.action?.name === "push_approval") {
    console.log("complete the approval in your link app");
  }

  card = await kernel.vaults.items.retrieve(card.key, {
    id_or_name: vault.id,
    wait: 60,
  });
  ```

  ```python Python theme={null}
  card = kernel.vaults.items.upsert(
      "notebook-order",
      id_or_name=vault.id,
      type="card",
      spec={
          "provider": "link",
          "wallet": wallet.key,
          "browser_id": browser.session_id,
          "page_url": checkout_url,
          "payment_method_id": payment_method.id,
          "amount": 2306,
          "currency": "usd",
          "merchant_name": "example shop",
          "context": (
              "buy one notebook from example shop for a total of 23.06 usd, including tax "
              "and shipping. this request is for this purchase only and must not be repeated."
          ),
      },
  )

  if card.action is not None and hasattr(card.action, "url"):
      present_provider_action(
          user_id=authenticated_user.id,
          vault_id=vault.id,
          item=card,
      )
  elif card.action is not None and card.action.name == "push_approval":
      print("complete the approval in your link app")

  card = kernel.vaults.items.retrieve(card.key, id_or_name=vault.id, wait=60)
  ```

  ```bash CLI theme={null}
  # browser_id is the session_id returned by kernel browsers create
  kernel vaults cards create user-12345 notebook-order \
    --provider link \
    --spec '{
      "wallet": "link-wallet",
      "browser_id": "k7q2m9x4p1w8",
      "page_url": "https://shop.example.com/checkout?cart=cart_8472",
      "payment_method_id": "pm_123",
      "amount": 2306,
      "currency": "usd",
      "merchant_name": "example shop",
      "context": "buy one notebook from example shop for a total of 23.06 usd, including tax and shipping. this request is for this purchase only and must not be repeated."
    }'
  kernel vaults items get user-12345 notebook-order --wait 60 -o json
  ```
</CodeGroup>

the new card returns `state.status: pending_authorization` and an `action`. `spend_approval` includes a `url` the user opens to approve the purchase in link; `push_approval` means the user approves in their link app. after approval, the card becomes `ready` and advertises `fill`. one bounded wait can finish before approval does; retrieve the item again rather than creating another card.

`amount` uses minor currency units, so `2306` means 23.06 usd. `context` must contain at least 100 characters. card creation is live-only, and `spec.test` is not supported.

### How KERNEL pays

when you create the card, KERNEL inspects the checkout page through [WebMCP](/browsers/webmcp) and chooses how link pays. you never choose or see the mode; read the card's advertised `fill` operation instead.

| checkout                                                        | payment credential                                                           | fill inputs                                          | maximum `amount` |
| --------------------------------------------------------------- | ---------------------------------------------------------------------------- | ---------------------------------------------------- | ---------------- |
| a stripe checkout page that exposes link pay token WebMCP tools | a merchant-bound link pay token that KERNEL passes to the page's WebMCP tool | `browser_id` and `page_url` only; no field selectors | 500000           |
| any other checkout                                              | a one-time virtual card that KERNEL writes into the page's card fields       | `browser_id`, `page_url`, and `fields` selectors     | 50000            |

the virtual-card path doesn't require a native processor adapter or a stripe merchant, but it needs uniquely selectable card inputs on the checkout page. don't call the page's WebMCP tools yourself or pass a merchant account id. if the checkout doesn't support link pay tokens and `amount` exceeds 50000, creation returns `400`.

### Change or retry a request

card items are immutable. repeating the same `upsert` with an identical `spec` returns the existing item without inspecting the checkout again or repeating approval. a different `spec` at the same key returns `409`; `PATCH` isn't supported for cards.

to change the purchase, browser, or checkout page, create a new card item under a new key. delete the old item when you no longer need it; deletion can be blocked while a provider outcome is unresolved. never create a replacement card to retry a payment whose outcome is uncertain.

### Handle creation errors

checkout inspection runs before KERNEL creates the item or contacts link. these errors mean no card was created and no approval started, so you can correct the cause and create the card again:

| status | code                        | cause                                                                                                                         |
| ------ | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `ambiguous_page`, `timeout` | the page exposes conflicting WebMCP tools, or inspection didn't finish in time                                                |
| `403`  | `destination_denied`        | `page_url` isn't a valid https checkout url                                                                                   |
| `404`  | `browser_not_found`         | the browser session doesn't exist                                                                                             |
| `409`  | `browser_unavailable`       | the browser was deleted, belongs to another project, doesn't have the vault attached, or is busy with another vault operation |
| `429`  | —                           | rate limited; back off before creating the card again                                                                         |
| `500`  | `browser_error`             | checkout inspection failed in the browser                                                                                     |

## Fill the checkout

after the user completes approval, retrieve the card with `wait: 60`. continue only when it's `ready` and advertises `fill`. read that operation's `description`: it names the inputs this card needs. the examples below show the fill request; for the full controller and agent handoff, follow the [browser agent payments guide](/browsers/enable-payments-in-browser-agent#4-prepare-payment-input-and-submit-once).

`browser_id` and `page_url` must exactly match the card's `spec.browser_id` and `spec.page_url`, and that page must still be open. any other browser or page returns `403 destination_denied`. the card must remain ready, unexpired, and undeleted, with an existing parent wallet. `timeout_ms` is optional.

`fill` never submits payment or clicks buttons. after a `completed` fill, the agent clicks the checkout's pay button once.

### Fill a link pay token checkout

when the advertised `fill` description says to omit fields, send only `browser_id` and `page_url`. KERNEL supplies the approved link pay token to the checkout through its verified WebMCP tool, and the result's `fields` array is empty.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await kernel.vaults.items.performOperation(card.key, {
    id_or_name: vault.id,
    type: "fill",
    browser_id: browser.session_id,
    page_url: checkoutURL,
  });
  ```

  ```python Python theme={null}
  result = kernel.vaults.items.perform_operation(
      card.key,
      id_or_name=vault.id,
      type="fill",
      browser_id=browser.session_id,
      page_url=checkout_url,
  )
  ```

  ```bash CLI theme={null}
  jq -n --arg browser "$BROWSER_ID" --arg url "$PAGE_URL" \
    '{browser_id: $browser, page_url: $url}' |
    kernel vaults items invoke user-12345 notebook-order fill --spec-file - -o json
  ```
</CodeGroup>

### Map card fields to inputs

when the advertised `fill` description asks for field/selector bindings, KERNEL writes the one-time virtual card into the page's card inputs. the examples below continue with a ready `notebook-order` card, the same project-scoped `kernel` client with retries disabled, and the `browser` and `checkoutURL` / `checkout_url` the card is bound to. verify the purchase and destination before calling fill; neither a model-proposed url nor a selector authorizes disclosure.

| card field                                                                                                                  | value and format                                           |
| --------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------- |
| `number`, `cvc`                                                                                                             | actual card values; leading zeros preserved                |
| `exp_month`, `exp_year`                                                                                                     | two-digit month (`MM`), four-digit year (`YYYY`)           |
| `expiration`                                                                                                                | combined expiry; requires `format: "MM/YY"` or `"MM/YYYY"` |
| `billing_name`, `billing_line1`, `billing_line2`, `billing_city`, `billing_state`, `billing_postal_code`, `billing_country` | stored provider billing values, without reformatting       |

`format` is forbidden for every field except a card's combined `expiration`. request only needed billing fields. an absent or empty requested billing value returns `400 field_unavailable` before any browser writes; it doesn't make other card fields unavailable. don't invent missing billing data.

these selectors illustrate a checkout you control with separate number, combined expiry, and cvc inputs. replace them with selectors verified on your page, including within payment frames. supply 1–32 ordered bindings, never raw values or frame ids.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const card = await kernel.vaults.items.retrieve("notebook-order", {
    id_or_name: vault.id,
  });
  if (
    card.type !== "card" ||
    card.spec.provider !== "link" ||
    !card.available_operations.some((operation) => operation.type === "fill")
  ) {
    throw new Error("fill is unavailable for this card");
  }
  const result = await kernel.vaults.items.performOperation(card.key, {
    id_or_name: vault.id,
    type: "fill",
    browser_id: browser.session_id,
    page_url: checkoutURL,
    fields: [
      { field: "number", selector: "#card-number" },
      { field: "expiration", selector: "#expiry", format: "MM/YY" },
      { field: "cvc", selector: "#security-code" },
    ],
    timeout_ms: 10000,
  });
  if (result.type !== "fill") throw new Error("unexpected operation response");
  console.log(result.status, result.fields);
  if (result.status !== "completed") {
    throw new Error("stop and reconcile per-field outcomes; do not retry");
  }
  ```

  ```python Python theme={null}
  card = kernel.vaults.items.retrieve("notebook-order", id_or_name=vault.id)
  if (
      card.type != "card"
      or card.spec.provider != "link"
      or not any(operation.type == "fill" for operation in card.available_operations)
  ):
      raise RuntimeError("fill is unavailable for this card")
  result = kernel.vaults.items.perform_operation(
      card.key,
      id_or_name=vault.id,
      type="fill",
      browser_id=browser.session_id,
      page_url=checkout_url,
      fields=[
          {"field": "number", "selector": "#card-number"},
          {"field": "expiration", "selector": "#expiry", "format": "MM/YY"},
          {"field": "cvc", "selector": "#security-code"},
      ],
      timeout_ms=10000,
  )
  if result.type != "fill":
      raise RuntimeError("unexpected operation response")
  print(result.status, result.fields)
  if result.status != "completed":
      raise RuntimeError("stop and reconcile per-field outcomes; do not retry")
  ```

  ```bash CLI theme={null}
  # BROWSER_ID and PAGE_URL are the card's spec.browser_id and spec.page_url.
  # Inspect the item and require the advertised fill operation before invoking it.
  kernel vaults items get user-12345 notebook-order -o json
  jq -n --arg browser "$BROWSER_ID" --arg url "$PAGE_URL" '{
    browser_id: $browser,
    page_url: $url,
    fields: [
      {field: "number", selector: "#card-number"},
      {field: "expiration", selector: "#expiry", format: "MM/YY"},
      {field: "cvc", selector: "#security-code"}
    ],
    timeout_ms: 10000
  }' | kernel vaults items invoke user-12345 notebook-order fill --spec-file - -o json
  ```
</CodeGroup>

if a completed fill reveals another card field, inspect the page and send a separate fill containing only the newly visible field. don't resubmit fields that were already filled.

### Handle the outcome

the cli exits nonzero for `failed` or `unknown`, but retains the value-free result on stdout with `-o json`. preserve that output and its per-field statuses; don't discard it or retry just because the exit code is nonzero. a transport error can leave the outcome uncertain even without a result body.

see the [shared fill outcome contract](/vaults/fill#handle-the-outcome) for per-field statuses and reconciliation.

`fill` returns a value-free outcome. `completed` means KERNEL supplied the payment credential to the page, not that payment succeeded. a `failed` result can leave earlier writes in place; `unknown` or a lost response requires reconciliation. don't automatically retry fill or fall back to aliases. decide whether to submit separately, and never retry checkout automatically.

validation errors before any write return `400`, `403`, `404`, or `409` with an error code such as `destination_denied`, `target_changed`, `ambiguous_page`, `browser_unavailable`, `field_unavailable`, `timeout`, or `execution_failed`. inspect and correct the cause before deciding on a new fill.

one-time describes the provider-issued credential, not item consumption. `fill` doesn't consume the item or clear its encrypted material on the first write. expiry, deletion, and item lifecycle restrictions still apply; a ready item isn't evidence that a purchase can safely be repeated.

don't create a replacement item to retry an unknown purchase. inspect outcomes, item events, and the merchant's order state first.

## Item reference

wallet and card items accept these `spec` fields. fields not listed here are rejected. a card's `spec.wallet` must reference a link wallet in the same vault.

| item   | required fields                                                                                                       | optional fields                                                                   |
| ------ | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| wallet | `provider: 'link'`, `authorization.method: 'oauth'`, `authorization.client`                                           | write-only `authorization.tokens` is required only with a customer-managed client |
| card   | `provider`, `wallet`, `browser_id`, `page_url`, `payment_method_id`, `amount`, `currency`, `merchant_name`, `context` | `line_items`, `totals`, `metadata`, `expires_at`                                  |

wallet items also return a read-only `description` with server-generated guidance for agents.

the default link client is `{type: 'kernel_managed'}`. for your own client, set
`authorization.client` to `{type: 'customer_managed', provider_config: {name: 'checkout-link'}}`
and supply `authorization.tokens` with `access_token` and `refresh_token`.

`browser_id` is a running browser session id with the vault attached, not a reusable browser name. `page_url` is the exact final checkout url and must use https without embedded credentials. `amount` uses minor currency units: 1–500000 when the checkout supports link pay tokens and 1–50000 otherwise. link requires a three-letter `currency`, limits `merchant_name` to 255 characters, and requires at least 100 characters in `context`. its optional `expires_at` is a unix timestamp in seconds.

link `line_items` support `name`, `quantity`, `unit_amount`, `description`, `sku`, `url`, `image_url`, `product_url`, and `totals`. each `totals` entry supports `type`, `display_text`, and `amount`. link `metadata` accepts string values.

card items are immutable. create a new item to change any `spec` field.

### Item states

* **wallet:** `pending_authorization`, `connected`, `declined`, `reconnect_required`, `degraded`
* **card:** `requested`, `pending_authorization`, `ready`, `consumed`, `expired`, `declined`, `recovery_required`

card state can include `masks.brand` and `masks.last4`. retrieve the card's advertised operations before using it. for an unresolved provider outcome, follow [payment recovery](/integrations/wallets/overview#payment-actions-and-recovery); a ready item alone isn't evidence that a purchase can safely be repeated.

deleting a card clears its stored provider value. deleting a wallet also invalidates its dependent cards.
