Wallet / Pass Purchase
When the originating surface is a wallet, payment app, or digital pass, the user has identity and payment context already available on the device. The Firmly flow can lean on that context — making the conversation between the user, the wallet, and Firmly much shorter than a typical agentic flow.
What’s different from a generic agent
A generic AI agent has to collect identity, address, and payment from the user during the conversation. A wallet already has all of these:
| Context | Generic agent | Wallet |
|---|---|---|
| Who the user is | Collected during conversation | Already known (wallet account) |
| Payment instrument | Card collected and tokenized in-flow | Already provisioned in the wallet |
| Shipping address | Asked during checkout | Available from the wallet profile |
| Identity confirmation | Conversational confirm | Biometric / device unlock |
The result: a wallet-initiated purchase can be a two-tap experience — see product, confirm, done.
Typical sequence
User taps a buy action in the wallet
The wallet hosts a product card, pass, or message that includes a “buy” or “purchase” action. The user taps it.
Wallet collects biometric / device confirmation
Face ID, fingerprint, PIN — whatever the wallet uses to confirm the user is present and consents. This is the consent capture moment — record it on the wallet side.
Wallet calls Firmly with pre-filled context
The wallet makes the agentic purchase calls (Catalog → Cart → Shipping → Complete Order), passing identity, address, and payment from the wallet’s own profile. No additional user input.
Order placed
Firmly returns the merchant order number. The wallet shows confirmation in its own UI.
Wallet updates the pass
If the purchase was for a pass-like item (gift card, ticket, voucher), the wallet provisions the new pass on the device. The user sees it immediately.
Identity handoff
If the merchant supports it, the wallet can assert the user’s identity so the order appears under the user’s existing merchant account — not as a guest order. This is what Firmly Connect handles.
The wallet bootstraps a Firmly session and includes a signed identity assertion (typically OIDC or SAML) that maps to the user’s merchant account. The merchant sees a logged-in user place the order, with loyalty and saved addresses flowing through correctly.
If the merchant doesn’t support identity federation, the order is placed as a guest with the wallet’s address and email — still valid, but not tied to the user’s existing merchant account.
Payment handoff
Three patterns, depending on the wallet’s capabilities:
Pattern A — Wallet tokenizes, Firmly forwards
The wallet has a card-on-file. It uses its own tokenization to produce a token; the token is encrypted (JWE) and passed to Firmly’s complete-order as encrypted_card.
This is the simplest pattern and works for any wallet with a stored card. The user never re-enters card details.
Pattern B — Network token from the wallet
For wallets with network token capability (e.g. Click to Pay, Paze), the wallet returns a network token from the card network. The token has a one-time cryptogram tied to the user’s biometric confirmation. Firmly forwards this to the merchant’s PSP.
This pattern has better authorization rates because the cryptogram is treated as strong cardholder authentication.
Pattern C — Wallet is the payment
For wallet-native payment methods (PayPal, Klarna, Affirm), the wallet handles the entire payment flow itself. The Firmly call uses the express-checkout endpoints (e.g. Klarna Express Checkout) instead of encrypted_card.
Shipping for digital vs physical goods
Wallet purchases tend to skew digital — gift cards, vouchers, subscriptions, passes. For these:
- Skip the shipping address entirely (some merchants accept orders with no shipping info for digital SKUs)
- Email is the delivery channel — pre-fill from the wallet profile
- The “fulfillment” is the wallet provisioning the new pass
For physical goods purchased from a wallet, the wallet’s saved shipping address pre-fills cart/shipping-info and the flow is identical to a generic agent’s flow from that point on.
Sample code — minimal wallet flow
The sample below omits response-status checks for brevity. In production, check response.ok on every call and handle failures — reuse the Deep Link error-handling shape rather than assuming each fetch succeeded. For a physical-goods purchase, a shipping method must also be selected before complete-order (read the options from the set-shipping-info response’s shipping_method_options, then call set-shipping-method); the digital-goods path below skips it because there is nothing to ship.
// Wallet has already authenticated the user via biometric.// Wallet has the user's saved address and a tokenized payment method.import { CompactEncrypt, importJWK } from 'jose';import { randomUUID } from 'node:crypto';async function purchaseFromWallet({ appId, domain, productRef, walletContext }) {// 1. Bootstrap Firmly session with the wallet's identity assertionconst session = await fetch(`https://api.firmly.work/api/v1/browser-session`, {method: 'POST',headers: {'x-firmly-app-id': appId,// Optionally: 'x-firmly-identity-assertion': walletContext.signedIdentity}}).then(r => r.json());const auth = {'x-firmly-authorization': session.access_token,'Content-Type': 'application/json'};// 2. Add to cartawait fetch(`https://api.firmly.work/api/v2/domains/${domain}/cart/line-items`, {method: 'POST',headers: auth,body: JSON.stringify({ add_to_cart_ref: productRef, quantity: 1 })});// 3. Physical goods: set shipping address, then pick a method.// Digital goods: skip this whole block — nothing to ship.if (walletContext.shippingAddress) {// set-shipping-info returns the cart with shipping_method_options// populated inline on each shipment.const cart = await fetch(`https://api.firmly.work/api/v2/domains/${domain}/cart/shipping-info`, {method: 'POST', headers: auth,body: JSON.stringify(walletContext.shippingAddress)}).then(r => r.json());// Pick a method per shipment (here: the first available on the first shipment)const shipment = cart.shipments?.[0];await fetch(`https://api.firmly.work/api/v2/domains/${domain}/cart/shipment/methods`, {method: 'POST', headers: auth,body: JSON.stringify({shipment_id: shipment?.shipment_id,shipping_method_id: shipment?.shipping_method_options?.[0]?.id})});}// 4. Encrypt the wallet's tokenized paymentconst jwk = await fetch(`https://cc.firmly.work/api/v1/payment/key?format=JWK`).then(r => r.json());const publicKey = await importJWK(jwk, 'RSA-OAEP-256');const encryptedCard = await new CompactEncrypt(new TextEncoder().encode(JSON.stringify(walletContext.paymentToken))).setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM', kid: jwk.kid }).encrypt(publicKey);// 5. Complete the order. The cart built above is the source of truth for line items// and shipping, so complete-order finalizes that existing cart — the body carries// only encrypted_card + billing_info. (place-order would flush and rebuild the cart// from an items[] array; that's the one-shot path, not this one.)// See the complete-order v2 reference for the authoritative request body.const order = await fetch(`https://cc.firmly.work/api/v2/payment/domains/${domain}/complete-order`, {method: 'POST',headers: { ...auth, 'Idempotency-Key': randomUUID() },body: JSON.stringify({encrypted_card: encryptedCard,billing_info: walletContext.billingAddress || walletContext.shippingAddress})}).then(r => r.json());return {cartId: order.cart_id, // Firmly's internal IDstatusUrl: order.urls?.thank_you_page, // merchant's confirmation pagemerchantRef: order.platform_order_number, // merchant's order numbertotal: order.total};}
Consent in wallet flows
Unlike a generic agent (where consent is a conversational “yes, buy”), wallet consent is the biometric confirmation on the device plus the on-screen total shown before the biometric prompt. Record:
- Wallet user ID
- Timestamp
- The displayed product, total, and merchant name
- The biometric type used (Face ID, Touch ID, PIN — without the biometric data itself)
- The session ID and resulting merchant order number
This is your evidence for any future dispute. See Consent & Disclosure.