From 1a43dab04bf71837f3dec2d31fa633e81f937555 Mon Sep 17 00:00:00 2001 From: Behroz Saadat Date: Fri, 30 Oct 2026 13:20:41 -0400 Subject: [PATCH] Document the Shop Pay wallet Adds a Shop Pay wallet page covering connection, payment method selection, checkout authorization, fill, and reporting the outcome, and links it from the wallet overview, payments overview, nav, and the browser agent walkthrough. Co-Authored-By: Claude Opus 5.5 (1M context) --- browsers/enable-payments-in-browser-agent.mdx | 18 +- browsers/payments.mdx | 4 +- docs.json | 1 + integrations/wallets/overview.mdx | 9 +- integrations/wallets/shop-pay.mdx | 555 ++++++++++++++++++ 5 files changed, 580 insertions(+), 7 deletions(-) create mode 100644 integrations/wallets/shop-pay.mdx diff --git a/browsers/enable-payments-in-browser-agent.mdx b/browsers/enable-payments-in-browser-agent.mdx index db618228..e6e86155 100644 --- a/browsers/enable-payments-in-browser-agent.mdx +++ b/browsers/enable-payments-in-browser-agent.mdx @@ -10,7 +10,7 @@ verify and approve each purchase before completing checkout. ## Before you start - have an existing browser agent. this guide adds payment handling, not navigation or reasoning. -- choose a wallet integration from the [wallet overview](/integrations/wallets/overview), then follow its tab below. +- choose a wallet integration from the [wallet overview](/integrations/wallets/overview), then follow its tab below. for shop pay, follow the [shop pay guide](/integrations/wallets/shop-pay), which uses the same controller pattern. - install a KERNEL sdk with the `vaults` resource. set `KERNEL_API_KEY` and `KERNEL_PROJECT_ID` in your controller. - use a low-value checkout you control. link is live-only and requires an https merchant origin. on stripe checkouts that support link pay tokens, KERNEL fills the payment without field selectors; elsewhere you need verified field selectors for the virtual card. agentcard requires a [native processor adapter](/integrations/wallets/overview#checkout-and-processor-coverage); the processor doesn't need to be stripe. - for agentcard, set `AGENTCARD_MODE` in your controller and verify it against the credential's mode. customer-owned configs expose `test_mode` (`true` means sandbox); for KERNEL-managed credentials, confirm the deployment's mode. stop if the mode is unknown or mismatched. @@ -165,6 +165,13 @@ kernel browsers create --vault user-12345 -o json + + the examples create a headful browser. to run checkout headless, create the + browser with at least `2GiB` of memory (`memory: "2GiB"`, or `--memory 2GiB` + in the cli). heavy checkout pages can exhaust a `1GiB` headless browser, which + then stops responding to KERNEL during authorization and fill. + + `browser_live_view_url` lets the user watch the checkout during confirmation and agentcard approval pauses. `presentLiveView` represents your authenticated user-facing page: keep the url server-side with the user and browser session @@ -668,6 +675,15 @@ native dom `click()`. read that same input's `checked` property and require it t be `true` before submission. if you can't verify the checked state, stop without submitting. + + + +stripe checkouts can pre-check "save my information with link for faster +checkout". while it's checked, stripe also requires a phone number and won't +submit the payment without one. unless the user asked to save their details to link, uncheck it before submitting and read +the input's `checked` property to confirm. these are stripe page details rather +than a KERNEL contract. + diff --git a/browsers/payments.mdx b/browsers/payments.mdx index d04c7044..6cecd138 100644 --- a/browsers/payments.mdx +++ b/browsers/payments.mdx @@ -23,9 +23,9 @@ agentcard uses an integration-specific alternative: the agent enters non-sensiti 1. create a vault for the user or task. 2. create a wallet item and send the user through the provider-hosted collection flow. 3. attach the vault when you create the browser session. the attachment also covers items created later in that vault. -4. create a card item for the verified, user-confirmed purchase. complete the provider's required preparation and approval. link authorizes at the checkout page and issues a merchant-bound link pay token on supported stripe checkouts or a single-use virtual card elsewhere; agentcard can reuse a card item with separate approval for each checkout. +4. create a card item for the verified, user-confirmed purchase. complete the provider's required preparation and approval. link authorizes at the checkout page and issues a merchant-bound link pay token on supported stripe checkouts or a single-use virtual card elsewhere; shop pay authorizes at the checkout page and issues a single-use card once the shopper approves in shop; agentcard can reuse a card item with separate approval for each checkout. 5. invoke KERNEL's advertised `fill` operation to inject payment fields without supplying their values, then inspect the outcomes before submitting. for agentcard's alias-based flow, enter aliases and keep a trusted approval observer running while the checkout request is held. -6. submit checkout once, complete any remaining provider-hosted approval, and reconcile with the merchant's order state. fill or approval alone is not payment success. +6. submit checkout once, complete any remaining provider-hosted approval, and reconcile with the merchant's order state. for shop pay, report the outcome with `confirm_transaction`. fill or approval alone is not payment success. `wallet` and `card` are vault item types. the wallet supplies the provider connection; the card represents payment material and its authorization state. retrieve the item and check `available_operations` before invoking an operation. don't assume every item supports `fill`. diff --git a/docs.json b/docs.json index 3c43796a..e77e3986 100644 --- a/docs.json +++ b/docs.json @@ -216,6 +216,7 @@ "integrations/wallets/overview", "integrations/wallets/kernel", "integrations/wallets/stripe-link", + "integrations/wallets/shop-pay", "integrations/wallets/agentcard", "integrations/wallets/embedded-card-capture", "integrations/wallets/embedded-card-cvc" diff --git a/integrations/wallets/overview.mdx b/integrations/wallets/overview.mdx index 25873a85..e8ef525e 100644 --- a/integrations/wallets/overview.mdx +++ b/integrations/wallets/overview.mdx @@ -19,13 +19,14 @@ see [how payments work on KERNEL](/browsers/payments#how-payments-work) for the | --- | --- | --- | | [KERNEL card](/integrations/wallets/kernel) | collect a user's card directly through hosted or embedded enrollment | live only | | [link by stripe](/integrations/wallets/stripe-link) | use payment methods from the user's link wallet; requires a us phone number | live only | +| [shop pay](/integrations/wallets/shop-pay) | use cards saved to the user's shop account, approved in shop for each purchase; usd only | live only | | [agentcard](/integrations/wallets/agentcard) | use agentcard's card vault and provider-executed checkout | sandbox or live, set by the agentcard credential | approval, eligibility, and reuse depend on the integration and card. follow the provider guide before choosing a checkout flow. ## Checkout and processor coverage -KERNEL's `fill` api does not require a native processor adapter. for link virtual cards, the page must be on the authorized origin and provide supported checkout inputs. on stripe checkouts that expose link's agent payment tools, KERNEL delivers a link pay token instead, with no field selectors. see [link card requirements](/integrations/wallets/stripe-link#fill-the-checkout). +KERNEL's `fill` api does not require a native processor adapter. for link virtual cards, the page must be on the authorized origin and provide supported checkout inputs. on stripe checkouts that expose link's agent payment tools, KERNEL delivers a link pay token instead, with no field selectors. see [link card requirements](/integrations/wallets/stripe-link#fill-the-checkout). shop pay cards follow the same origin rule; see [shop pay fill](/integrations/wallets/shop-pay#fill-the-checkout). agentcard's alias-based checkout requires a recognized processor request. see [agentcard's processor coverage](/integrations/wallets/agentcard#checkout-and-processor-coverage) for supported formats and limitations. neither field filling nor provider handoff guarantees processor acceptance or payment success. @@ -37,7 +38,7 @@ see [why use KERNEL payments](/browsers/payments#why-use-kernel-payments) for sa use KERNEL-managed credentials by default. if you need your own client, follow the optional setup in [link](/integrations/wallets/stripe-link#bring-your-own-link-oauth-client) -or [agentcard](/integrations/wallets/agentcard#bring-your-own-agentcard-oauth-client). a named +or [agentcard](/integrations/wallets/agentcard#bring-your-own-agentcard-oauth-client). shop pay connects through KERNEL's shop app only and has no provider configuration. a named provider configuration stores your application's `client_id` and `client_secret`, not an end user's wallet grant. credentials are encrypted at rest; secrets are never returned. @@ -68,11 +69,11 @@ has a wallet in any state, and reuse or recover the existing item. a card's `spec.wallet` must reference a wallet in the same vault and from the same provider. -for provider-specific behavior, see [KERNEL card](/integrations/wallets/kernel), [link](/integrations/wallets/stripe-link#item-reference), or [agentcard](/integrations/wallets/agentcard#item-reference). +for provider-specific behavior, see [KERNEL card](/integrations/wallets/kernel), [link](/integrations/wallets/stripe-link#item-reference), [shop pay](/integrations/wallets/shop-pay#item-reference), or [agentcard](/integrations/wallets/agentcard#item-reference). ## Payment actions and recovery -`action.name` can be `link_oauth`, `spend_approval`, `push_approval`, `collect`, `mfa`, `embedded_ceremony`, or `card_enrollment`. actions that require a hosted interaction include a `url`. don't send action urls or provider authorization material to the agent. +`action.name` can be `link_oauth`, `shop_pay_oauth`, `spend_approval`, `push_approval`, `collect`, `mfa`, `embedded_ceremony`, or `card_enrollment`. actions that require a hosted interaction include a `url`. don't send action urls or provider authorization material to the agent. `recovery_required` means a card's provider outcome is unresolved. it stops item wait loops and blocks new authorization, checkout, and deletion of the diff --git a/integrations/wallets/shop-pay.mdx b/integrations/wallets/shop-pay.mdx new file mode 100644 index 00000000..8ecc12cc --- /dev/null +++ b/integrations/wallets/shop-pay.mdx @@ -0,0 +1,555 @@ +--- +title: "Shop Pay" +description: "use shop pay to approve a single-use card for a browser checkout" +--- + +[shop pay](https://shop.app) connects a shopper's shop account through oauth and issues a single-use card for each purchase the shopper approves in shop. KERNEL's native integration handles the connection, token refresh, approval requests, and card storage through vault items. oauth credentials and card material use kms-backed envelope encryption. + +use KERNEL's [`fill` api](/vaults/fill) to inject the approved card into an attached browser without passing its values through your application's injection code or model context. you provide references and selectors; KERNEL returns outcomes, not the values. + +shop pay is the wallet provider, not the merchant's payment processor. the card works on any checkout with supported, uniquely selectable card inputs, so the merchant doesn't need to be on shopify. use shop pay on card-form checkouts. prefer a site's own agent checkout when one is available, and don't fall back to shop pay after another payment method fails. for the overall offering, see [payments on KERNEL](/browsers/payments). + +## Before you start + + + shop pay is live only: every approved card is a real card funded by the shopper's saved payment method, and purchases must be in usd. + + +the shopper needs a shop account with a saved card. shop pay connects through KERNEL's shop app only, so there's no provider configuration to create. + +create a project-scoped client and vault. the examples below use these `kernel` and `vault` variables. + + + +```typescript TypeScript +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 +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 +kernel vaults create --name user-12345 +``` + + + +## Lifecycle + +1. create a shop pay wallet and present the returned `shop_pay_oauth` action. the shopper signs in to shop and approves the connection. +2. require the wallet to be `connected`. +3. optionally request the `payment_methods` expansion and let the shopper choose a saved card. without a choice, shop uses the shopper's preferred card. +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 shop. +5. retrieve the card, verify that it advertises `authorize`, and once the shopper has confirmed the purchase, invoke it with the browser's `browser_id` and the checkout's exact current top-level `page_url`. KERNEL confirms the checkout is open, sends that page to shop as the merchant, and asks the shopper to approve the purchase in shop. +6. complete the returned action: share a `spend_approval` url unchanged, or for `push_approval` tell the shopper to approve in the shop app. then wait for `state.status` to become `ready`. +7. fill the checkout within an hour of the card becoming ready, submit it yourself once, then report the merchant's outcome with `confirm_transaction`. reporting ends the card. + +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 + +before showing a shop pay connection option, list the vault's items. if a shop pay wallet already exists in any state, reuse it and 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 shop pay wallet per vault. + +the examples use `presentProviderAction`, an application-owned function that publishes the action to an authenticated session for the shopper who owns the vault. bind the action to that shopper, vault, and item; apply a short application ttl capped by `wallet.expires_at`; and stop serving it when the action changes or disappears. the sign-in link expires after 10 minutes, and reading the wallet after that returns a new one. derive `authenticatedUser` from the server-side session, not a request field. do not log the url or put it in model context. + + + +```typescript TypeScript +const items = await kernel.vaults.items.list(vault.id); +const shopPayWallets = items.filter( + (item) => item.type === "wallet" && item.spec.provider === "shop_pay", +); +if (shopPayWallets.length > 1) { + throw new Error("vault has more than one shop pay wallet"); +} + +let wallet = shopPayWallets[0]; +if (!wallet) { + wallet = await kernel.vaults.items.upsert("shop-pay-wallet", { + id_or_name: vault.id, + type: "wallet", + spec: { + provider: "shop_pay", + authorization: { + method: "oauth", + client: { type: "kernel_managed" }, + }, + }, + }); +} + +if (wallet.action?.name === "shop_pay_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 +items = kernel.vaults.items.list(vault.id) +shop_pay_wallets = [ + item + for item in items + if item.type == "wallet" and item.spec.provider == "shop_pay" +] +if len(shop_pay_wallets) > 1: + raise RuntimeError("vault has more than one shop pay wallet") + +wallet = shop_pay_wallets[0] if shop_pay_wallets else None +if wallet is None: + wallet = kernel.vaults.items.upsert( + "shop-pay-wallet", + id_or_name=vault.id, + type="wallet", + spec={ + "provider": "shop_pay", + "authorization": { + "method": "oauth", + "client": {"type": "kernel_managed"}, + }, + }, + ) + +if wallet.action is not None and wallet.action.name == "shop_pay_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 +# create only when the list has no shop pay wallet +kernel vaults items list user-12345 -o json +kernel vaults wallets create user-12345 shop-pay-wallet \ + --provider shop_pay \ + --spec '{"authorization":{"method":"oauth","client":{"type":"kernel_managed"}}}' \ + --open +kernel vaults items get user-12345 shop-pay-wallet --wait 60 +``` + + + + + open connection 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. + + +a `declined` wallet, from a shopper who denied the connection, can't be reconnected; delete it and create a new one. + +shop's refresh tokens last 30 days from the connection, so a shopper reconnects about once a month. when shop stops accepting the grant, the wallet becomes `reconnect_required` and returns a new `shop_pay_oauth` action; present it the same way. shop may skip the consent screen if the shopper already approved KERNEL. + +deleting the wallet deletes its cards, cancels their pending requests, and revokes KERNEL's access to the shop account. to remove KERNEL from shop's connected apps, the shopper removes it in shop. + +## Select a payment method + +request `payment_methods` only when `available_expansions` advertises it. the expansion comes from shop at request time and is not persisted in the vault item. each method has an `id`, a `display` with `brand`, `label` (such as `Visa •••• 4242`), and `last4`, and `is_default` for the shopper's preferred card. + + + +```typescript TypeScript +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"], +}); +if (wallet.type !== "wallet") { + throw new Error("unexpected item type"); +} + +const methods = wallet.expanded?.payment_methods ?? []; +for (const method of methods) { + console.log(method.id, method.display, method.is_default); +} +``` + +```python Python +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.is_default) +``` + +```bash CLI +kernel vaults wallets payment-methods user-12345 shop-pay-wallet -o json +``` + + + +show the returned methods in a trusted user-facing surface and pass the chosen `id` as the card's `payment_method_id`, or omit it to use the shopper's preferred card. + +## Create and authorize a card item + +create the card once the browser is on the final checkout page and you know the final amount 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. + + + run checkout in a headful browser, or a headless one with at least `2GiB` of + memory. heavy checkout pages can exhaust a `1GiB` headless browser, which then + stops responding to KERNEL during authorization and fill. + + + + +```typescript TypeScript +let card = await kernel.vaults.items.upsert("notebook-order", { + id_or_name: vault.id, + type: "card", + spec: { + provider: "shop_pay", + wallet: wallet.key, + 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.", + line_items: [{ name: "notebook", quantity: 1, unit_amount: 2000 }], + totals: [ + { type: "subtotal", amount: 2000 }, + { type: "tax", amount: 166 }, + { type: "fulfillment", amount: 140 }, + { type: "total", amount: 2306 }, + ], + }, +}); + +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) { + await presentProviderAction({ + userID: authenticatedUser.id, + vaultID: vault.id, + item: card, + }); +} +``` + +```python Python +card = kernel.vaults.items.upsert( + "notebook-order", + id_or_name=vault.id, + type="card", + spec={ + "provider": "shop_pay", + "wallet": wallet.key, + "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." + ), + "line_items": [{"name": "notebook", "quantity": 1, "unit_amount": 2000}], + "totals": [ + {"type": "subtotal", "amount": 2000}, + {"type": "tax", "amount": 166}, + {"type": "fulfillment", "amount": 140}, + {"type": "total", "amount": 2306}, + ], + }, +) + +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: + present_provider_action( + user_id=authenticated_user.id, + vault_id=vault.id, + item=card, + ) +``` + +```bash CLI +kernel vaults cards create user-12345 notebook-order \ + --provider shop_pay \ + --spec '{ + "wallet": "shop-pay-wallet", + "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.", + "line_items": [{"name": "notebook", "quantity": 1, "unit_amount": 2000}], + "totals": [ + {"type": "subtotal", "amount": 2000}, + {"type": "tax", "amount": 166}, + {"type": "fulfillment", "amount": 140}, + {"type": "total", "amount": 2306} + ] + }' +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 +``` + + + +`amount` uses minor currency units, so `2306` means 23.06 usd, and is the most the shopper approves. `totals` needs exactly one entry with `type: "total"`, positive and no higher than `amount`; entries such as `subtotal`, `tax`, `fulfillment`, or a negative `discount` describe the purchase. `context` is shown to the shopper when they approve, so write it for them. see [item reference](#item-reference) for every limit. + +until you authorize the card, you can replace its spec with an update. once it is authorized, updates return `409`: delete the card and create a new one with the updated details. + +authorization sends the checkout page to shop as the merchant and limits fill to that page's origin. authorizing without `browser_id` and `page_url` uses `merchant_url` instead; use it only as a fallback when checkout inspection keeps failing. + +`presentProviderAction` shares a `spend_approval` url with the shopper unchanged. a `push_approval` has no url: tell the shopper to approve the purchase in the shop app. the shopper has until the card's `expires_at`, currently 24 hours after authorization, to approve. + +an approval link works once: if the shopper opens it again, shop shows the approval as unavailable. retrieve the card and share the url from its current action instead. + +once approval has been requested, `{"type":"authorize"}` only re-checks the request and never creates another payment request. + +### Authorization errors + +these errors happen before shop receives a payment request, 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. on a `1GiB` headless browser, `browser_error` usually means it ran out of memory; see the tip above | + +if shop refuses the request, authorize returns an error with shop's reason and the card becomes `declined`: read `status_reason`, then delete the card and create a new one. if shop rate limits the request (`429`), the card stays `requested`; wait for `Retry-After`, then authorize again. if the shopper declines or the request expires, delete the card and create a new one. + +## Fill the checkout + +after the shopper approves, retrieve the card with `wait: 60`. continue only when it is `ready` and advertises `fill`; one bounded wait may finish before approval does. a ready card's `expires_at` is its fill deadline, at most an hour later, so fill and submit promptly. after that the card is `expired` and can't be filled, but you can still report its outcome. + +every fill supplies `browser_id`, the exact current top-level `page_url`, and ordered `fields` bindings. `page_url` must use https and must be on the origin of the checkout you authorized at, or on the origin of `spec.merchant_url` for a card authorized without a checkout. the origin includes the host and port and doesn't allow other subdomains, even though the card's `state.domains` lists the registrable domain. a disallowed origin returns `403 destination_denied`. for the shared rules, see [fill browser fields](/vaults/fill). + +| 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_first_name`, `billing_last_name`, `billing_line1`, `billing_line2`, `billing_city`, `billing_state`, `billing_postal_code`, `billing_country` | the billing address shop returns with the card | + +`billing_name` is shop's cardholder name, or the first and last name joined when shop returns only those. `billing_first_name` and `billing_last_name` are available only when shop returns them separately; `billing_name` is never split. request only needed billing fields. an absent or empty requested billing value returns `400 field_unavailable` before any browser writes. + +supply 1 to 32 ordered bindings, never raw values. `format` is allowed only on the combined `expiration`. these selectors illustrate a checkout you control; replace them with selectors verified on your page, including within payment frames. + + + +```typescript TypeScript +card = await kernel.vaults.items.retrieve(card.key, { + id_or_name: vault.id, + wait: 60, +}); +if (!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" }, + { field: "billing_name", selector: "#name-on-card" }, + { field: "billing_postal_code", selector: "#postal-code" }, + ], +}); +if (result.type !== "fill") throw new Error("unexpected operation response"); +if (result.status !== "completed") { + throw new Error("fill did not complete; inspect per-field outcomes and retry"); +} +``` + +```python Python +card = kernel.vaults.items.retrieve(card.key, id_or_name=vault.id, wait=60) +if 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"}, + {"field": "billing_name", "selector": "#name-on-card"}, + {"field": "billing_postal_code", "selector": "#postal-code"}, + ], +) +if result.type != "fill": + raise RuntimeError("unexpected operation response") +if result.status != "completed": + raise RuntimeError("fill did not complete; inspect per-field outcomes and retry") +``` + +```bash CLI +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"}, + {field: "billing_name", selector: "#name-on-card"}, + {field: "billing_postal_code", selector: "#postal-code"} + ] +}' | kernel vaults items invoke user-12345 notebook-order fill --spec-file - -o json +``` + + + +`completed` means the fields were filled, not that payment succeeded. `fill` never submits, so a retry is safe after checking the page, but never in an automatic loop. submit the checkout yourself once. see the [shared fill outcome contract](/vaults/fill#handle-the-outcome) for per-field statuses. + +## Report the outcome + +after submitting the checkout and observing the merchant's result, invoke `confirm_transaction` once when the card advertises it. reporting ends the card: KERNEL tells shop the outcome, deletes the stored card, and moves the item to `consumed` whatever the outcome. another attempt needs a new card and a new approval. + +| field | value | +| --- | --- | +| `status` | `APPROVED`, `DECLINED`, `PENDING`, `ERROR`, or `CANCELLED`, as the merchant reported it | +| `transaction_type` | `PURCHASE`, `AUTHORIZATION`, or `CAPTURE` | +| `amount`, `currency` | the actual amount in minor units, no higher than the approved limit, and the uppercase currency, `USD` | +| `occurred_at` | when you observed the outcome | +| `order_id`, `order_url` | optional, for `APPROVED`: the merchant's order number and https order page | +| `failure_code` | optional, for other outcomes, such as `3ds_challenge`, `captcha`, `login_required`, `timeout`, or `site_error`. `payment_declined` goes only with `DECLINED` | + + + +```typescript TypeScript +card = await kernel.vaults.items.retrieve(card.key, { id_or_name: vault.id }); +if ( + !card.available_operations.some(({ type }) => type === "confirm_transaction") +) { + throw new Error("confirm_transaction is unavailable"); +} +const confirmed = await kernel.vaults.items.performOperation(card.key, { + id_or_name: vault.id, + type: "confirm_transaction", + status: "APPROVED", + transaction_type: "PURCHASE", + amount: 2306, + currency: "USD", + occurred_at: new Date().toISOString(), + order_id: "1001", + order_url: "https://shop.example.com/orders/1001", +}); +``` + +```python Python +from datetime import datetime, timezone + +card = kernel.vaults.items.retrieve(card.key, id_or_name=vault.id) +if not any(operation.type == "confirm_transaction" for operation in card.available_operations): + raise RuntimeError("confirm_transaction is unavailable") +confirmed = kernel.vaults.items.perform_operation( + card.key, + id_or_name=vault.id, + type="confirm_transaction", + status="APPROVED", + transaction_type="PURCHASE", + amount=2306, + currency="USD", + occurred_at=datetime.now(timezone.utc).isoformat(), + order_id="1001", + order_url="https://shop.example.com/orders/1001", +) +``` + +```bash CLI +kernel vaults items invoke user-12345 notebook-order confirm_transaction \ + --params '{ + "status": "APPROVED", + "transaction_type": "PURCHASE", + "amount": 2306, + "currency": "USD", + "occurred_at": "2026-10-30T16:40:00Z", + "order_id": "1001", + "order_url": "https://shop.example.com/orders/1001" + }' -o json +``` + + + +report only what the merchant showed. if the outcome is unclear, ask the shopper to check the merchant's order confirmation or order history, then report what it shows. don't retry checkout. + +## Item reference + +wallet and card items accept these `spec` fields. fields not listed here are rejected. a card's `spec.wallet` must reference a shop pay wallet in the same vault. + +| item | required fields | optional fields | +| --- | --- | --- | +| wallet | `provider: 'shop_pay'`, `authorization: {method: 'oauth', client: {type: 'kernel_managed'}}` | none | +| card | `provider`, `wallet`, `amount`, `currency`, `merchant_name`, `merchant_url`, `context`, `totals` | `payment_method_id`, `merchant_country`, `line_items` | + +`amount` is 1 to 1000000 minor units, `currency` is `usd`, `merchant_name` is at most 255 characters, `merchant_url` is an https url with a registrable domain, and `context` is 100 to 500 characters. `merchant_country` is a two-letter country code. each `line_items` entry has `name`, `quantity` (at least 1), and `unit_amount` (at least 0); each `totals` entry has `type` and `amount`. + +### Item states + +- **wallet:** `pending_authorization`, `connected`, `declined`, `reconnect_required`, `degraded` +- **card:** `requested`, `pending_authorization`, `ready`, `consumed`, `expired`, `declined`, `recovery_required` + +`declined` covers a request the shopper declined, that was cancelled, or that shop refused at authorization. card state can include `masks.brand` and `masks.last4` once shop issues the card. + +`recovery_required` means a provider operation has an unresolved outcome and the card can't be used. when `status_reason` says shop refused retrieval before acceptance, no card was issued and a later read retries. otherwise follow `status_reason`: recovery for issuance can be abandoned by deleting the card and creating a new one, which the shopper approves separately. recovery while reporting a merchant transaction can't be deleted; [contact support](/info/support) with the vault, item key, and `status_reason`. + +deleting a card cancels its pending shop request and clears its stored card. it doesn't revoke a card shop already issued.