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

Headless SDK

​​ Overview

@firmly/agentic-pay-sdk is a browser SDK that wraps Firmly’s Agentic Pay endpoints (/enroll, /select-card, /enroll/trigger, /enroll/verify, /intent, /intent/challenge, /complete, /cards/delete) behind a small, opinionated JavaScript API.

It supports Visa and Mastercard cards. Discover enrollment has no browser ceremony, so drive it with the REST endpoints directly rather than through the SDK.

It is headless — it ships no UI. Your application renders the checkout experience and calls the SDK methods; the SDK handles the network-specific mechanics (token state, the Mastercard / Visa authentication iframe, and the hand-off to Firmly’s payment vault for order completion).

​​ Creating a client


import { FirmlyAgenticPay } from '@firmly/agentic-pay-sdk';
const fap = new FirmlyAgenticPay({
baseUrl: 'https://api.firmly.work',
apiToken: '<bearer>'
});

​​ ClientConfig

These are the constructor inputs passed to new FirmlyAgenticPay({ ... }).

  • baseUrl (string, required) — Agentic Pay base URL, e.g. https://api.firmly.work.

  • apiToken (string, required) — API token provisioned by Firmly with Agentic Pay access. The SDK sends it as a standard Authorization: Bearer <apiToken> header. (Firmly’s endpoints accept the token either as Authorization: Bearer or in the raw x-firmly-authorization header; the SDK uses the bearer form.) Used on the enrollment and intent endpoints.

  • iframeContainer (Element) — Default DOM container for registerPasskey / createIntent to mount the network authentication iframe in. A per-call container override is also supported.

  • iframeOrigins (string[]) — Allow-list of origins permitted to send the result postMessage. Required and non-empty for registerPasskey and createIntent — an empty list would let any page forge an iframe_result and bypass cardholder authentication. Typical values: the Firmly bridge origin plus the network auth iframe origin (e.g. ['https://api.firmly.work', 'https://sandbox.src.mastercard.com']).

  • bridgeUrl (string) — Default callback_uri for iframe flows — sent on /enroll/trigger (during registerPasskey) and on /intent (during createIntent). Firmly forwards this to the network’s iframe, which redirects to it on completion — typically a Firmly-hosted bridge route that decodes the result and postMessages it back to the SDK.

  • vaultBaseUrl (string) — Base URL for Firmly’s payment vault (defaults to baseUrl). completeOrder calls the vault, which proxies internally into the service-binding-only /complete route.

  • deviceToken (string) — Device JWT for the payment vault’s deviceAuth — required by completeOrder. Obtained from /api/v1/browser-session. Distinct from apiToken.

  • merchantDomain (string) — Default merchant domain for the vault’s /domains/:domain/wallet-complete-order path. A per-call domain override is supported.

  • timeoutMs (number) — Per-HTTP-request timeout. Default 90000 ms.

  • iframeTimeoutMs (number) — How long registerPasskey / createIntent wait for the iframe postMessage result before rejecting. Distinct from timeoutMs (which is the HTTP request timeout). Default 90000 ms.

​​ The flow

There are two entry points. Choose one based on whether the buyer is entering a new card or reusing a saved one — both return an EnrollResult and converge on the shared intent → complete chain below.

Path A — enrollCard (new card) Submit the cardholder’s PAN. Firmly detects the network, enrolls the card, and returns a FlowSession plus verification methods. Persist virtualCardId, cardArt, and masked for returning buyers.
Path B — selectCard (saved card) For a returning buyer, call selectCard with the stored virtualCardId — no PAN re-entry. Returns the same shape as enrollCard.

Once you have a FlowSession from either path, the rest of the flow is shared:

​​ Verify the cardholder

For OTP methods, call triggerOtp then verifyOtp. For iframe methods, call registerPasskey — the SDK mounts the network iframe and resolves when the cardholder completes authentication. (A Mastercard saved card via selectCard produces its challenge in createIntent instead.)

​​ Create a payment intent — createIntent

Create an intent bound to a mandate (amount + currency). The SDK handles the intent challenge iframe. When the wallet returns the intent_id without a challenge (Mastercard reusing a fresh enrollment authentication) the call resolves without mounting anything — but iframeOrigins and a container are still required up front, because the SDK cannot know in advance whether a challenge will be issued.

​​ Place the order — completeOrder

Hand the flow to Firmly’s payment vault, which retrieves the network token credentials and submits the order to the merchant.

​​ Methods

Method Purpose Auth
enrollCard Enroll a new card from its PAN apiToken
selectCard Start a payment from a previously-enrolled card apiToken
triggerOtp Send an SMS / email OTP challenge apiToken
verifyOtp Submit the OTP code the cardholder entered apiToken
registerPasskey Run an iframe-based verification ceremony apiToken
createIntent Create a payment intent for a mandate apiToken
completeOrder Complete the order via Firmly’s payment vault deviceToken
deleteCard Revoke an enrolled card on the network apiToken

​​ Errors

The SDK surfaces failures as rejected promises — await each call inside a try / catch.

  • HTTP / API errors. A non-2xx response from an Agentic Pay or vault endpoint rejects with an error whose message and, where available, structured { code, error, description } payload mirror the underlying REST error envelope. Program against the error value, not the human-readable description.
  • Timeouts. timeoutMs bounds each HTTP request; iframeTimeoutMs bounds how long registerPasskey / createIntent wait for the iframe postMessage. Exceeding either rejects the call.
  • Abort. Passing an already-aborted (or later-aborted) AbortSignal via options.signal rejects the pending call.
  • Configuration guards. registerPasskey and createIntent reject up front if iframeOrigins is empty, or if no iframe container is available (neither options.container nor ClientConfig.iframeContainer is set).