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

Agentic Pay Integration

This guide walks you through integrating Agentic Pay — Firmly’s Network Token Router for agentic commerce. Firmly handles the network-specific mechanics behind one abstract API.

​​ Overview

There are two entry points — both converge on the same intent → order chain:

​​ New card: Enroll

Send the cardholder’s PAN to Firmly. Firmly detects the network, enrolls the card, and returns verification challenges. Persist virtual_card_id, card_art, and masked for returning buyers.

​​ Saved card: Select Card

For returning buyers, call /select-card with the stored virtual_card_id — no PAN re-entry. Returns the same shape as /enroll, so the rest of the flow is identical.

​​ Create a payment intent

Create an intent with the transaction amount. A FIDO assertion proves cardholder presence.

​​ Place the order

Send the flow_token and intent_id to the wallet-complete-order endpoint. Firmly retrieves network token credentials and completes the payment.

​​ Step 1: Enroll a Card (New Card Only)

Send the cardholder’s PAN to Firmly. Firmly inspects the BIN and routes to the correct network automatically. For returning buyers with a stored card, skip to Select Card instead.


const enrollResponse = await fetch(
'https://api.firmly.work/api/v1/wallets/agentic-pay/enroll',
{
method: 'POST',
headers: {
'x-firmly-authorization': apiToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
pan: '4111111111111111',
expiry_month: '12',
expiry_year: '2030',
cvv: '123',
consumer: {
email: 'cardholder@example.com',
country_code: 'US'
}
})
}
);
const { flow_token, virtual_card_id, verification_methods } = await enrollResponse.json();

import requests
response = requests.post(
'https://api.firmly.work/api/v1/wallets/agentic-pay/enroll',
headers={
'x-firmly-authorization': api_token,
'Content-Type': 'application/json'
},
json={
'pan': '4111111111111111',
'expiry_month': '12',
'expiry_year': '2030',
'cvv': '123',
'consumer': {
'email': 'cardholder@example.com',
'country_code': 'US'
}
}
)
data = response.json()
flow_token = data['flow_token']
verification_methods = data['verification_methods']

curl -X POST https://api.firmly.work/api/v1/wallets/agentic-pay/enroll \
-H "x-firmly-authorization: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"pan": "4111111111111111",
"expiry_month": "12",
"expiry_year": "2030",
"cvv": "123",
"consumer": {
"email": "cardholder@example.com",
"country_code": "US"
}
}'

The response includes verification_methods, card_art, and masked (last4, expiry, brand).

Check verification_methods for embed_iframe_handshake or embed_iframe to determine the next step.

​​ Step 2: Iframe Handshake (If Required)

If the response includes a verification method of type embed_iframe_handshake, load the iframe URI in the cardholder’s browser and capture the secure_token via postMessage. Skip this step if no handshake method is returned.


const iframe = document.createElement('iframe');
iframe.src = verification_methods[0].attributes.uri;
document.body.appendChild(iframe);
window.addEventListener('message', (event) => {
// Always verify the sender's origin
if (event.origin !== new URL(iframe.src).origin) return;
const data = event.data;
if (data.type === 'AUTH_READY') {
iframe.contentWindow.postMessage({
requestID: data.requestID,
type: 'CREATE_AUTH_SESSION',
version: '1',
client: { id: '__from-server__' },
contentType: 'application/json'
}, new URL(iframe.src).origin);
}
if (data.sessionContext?.secureToken) {
const secureToken = data.sessionContext.secureToken;
sendToServer({ secure_token: secureToken });
}
});

​​ Step 3: Trigger Enrollment Verification

Call the trigger endpoint with the selected verification method. Include secure_token if captured from an iframe handshake, or callback_uri for iframe-based challenges without a handshake.


const triggerResponse = await fetch(
'https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/trigger',
{
method: 'POST',
headers: {
'x-firmly-authorization': apiToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
flow_token: flowToken,
method_id: 'OTP_SMS',
secure_token: secureToken // omit if not applicable
})
}
);
const { flow_token: updatedToken, challenge } = await triggerResponse.json();
// challenge.type === 'otp' means an OTP was sent to the cardholder

curl -X POST https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/trigger \
-H "x-firmly-authorization: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"flow_token": "eyJhbGci...",
"method_id": "OTP_SMS",
"secure_token": "ezAwMX06..."
}'

​​ Step 4: Verify Enrollment

Submit the OTP or iframe result. This step may need to be called more than once — for example, an OTP verification may be followed by a FIDO registration step.


const verifyResponse = await fetch(
'https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/verify',
{
method: 'POST',
headers: {
'x-firmly-authorization': apiToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
flow_token: updatedToken,
type: 'otp',
value: '456789'
})
}
);
const verifyResult = await verifyResponse.json();
// If verifyResult.verification_methods exists, another verification step is needed
// If verifyResult.virtual_card_id exists, enrollment is complete

curl -X POST https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/verify \
-H "x-firmly-authorization: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"flow_token": "eyJhbGci...",
"type": "otp",
"value": "456789"
}'

​​ Step 5: Complete FIDO Registration

Load the FIDO iframe from the verification method’s uri, complete the ceremony, and submit the result.


const fidoVerifyResponse = await fetch(
'https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/verify',
{
method: 'POST',
headers: {
'x-firmly-authorization': apiToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
flow_token: latestFlowToken,
type: 'embed_iframe',
iframe_result: {
fidoBlob: capturedFidoBlob,
assuranceData: capturedAssuranceData
}
})
}
);
const { flow_token: enrolledToken, virtual_card_id } = await fidoVerifyResponse.json();
// virtual_card_id confirms the card is enrolled

​​ Step 6: Create Payment Intent

