> ## Documentation Index
> Fetch the complete documentation index at: https://kernel.sh/docs/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 payment credential for an approved purchase: a merchant-bound link pay token on supported stripe checkouts, or a single-use virtual card everywhere else. KERNEL inspects the checkout and chooses which one; you never choose or handle it. KERNEL's native integration handles wallet connection, token refresh, approval actions, and card storage through vault items. oauth credentials and card material use kms-backed envelope encryption.

use KERNEL's [`fill` api](/docs/vaults/fill) to safely inject the approved card's values into an attached browser without passing them through your application's injection code or model context. you provide references and selectors; KERNEL returns outcomes, not the values. link supplies the card, while `fill` is a KERNEL operation shared with other supported vault items.

link is the wallet provider, not the merchant's payment processor. a virtual card works on any checkout with supported, uniquely selectable card inputs, so the merchant's processor doesn't need to be stripe. a link pay token works only on stripe checkouts that expose link's agent payment tools; KERNEL detects them during authorization. for the overall offering, see [payments on KERNEL](/docs/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](/docs/integrations/wallets/overview#provider-configurations) once for
your organization. configurations are shared across projects, so authenticate
this step with an [organization-scoped api key](/docs/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. reach the final checkout page in a browser with the vault attached, then create a `card` item with the purchase details. creating the card doesn't contact link. to change the details before authorizing, update the card.
5. retrieve the card, verify that it advertises `authorize`, and after explicit user approval invoke it with the browser's `browser_id` and the checkout's exact current top-level `page_url`. KERNEL inspects that checkout, chooses a link pay token or a virtual card, and sends the user an approval request.
6. complete the returned `spend_approval` or `push_approval` action and wait for `state.status` to become `ready`.
7. require the advertised `fill` operation and follow its description: a link pay token takes `browser_id` and `page_url` only, and a virtual card also takes field/selector bindings. inspect the fill outcome, then submit the checkout yourself once.

each item's `available_operations[].description` describes the next step and its recovery, so have your agent read it rather than hard-coding the flow.

## 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.

## Create and authorize a card item

create the card once the browser is on the final checkout page and you know the final amount, currency, and merchant. the examples use `browser`, a KERNEL browser created with this vault attached, and `checkoutURL` / `checkout_url`, the browser's exact current top-level checkout url obtained through your browser-control method.

<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,
      payment_method_id: paymentMethod.id,
      amount: 2306,
      currency: "usd",
      merchant_name: "example shop",
      merchant_url: "https://shop.example.com",
      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.",
    },
  });

  card = await kernel.vaults.items.retrieve(card.key, { id_or_name: vault.id });
  if (!card.available_operations.some(({ type }) => type === "authorize")) {
    throw new Error("authorization is unavailable");
  }

  const authorized = await kernel.vaults.items.performOperation(card.key, {
    id_or_name: vault.id,
    type: "authorize",
    browser_id: browser.session_id,
    page_url: checkoutURL,
  });
  if (authorized.type !== "card") {
    throw new Error("unexpected operation response");
  }
  card = authorized;
  if (card.action && "url" in card.action) {
    await presentProviderAction({
      userID: authenticatedUser.id,
      vaultID: vault.id,
      item: card,
    });
  }
  ```

  ```python Python theme={null}
  card = kernel.vaults.items.upsert(
      "notebook-order",
      id_or_name=vault.id,
      type="card",
      spec={
          "provider": "link",
          "wallet": wallet.key,
          "payment_method_id": payment_method.id,
          "amount": 2306,
          "currency": "usd",
          "merchant_name": "example shop",
          "merchant_url": "https://shop.example.com",
          "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."
          ),
      },
  )

  card = kernel.vaults.items.retrieve(card.key, id_or_name=vault.id)
  if not any(operation.type == "authorize" for operation in card.available_operations):
      raise RuntimeError("authorization is unavailable")

  card = kernel.vaults.items.perform_operation(
      card.key,
      id_or_name=vault.id,
      type="authorize",
      browser_id=browser.session_id,
      page_url=checkout_url,
  )
  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,
      )
  ```

  ```bash CLI theme={null}
  kernel vaults cards create user-12345 notebook-order \
    --provider link \
    --spec '{
      "wallet": "link-wallet",
      "payment_method_id": "pm_123",
      "amount": 2306,
      "currency": "usd",
      "merchant_name": "example shop",
      "merchant_url": "https://shop.example.com",
      "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 -o json
  # BROWSER_ID is the attached session ID; PAGE_URL is its exact current checkout URL.
  kernel vaults items invoke user-12345 notebook-order authorize \
    --browser-id "$BROWSER_ID" --page-url "$PAGE_URL" --open
  kernel vaults items get user-12345 notebook-order --wait 60 -o json
  ```
</CodeGroup>

`amount` uses minor currency units, so `2306` means 23.06 usd. link accepts values from 1 to 500000, but a virtual card allows at most 50000. `context` must contain at least 100 characters. card creation is live-only, and `spec.test` is not supported.

until you authorize the card at a checkout, you can change its spec with an update. after that, updates return `409`: delete the card and create a new one with the updated details.

### Link pay tokens and virtual cards

authorization fixes how link delivers the credential:

| | link pay token | virtual card |
| - | - | - |
| when KERNEL chooses it | the checkout is a stripe checkout that exposes link's agent payment tools | any other checkout |
| amount limit | 500000 minor units | 50000 minor units |
| what `fill` does | passes a short-lived token to the checkout's link tools; the checkout shows link as the payment method | writes the card number, cvc, expiry, and requested billing fields into the inputs you select |
| where `fill` works | pages on the origin of the checkout you authorized at, for the stripe account the user approved | pages on the origin of the checkout you authorized at |

link resolves the merchant shown on the approval screen from the checkout's stripe account, so a link pay token request doesn't send `merchant_name` or `merchant_url`. the token is fetched fresh for each fill and never returned to you.

don't inspect or invoke the checkout's webmcp tools yourself. KERNEL does that during `authorize` and `fill`.

authorizing without `browser_id` and `page_url` still issues a virtual card that fills pages on the `merchant_url` origin. use it only as a fallback when checkout inspection keeps failing.

### Authorization errors

these errors happen before any payment request is created, so the card stays `requested` and you can authorize again:

| code | what to do |
| - | - |
| `page_not_found` | open the checkout in the browser at exactly `page_url`, then authorize again |
| `ambiguous_page` | close extra tabs so exactly one is at `page_url`, then authorize again |
| `browser_not_found`, `browser_unavailable`, `destination_denied`, `timeout`, `browser_error` | make sure the browser exists in this project, is leased, has the vault attached, and isn't busy with another fill, and that the checkout is open in exactly one tab, then authorize again |
| `invalid_request` for the amount | the checkout needs a virtual card and the amount exceeds 50000. delete the card and tell the user this checkout can't be paid with a link virtual card; don't lower the amount of a confirmed purchase |

if the user declines or the request expires, delete the card and create a new one. once approval has been requested, `{"type":"authorize"}` only re-checks the request and never creates another payment request.

## Fill the checkout

after the user completes approval, retrieve the card with `wait: 60`. continue only when it is `ready` and advertises `fill`; one bounded wait may finish before approval does. the examples below show the fill request; for the full controller and agent handoff, follow the [browser agent payments guide](/docs/browsers/enable-payments-in-browser-agent#4-fill-payment-fields).

the advertised `fill` description says which inputs the card needs. every fill supplies `browser_id` and the exact current top-level `page_url`. the browser must have the vault attached in the same project, and the card must remain ready, unexpired, and undeleted with its parent wallet. a link pay token fill also requires that wallet to be connected, because the token is retrieved with it. `timeout_ms` is optional.

`page_url` must use https without embedded credentials and must be on the origin of the checkout page you authorized at, or on the origin of `spec.merchant_url` for a card authorized without a checkout. the origin comparison includes the host and port (default `443` is normalized); it does not allow other subdomains. the path can differ, so a fill still works after the checkout moves to another page on the same origin, but the request must name the exact current page. a disallowed origin returns `403 destination_denied`. descendant payment frames can have different origins; the restriction applies to the top-level merchant page.

### Fill a link pay token

omit `fields`. KERNEL selects card on the checkout if needed, checks that the checkout's stripe account is the one the user approved, retrieves a fresh token, and passes it to the checkout's link tools. on stripe checkouts, the card form is replaced by the user's link payment method.

<CodeGroup>
  ```typescript TypeScript theme={null}
  const result = await kernel.vaults.items.performOperation("notebook-order", {
    id_or_name: vault.id,
    type: "fill",
    browser_id: browser.session_id,
    page_url: checkoutURL,
  });
  if (result.type !== "fill") throw new Error("unexpected operation response");
  if (result.status !== "completed") {
    throw new Error(`fill ${result.status}; check the checkout page, then retry`);
  }
  ```

  ```python Python theme={null}
  result = kernel.vaults.items.perform_operation(
      "notebook-order",
      id_or_name=vault.id,
      type="fill",
      browser_id=browser.session_id,
      page_url=checkout_url,
  )
  if result.type != "fill":
      raise RuntimeError("unexpected operation response")
  if result.status != "completed":
      raise RuntimeError(f"fill {result.status}; check the checkout page, then retry")
  ```

  ```bash CLI theme={null}
  kernel vaults items invoke user-12345 notebook-order fill \
    --params "{\"browser_id\":\"$BROWSER_ID\",\"page_url\":\"$PAGE_URL\"}" -o json
  ```
</CodeGroup>

a different stripe account on the page returns `403 destination_denied`, and nothing is passed to the page. KERNEL checks the account before retrieving the token and again before passing it.

### Fill a virtual card

supply ordered `fields` bindings. combined `expiration` requires `format: "MM/YY"` or `"MM/YYYY"`.

#### Map card fields to inputs

the examples below continue after wallet connection and spend approval with a ready `notebook-order` card, the same project-scoped `kernel` client with retries disabled, and a `browser` created with its vault attached. `checkoutURL` / `checkout_url` is the browser's exact current top-level url, obtained through your browser-control method; no playwright page object is required. 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("fill did not complete; inspect per-field outcomes and 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("fill did not complete; inspect per-field outcomes and retry")
  ```

  ```bash CLI theme={null}
  # BROWSER_ID is the attached session ID; PAGE_URL is its exact current checkout 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>

### Handle the outcome

the cli exits nonzero for `failed` or `unknown`, but retains the value-free result on stdout with `-o json`. use the per-field statuses to decide what to fix before retrying. a transport error can leave the outcome uncertain even without a result body.

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

`fill` returns value-free per-field outcomes. `completed` means the fields were filled, not that payment succeeded. a `failed` result can leave earlier writes in place, and `unknown` or a lost response means the outcome is uncertain. `fill` never submits, so a retry is safe. retry only after checking the page, and never in an automatic loop. don't fall back to aliases. decide whether to submit separately, and never retry checkout automatically.

single-use describes the provider-issued card, not one-use alias consumption. `fill` does not consume the item or clear its encrypted material on the first field write. expiry, deletion, and item lifecycle restrictions still apply; a ready item is not evidence that a purchase can safely be repeated.

don't repeat `authorize` or 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`, `payment_method_id`, `amount`, `currency`, `merchant_name`, `merchant_url`, `context` | `line_items`, `totals`, `metadata` |

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`.

`amount` uses minor currency units. link accepts 1–500000, and a virtual card allows at most 50000. link requires a three-letter `currency`, limits `merchant_name` to 255 characters, requires an absolute http or https `merchant_url`, and requires at least 100 characters in `context`. the card item's `expires_at` comes from link: the spend request's expiry, or the issued virtual card's validity. when link reports no expiry for a link pay token request, `fill` stays available for 30 minutes after authorization, including the time the user takes to approve.

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 updates replace the complete `spec`; they are not partial merges. a `requested` link card accepts any valid spec, and a pending virtual-card request can update the fields link supports on its existing request. once a card is authorized with `browser_id` and `page_url`, updates return `409`: delete it and create a new card.

### 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](/docs/integrations/wallets/overview#payment-actions-and-recovery); a ready item alone is not evidence that a purchase can safely be repeated.

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


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.