Create Intent
POST https://api.firmly.work/api/v1/wallets/agentic-pay/intent
Overview
After card enrollment is complete, call this endpoint to initiate a payment. Provide the transaction details (amount, currency, merchant info). Depending on the network and the enrollment state, the response is either a challenge the cardholder must complete in an iframe (then finalized with Intent Challenge), or the intent_id directly — ready for order placement with no further call.
- Enrollment required: The flow must be fully enrolled (a
virtual_card_idwas returned from Verify Enrollment or Select Card) before creating an intent - Two response shapes:
{ flow_token, challenge }when cardholder authorization is required (Visa; Mastercard by default), or{ flow_token, intent_id }when it is not (Discover always; Mastercard when the cardholder authenticated at enrollment moments earlier and that proof is still fresh). Branch on which key is present. - Mandate details: Transaction amount, currency, and merchant information are captured in the
mandateobject
Authentication
x-firmly-authorization(string, required) — API token for authenticating the request
Request Body
-
flow_token(string, required) — Flow token from a completed enrollment — the flow must be fully enrolled (avirtual_card_idwas returned). -
secure_token(string) — Secure token from a prior iframe handshake. Required if the enrollment used an iframe handshake. -
mandate(object, required) — Transaction details for the payment intent Required fields:amount(string): Transaction amount (e.g.,"585.49")currency_code(string): ISO 4217 currency code (e.g.,"USD") Optional fields:currency_numeric(string): ISO 4217 numeric currency code (e.g.,"840")merchant_name(string): Display name for the merchantmerchant_category(string): Merchant category descriptionmerchant_category_code(string): Merchant Category Code (MCC)description(string): Transaction description shown to the cardholderconsumer_prompt(string): Custom message displayed during FIDO assertionquantity(number): Item quantityttl_seconds(number): Time-to-live for the intent in secondscallback_uri(string): Callback URI passed to the FIDO iframe
Response
The response is one of two shapes. Exactly one of challenge or intent_id is present.
Challenge required (Visa; Mastercard by default):
-
flow_token(string) — Refreshed flow token for the subsequent Intent Challenge call -
challenge(object) — FIDO assertion challenge for cardholder authorization Properties:type(string): Challenge type, typicallyembed_iframeuri(string): Iframe URI for the FIDO assertion flowiframeContext(object): Additional context for configuring the iframe (when present). This object is passed through from the card network, so its keys follow the network’s own casing rather than Firmly’s snake_case convention.
No challenge (Discover always; Mastercard when a fresh enrollment authentication can be reused):
-
flow_token(string) — Refreshed flow token to pass to Wallet Complete Order -
intent_id(string) — Intent identifier for order placement. Skip/intent/challengeand place the order directly.
Code Examples
curl --request POST \--url https://api.firmly.work/api/v1/wallets/agentic-pay/intent \--header 'Content-Type: application/json' \--header 'x-firmly-authorization: YOUR_TOKEN' \--data '{"flow_token": "eyJhbGciOiJBMjU2S1ci...","mandate": {"amount": "585.49","currency_code": "USD","merchant_name": "Example Store","description": "Order #12345"}}'
const response = await fetch('https://api.firmly.work/api/v1/wallets/agentic-pay/intent', {method: 'POST',headers: {'Content-Type': 'application/json','x-firmly-authorization': 'YOUR_TOKEN'},body: JSON.stringify({flow_token: 'eyJhbGciOiJBMjU2S1ci...',mandate: {amount: '585.49',currency_code: 'USD',merchant_name: 'Example Store',description: 'Order #12345'}})});const { flow_token, challenge } = await response.json();// Render the FIDO assertion iframe using challenge.uriconsole.log('Challenge URI:', challenge.uri);
import requestsresponse = requests.post('https://api.firmly.work/api/v1/wallets/agentic-pay/intent',headers={'Content-Type': 'application/json','x-firmly-authorization': 'YOUR_TOKEN'},json={'flow_token': 'eyJhbGciOiJBMjU2S1ci...','mandate': {'amount': '585.49','currency_code': 'USD','merchant_name': 'Example Store','description': 'Order #12345'}})result = response.json()print('Challenge URI:', result['challenge']['uri'])
Response Examples
Challenge required:
{"flow_token": "<encrypted-jwe-token>","challenge": {"type": "embed_iframe","uri": "https://sbx.vts.auth.visa.com/vts-auth/authenticate?apiKey=...","iframeContext": {"endpoint": "L29hdXRoMi9hdXRob3JpemF0aW9u","identifier": "59c3208baeadec4566181b18356a5e02","payload": "eyJraWQiOiI0ZDkxZWY5MiIsImFsZyI6IlJTMjU2In0","action": "AUTHENTICATE"}}}
No challenge — intent_id returned directly:
{"flow_token": "<encrypted-jwe-token>","intent_id": "1-5C90F1500800b0be2dc0-e6cf-55d5-54e6-12d8519fad02"}
Error Responses
| Code | Status | Description |
|---|---|---|
BadRequest |
400 | Invalid flow_token format or missing required mandate fields |
BadRequest |
400 | Card not enrolled — the flow is not fully enrolled (no virtual_card_id was returned) |
ErrorServiceUnavailable |
503 | Network intent creation failed or timed out |
Related Endpoints
- Verify Enrollment — Complete card enrollment before creating an intent
- Intent Challenge — Submit the FIDO assertion result to finalize the intent (only when the response carried a
challenge) - Wallet Complete Order — Place the order once you hold an
intent_id