With the card enrolled (via Step 1 or Select Card), create a payment intent when the cardholder is ready to pay.


const intentResponse = await fetch(
'https://api.firmly.work/api/v1/wallets/agentic-pay/intent',
{
method: 'POST',
headers: {
'x-firmly-authorization': apiToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
flow_token: enrolledToken,
mandate: {
amount: '585.49',
currency_code: 'USD',
merchant_name: 'My Store'
}
})
}
);
const { flow_token: intentToken, challenge, intent_id } = await intentResponse.json();
// challenge.type === 'embed_iframe' — load the FIDO assertion iframe (Step 7)
// intent_id present instead of challenge — no challenge needed, go straight to Step 8

curl -X POST https://api.firmly.work/api/v1/wallets/agentic-pay/intent \
-H "x-firmly-authorization: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"flow_token": "eyJhbGci...",
"mandate": {
"amount": "585.49",
"currency_code": "USD",
"merchant_name": "My Store"
}
}'

​​ Step 7: Complete Intent Challenge

Only when Step 6 returned a challenge. Discover — and Mastercard when the cardholder authenticated at enrollment moments earlier — return intent_id directly from /intent; in that case skip to Step 8.

Load the FIDO assertion iframe, capture the result, and submit it.


const challengeResponse = await fetch(
'https://api.firmly.work/api/v1/wallets/agentic-pay/intent/challenge',
{
method: 'POST',
headers: {
'x-firmly-authorization': apiToken,
'Content-Type': 'application/json'
},
body: JSON.stringify({
flow_token: intentToken,
type: 'embed_iframe',
iframe_result: {
fidoBlob: capturedFidoBlob,
assuranceData: capturedAssuranceData
}
})
}
);
const { flow_token: finalToken, intent_id } = await challengeResponse.json();

​​ Step 8: Place the Order

With the flow_token and intent_id, call the wallet-complete-order endpoint using the cardholder’s device JWT.


const orderResponse = await fetch(
'https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/wallet-complete-order',
{
method: 'POST',
headers: {
'x-firmly-authorization': deviceJwt,
'Content-Type': 'application/json'
},
body: JSON.stringify({
wallet: 'agentic-pay',
additional_data: {
flow_token: finalToken,
intent_id: intentId,
transaction: {
amount: '585.49',
currency_code: 'USD',
merchant_name: 'My Store'
}
},
billing_info: {
first_name: 'John',
last_name: 'Smith',
address1: '123 Main St',
city: 'San Francisco',
state_or_province: 'CA',
country: 'US',
postal_code: '94105',
email: 'john@example.com',
phone: '4155551212'
}
})
}
);
const order = await orderResponse.json();

curl -X POST https://cc.firmly.work/api/v2/payment/domains/staging.luma.gift/wallet-complete-order \
-H "x-firmly-authorization: DEVICE_JWT" \
-H "Content-Type: application/json" \
-d '{
"wallet": "agentic-pay",
"additional_data": {
"flow_token": "eyJhbGci...",
"intent_id": "1-5C90F150...",
"transaction": {
"amount": "585.49",
"currency_code": "USD"
}
},
"billing_info": {
"first_name": "John",
"last_name": "Smith",
"address1": "123 Main St",
"city": "San Francisco",
"state_or_province": "CA",
"country": "US",
"postal_code": "94105",
"email": "john@example.com",
"phone": "4155551212"
}
}'

​​ Saved Card: Select Card (Alternative to Step 1)

For returning buyers with a stored virtual_card_id, use /select-card instead of /enroll. No PAN re-entry needed. It returns the same shape as /enroll, so the rest of the flow (handshake → intent → order) is identical.


const { flow_token, verification_methods } = await fetch(
'https://api.firmly.work/api/v1/wallets/agentic-pay/select-card',
{
method: 'POST',
headers: { 'x-firmly-authorization': apiToken, 'Content-Type': 'application/json' },
body: JSON.stringify({
network: savedCard.brand,
virtual_card_id: savedCard.virtual_card_id,
consumer: { email: savedCard.email }
})
}
).then(r => r.json());
if (verification_methods.length > 0) {
// Iframe handshake needed (Visa) — continue from Step 2
} else {
// No handshake (Mastercard) — skip to Step 6 (Create Payment Intent)
}

curl -X POST https://api.firmly.work/api/v1/wallets/agentic-pay/select-card \
-H "x-firmly-authorization: YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"network": "visa",
"virtual_card_id": "cf90be5c86363de702ed19beec46e102",
"consumer": { "email": "jane@example.com" }
}'

Key differences from /enroll:

  • consumer.email is required for Visa (device binding); optional for Mastercard
  • card_art and masked are null — use the values persisted from the original /enroll
  • Visa returns verification_methods with an iframe handshake; Mastercard and Discover return an empty array (skip to Step 6)
  • If the card is revoked or unknown, the API returns PaymentMethodNotAvailable — fall back to /enroll

​​ Error Handling

PaymentMethodNotAvailable (409)
The card network is not supported or not enabled for your account.
BadRequest (400) — secure_token required
An iframe handshake is required before triggering. Load the iframe URI from verification_methods, capture the secure_token via postMessage, and include it in the trigger request.
BadRequest (400) — Invalid flow_token format
The flow_token is malformed or expired. Flow tokens must be used promptly and in sequence — each API call returns a fresh token for the next step.
BadRequest (400)
The OTP value was incorrect. A rejected OTP surfaces as BadRequest (400), not InvalidOtp — do not branch on InvalidOtp here. Prompt the cardholder to re-enter or request a new OTP via /enroll/trigger.
CheckoutError (412)
Payment info or billing address invalid. Verify the billing address and ensure the cart has a valid payment handle.