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

Trigger Enrollment

POST https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/trigger

​​ Overview

The Trigger Enrollment endpoint initiates a specific verification challenge for a card enrollment that was started via the Enroll Card endpoint. The caller selects a method_id from the verification methods returned by /enroll and triggers the corresponding challenge.

The response has two possible shapes:

  • Challenge returned: The cardholder must complete a verification step (OTP code entry or iframe interaction)
  • Short-circuit: The device is already bound to this card, and enrollment completes immediately with a virtual_card_id

​​ Authentication

  • x-firmly-authorization (string, required) — API token for server-to-server authentication

​​ Request Body

  • flow_token (string, required) — JWE-encrypted flow token returned from the /enroll response

  • method_id (string, required) — Verification method ID from the /enroll response (e.g., "OTP_SMS", "OTP_EMAIL", "MANAGED_AUTHENTICATION", "VISA_IFRAME_HANDSHAKE")

  • callback_uri (string) — Callback URI for iframe-based verification methods. The iframe will redirect to this URI upon completion.

  • secure_token (string) — Secure token captured from the iframe handshake via postMessage. Required when the enrollment included an embed_iframe_handshake verification method.

​​ Response

The response takes one of two shapes depending on whether a challenge is required.

​​ Shape 1: Challenge Returned

  • flow_token (string) — Updated JWE-encrypted flow token for the next call in the enrollment flow

  • challenge (object) — The verification challenge the cardholder must complete

    challenge properties
    • challenge.type (string) — Challenge type: "otp" for one-time password or "embed_iframe" for iframe-based verification
    • challenge.uri (string) — Iframe URI for embed_iframe type challenges. Load this in the cardholder’s browser.

​​ Shape 2: Short-Circuit (Device Already Bound)

  • flow_token (string) — Updated JWE-encrypted flow token

  • virtual_card_id (string) — Network-assigned card identifier. Presence of this field indicates the card is already enrolled on this device and no further verification is needed.

​​ Code Examples


curl --request POST \
--url https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/trigger \
--header 'Content-Type: application/json' \
--header 'x-firmly-authorization: YOUR_API_TOKEN' \
--data '{
"flow_token": "eyJhbGciOiJBMjU2S1ci...",
"method_id": "OTP_SMS"
}'

const response = await fetch('https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/trigger', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-firmly-authorization': 'YOUR_API_TOKEN'
},
body: JSON.stringify({
flow_token: 'eyJhbGciOiJBMjU2S1ci...',
method_id: 'OTP_SMS'
})
});
const data = await response.json();
if (data.virtual_card_id) {
// Short-circuit: device already bound, no challenge needed
console.log('Card already enrolled:', data.virtual_card_id);
} else {
// Challenge returned: present to cardholder
console.log('Challenge type:', data.challenge.type);
}

import requests
response = requests.post(
'https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/trigger',
headers={
'Content-Type': 'application/json',
'x-firmly-authorization': 'YOUR_API_TOKEN'
},
json={
'flow_token': 'eyJhbGciOiJBMjU2S1ci...',
'method_id': 'OTP_SMS'
}
)
data = response.json()
if 'virtual_card_id' in data:
# Short-circuit: device already bound
print(f"Card already enrolled: {data['virtual_card_id']}")
else:
# Challenge returned
print(f"Challenge type: {data['challenge']['type']}")

​​ With Iframe Handshake

If the /enroll response included an embed_iframe_handshake verification method, load the iframe, capture the secure_token via postMessage, and include it here. In this case the method_id is "VISA_IFRAME_HANDSHAKE" — secure_token accompanies that method specifically (the other method_id values, such as "OTP_SMS", do not carry a secure_token).


curl --request POST \
--url https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/trigger \
--header 'Content-Type: application/json' \
--header 'x-firmly-authorization: YOUR_API_TOKEN' \
--data '{
"flow_token": "<encrypted-jwe-token>",
"method_id": "VISA_IFRAME_HANDSHAKE",
"secure_token": "captured-from-iframe-postmessage"
}'

const response = await fetch('https://api.firmly.work/api/v1/wallets/agentic-pay/enroll/trigger', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-firmly-authorization': 'YOUR_API_TOKEN'
},
body: JSON.stringify({
flow_token: '<encrypted-jwe-token>',
method_id: 'VISA_IFRAME_HANDSHAKE',
secure_token: 'captured-from-iframe-postmessage'
})
});
const data = await response.json();
console.log(data.challenge);

​​ Response Example


{
"flow_token": "<encrypted-jwe-token>",
"challenge": {
"type": "otp"
}
}

{
"flow_token": "<encrypted-jwe-token>",
"virtual_card_id": "cf90be5c86363de702ed19beec46e102"
}

​​ Error Responses

Error Code Status Description
BadRequest 400 secure_token is required for Visa iframe handshake methods
BadRequest 400 Invalid or expired flow_token format
ErrorServiceUnavailable 503 Network provider (Visa/Mastercard) temporarily unavailable
400 - Missing Secure Token (Visa)

{
"code": 400,
"error": "BadRequest",
"description": "secure_token is required for VISA_IFRAME_HANDSHAKE method (capture from iframe postMessage)"
}
400 - Invalid Flow Token

{
"code": 400,
"error": "BadRequest",
"description": "Invalid or expired flow_token"
}
503 - Service Unavailable

{
"code": 503,
"error": "ErrorServiceUnavailable",
"description": "Network provider temporarily unavailable. Retry after a short delay."
}