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/enrollresponse -
method_id(string, required) — Verification method ID from the/enrollresponse (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 anembed_iframe_handshakeverification 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 completechallenge properties
challenge.type(string) — Challenge type:"otp"for one-time password or"embed_iframe"for iframe-based verificationchallenge.uri(string) — Iframe URI forembed_iframetype 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 neededconsole.log('Card already enrolled:', data.virtual_card_id);} else {// Challenge returned: present to cardholderconsole.log('Challenge type:', data.challenge.type);}
import requestsresponse = 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 boundprint(f"Card already enrolled: {data['virtual_card_id']}")else:# Challenge returnedprint(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."}
Related Endpoints
- Enroll Card — Start enrollment and get available verification methods
- Agentic Pay Overview — Concepts and full endpoint list