Docs
Firmly Agentic Commerce
Set theme to dark (⇧+D)

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 assertion
const 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 cart
await 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 payment
const 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 ID
statusUrl: order.urls?.thank_you_page, // merchant's confirmation page
merchantRef: order.platform_order_number, // merchant's order number
total: order.total
};
}

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.