1. Identity Mfa
BNW CORE SERVICE
  • Card Vaulting
    • Create a card vault session
      POST
  • Fee Lookups
    • Compute fees for a transaction
      POST
  • Intent Action Status Lookups
    • Look up the status of an intent action
      GET
  • Payment Intents
    • Create a payment intent
      POST
    • Cancel a payment intent
      PATCH
    • List payment intents
      GET
    • Retrieve a payment intent
      GET
  • Intent Actions
    • Create an intent action
      POST
    • Advance an intent action to the next step
      PATCH
    • Retrieve the authentication session for a pending redirect action
      GET
  • Accounts
    • Create a new account
      POST
    • List accounts
      GET
    • Close an account
      DELETE
    • Update an account
      PATCH
    • Retrieve an account
      GET
    • Retrieve an account's daily payout limit status
      GET
  • API Keys
    • Create an API key
      POST
    • List API keys
      GET
    • Update an API key
      PATCH
    • Retrieve an API key
      GET
    • Revoke an API key
      DELETE
    • Rotate an API key's secret
      POST
  • App Setting Values
    • Delete an app setting value
    • Retrieve an app setting value
    • List app setting values
    • Create or update an app setting value
  • Entities
    • Create an entity
    • List entities
    • Close an entity
    • Update an entity
    • Retrieve an entity
    • Set the selected entity
    • Retrieve public metadata for the authenticated entity
    • Create an entity sandbox session
    • Retrieve one of your entities, including your membership
    • List my entities
  • Counterparties
    • Create a counterparty
    • List counterparties
    • Update a counterparty
    • Retrieve a counterparty
  • Maker Checker Approver Groups
    • Create a maker-checker approver group
    • List maker-checker approver groups
    • Deactivate a maker-checker approver group
    • Update a maker-checker approver group
    • Retrieve a maker-checker approver group
  • Bulk Payout Intent Actions
    • Create bulk payout intent actions
    • Retrieve a bulk payout intent action batch
    • Preview bulk payout intent actions
  • Payout Intent Action Retries
    • Retry a failed payout intent action
  • Payout Intent Actions
    • Create a payout intent action
    • List payout intent actions
    • Retrieve a payout intent action
  • Payout Intents
    • Create a payout intent
    • List payout intents
    • Retrieve a payout intent
    • Edit a payout intent
    • Cancel a payout intent's active schedule
    • Edit a payout intent's active schedule
    • Cancel a payout intent
  • Virtual Cards
    • Create a virtual card
    • List virtual cards
    • Update a virtual card
    • Retrieve a virtual card
  • Maker Checker Action Decisions
    • Record a decision on a maker-checker action
  • Maker Checker Actions
    • Retrieve a maker-checker action's status
    • List maker-checker actions
    • Withdraw a maker-checker action
  • Payment Pages
    • Create a payment page
    • List payment pages
    • Delete a payment page
    • Edit a payment page
    • Retrieve a payment page
    • Retrieve a published payment page by slug
  • Invoices
    • Create an invoice
    • List invoices
    • Retrieve an invoice
    • Edit an invoice
    • Retrieve an invoice as a PDF
    • Download a public invoice as a PDF
    • Retrieve a public invoice by invoice number
    • Send a reminder for an invoice
    • Send or resend an invoice
  • Entity Data
    • Retrieve the data submission checklist for an entity
    • Submit entity data for KYC requirements
    • List entity data
    • Review and decide on entity data submission
  • Unified Verifs
    • Proxy a DELETE request to the Verification API
    • Proxy a GET request to the Verification API
    • Proxy a PATCH request to the Verification API
    • Forward a POST request to the Verification API
  • Identity Session
    • List identity sessions
    • Complete the MFA step of a pending login session
    • Request a login MFA challenge or one-time-passcode
  • Identity Sessions
    • Terminate all active sessions for an identity
    • Terminate an identity session
  • Entity Sandboxes
    • Create an additional named entity sandbox
    • Delete an entity sandbox
    • Rename an entity sandbox
  • Identity Mfa
    • Passkeys
    • Enroll a new MFA method for the authenticated identity
      POST
    • List MFA methods for the authenticated identity
      GET
    • Retrieve an identity MFA method
      GET
    • Advance or transition the state of an existing MFA method
      PATCH
  • Auth
    • Log in with email and password
    • Register a new identity
    • Request an email verification link
    • Verify an email address
    • Request a password reset
    • Complete a password reset with a one-time token
    • Change the authenticated identity's password
    • Confirm a pending email address change
    • Request a change of the authenticated identity's email address
    • Resend an identity-confirmation email
    • Confirm a phone number change for the current identity
    • Request a phone number change for the current identity
  • Virtual Card Actions
    • Perform an action on a virtual card
  • ACLs
    • List all ACL definitions
  • Account Transfers
    • Create an account transfer
    • List account transfers
    • Retrieve an account transfer
  • ACL Roles
    • List all ACL roles
  • Counterparty Instruments
    • Add a bank account to a counterparty
    • List counterparty instruments
    • Remove a counterparty's bank account
  • Currency Conversion Quotes
    • Create a currency conversion quote
  • Entity Access
    • Change a team member's role on an entity
    • Remove a member's access to an entity
    • List an entity's team members
  • Entity Access Invites
    • Accept an entity access invitation
    • Invite a new member to an entity
    • List entity access invites
    • Revoke an entity access invite
  • Entity Ownership Transfers
    • Transfer ownership of an entity
  • Identity
    • Update the authenticated identity's name
    • Retrieve the authenticated identity
  • Money Action Confirmations
    • Confirm a money-moving action with PIN (and, where required, an authenticator code)
  • Payout Intent Action Returns
    • Record a payout intent action as returned
  • Payout Intent Action Status Logs
    • List a payout intent action's status history
  • Payout Intent Schedule Actions
    • Apply an action to a payout intent's schedule
  • Sandbox
    • Credit test money into an entity's sandbox wallet
    • Reset a shadow entity's sandbox
    • Get or create the sandbox identity for the authenticated identity
    • Provision a sandbox identity and master API key
    • Exchange the sandbox master API key for a login session
    • Fund an entity's test wallet with sandbox test money
  • Transactions
    • Retrieve a transaction
    • List transactions
  • Virtual Card Activity
    • List a virtual card's activity
  • Virtual Card Sessions
    • Create a virtual card viewing session
  • Virtual Card Topups
    • Top up a virtual card
  • Virtual Card Transaction Callbacks
    • Handle an inbound virtual card transaction callback
  • Virtual Card Withdrawals
    • Withdraw funds from a virtual card
  • Schemas
    • ErrorResponse
    • SuccessResponse
    • AccountBalance
    • Account
  1. Identity Mfa

Passkeys

Passkey (WebAuthn) MFA — Frontend Handover#

This document explains how to integrate the frontend with the passkey MFA support added to
identity-mfa (registration/management) and identity-session (login).
Passkeys use the browser's native WebAuthn API. The backend uses
@simplewebauthn/server to generate/verify challenges, so the
frontend should use the matching client library,
@simplewebauthn/browser:
That library exposes two helpers used throughout this doc:
startRegistration(options) — wraps navigator.credentials.create()
startAuthentication(options) — wraps navigator.credentials.get()
Both take the JSON options object returned by our API as-is, and both return a JSON response
object that should be sent back to our API unmodified (as attestation_response /
assertion_response).
Requires HTTPS (or localhost) — WebAuthn will not work over plain HTTP in any other host.

1. Registering a new passkey (user is already logged in)#

This is a two-step flow: request registration options, then verify what the browser returns.

Step 1 — Request registration options#

POST /identity-mfas
Requires the normal authenticated session (JWT), same as any other identity-mfa creation call.
Request body:
{
  "type": "passkey",
  "device_id": "a stable, client-generated identifier for this device/browser",
  "device_name": "e.g. \"Chrome on Sam's MacBook\" (optional, shown in device lists)"
}
device_id is required for passkeys. It's how we tell one registered device/browser apart
from another for the same identity (also used later to remove a specific passkey). Generate it
once per device/browser (e.g. crypto.randomUUID()) and persist it (e.g. localStorage) — do
not regenerate it on every registration attempt from the same device.
device_name is optional but recommended for a good "manage your passkeys" UX.
Response (201):
{
  "id": "01J...",
  "identity_id": "01J...",
  "type": "passkey",
  "status": "pending",
  "contexts": ["login"],
  "system_meta": { "passkey": { "device_id": "...", "device_name": "...", "device_type": null } },
  "created": 1234567890,
  "updated": 1234567890,
  "provisioning": {
    /* PublicKeyCredentialCreationOptionsJSON — pass this directly to startRegistration() */
  },
  "backup_codes": null
}
Keep the id from the response — you'll need it in step 2 (as the :id path param).

Step 2 — Create the credential in the browser and verify it#

Then confirm it with the server:
PATCH /identity-mfas/:id
{
  "attestation_response": "<the object returned by startRegistration()>"
}
Response (200) is the same normalized shape as step 1, now with status: "active".
The registration challenge expires after MFA_PASSKEY_CHALLENGE_TTL_MS (5 minutes by default) —
if the user takes too long between step 1 and step 2, this call will fail and step 1 must be
repeated.

Removing a passkey#

PATCH /identity-mfas/:id
{ "status": "inactive" }
Notes:
This works directly (no re-verification step) — it's treated like "remove this device".
The API will reject this (403) if it's the identity's last active MFA method usable for
login, to avoid locking the user out. Surface this as a normal error message; there's nothing
the frontend needs to do differently, just handle the error response.

2. Logging in with a passkey#

Login always starts the same way as password login, then branches into an MFA step if the
identity has any active login MFA methods.

Step 1 — Password login#

POST /identity-sessions
{ "email": "...", "password": "..." }
If mfa_required is false, login is complete — nothing else to do.
If mfa_required is true, the response looks like:
{
  "mfa_required": true,
  "session_id": "01J...",
  "session_token": "...",
  "expires_at": 1234567890,
  "next_steps": [
    { "identity_mfa_id": "01J...", "type": "passkey" },
    { "identity_mfa_id": "01J...", "type": "email-mobile", "masked_email": "j***@example.com" }
  ]
}
next_steps tells you which MFA methods are available. If any entry has "type": "passkey",
you can offer a "Sign in with a passkey" option (in addition to, or instead of, the existing OTP
flow — there is no need to pick a specific identity_mfa_id for passkeys, since the browser
handles that as part of resolving which credential to use).

Step 2 — Request passkey authentication options#

POST /session-otps
{ "session_token": "<from step 1>", "type": "passkey" }
Response (201):
{
  "type": "passkey",
  "options": {
    /* PublicKeyCredentialRequestOptionsJSON — pass this directly to startAuthentication() */
  }
}
If the identity has no active passkeys, this returns a 404.
Note: this is the same endpoint used for requesting an email OTP — pass identity_mfa_id
instead of type for that flow. The two are mutually exclusive per call.

Step 3 — Get the assertion from the browser and complete the session#

PATCH /identity-sessions
{
  "session_token": "<from step 1>",
  "assertion_response": "<the object returned by startAuthentication()>"
}
The server figures out which registered passkey was used from the assertion itself — you don't
need to pass identity_mfa_id. Response (200) is the standard completed-session shape:
{
  "mfa_required": false,
  "session_token": "...",
  "session_id": "01J...",
  "identity_id": "01J...",
  "expires_at": 1234567890,
  "identity": { "name": "...", "primary_email": "...", "status": "active" }
}
This session_token replaces the pending one from step 1 — use it going forward exactly like the
token from a non-MFA login.
The authentication challenge from step 2 also expires after MFA_PASSKEY_CHALLENGE_TTL_MS (5
minutes by default); if the user takes too long, step 2 must be repeated.

Error reference#

These are the new error messages the frontend may encounter (in addition to the existing
identity-mfa / identity-session errors already handled elsewhere):
MessageWhen
A device_id is required to enable passkey MFAdevice_id missing on POST /identity-mfas with type: passkey
This passkey registration request has expired. Please try againToo long between registration steps 1 and 2
A passkey registration response is required to verify this methodattestation_response missing on the verify PATCH
The passkey registration response is invalidBrowser response failed WebAuthn verification
This passkey is already registered to an identityThe exact same credential is already registered (e.g. re-registering)
This is the last active login MFA method for this identity and cannot be deactivatedTrying to remove the identity's only active login MFA method
Either identity_mfa_id or type is requiredNeither provided on POST /session-otps
No active passkeys are registered for this identityPOST /session-otps with type: passkey but identity has none
A passkey assertion response is required to complete this loginassertion_response missing on PATCH /identity-sessions
This passkey is not registered for this identityAssertion's credential id doesn't match any active passkey for the session's identity
No pending passkey login challenge found for this sessionStep 3 attempted without (or after expiry of) step 2's challenge
The passkey login response is invalid or has expiredBrowser assertion failed WebAuthn verification, or a sign-counter anomaly was detected
All of the above are returned as standard API error responses — no special-case handling is
needed beyond displaying the message and, where relevant, letting the user retry from the
appropriate step.

Summary of endpoints#

StepMethodPathAuth
Start passkey registrationPOST/identity-mfasJWT (logged in)
Verify passkey registrationPATCH/identity-mfas/:idJWT (logged in)
Remove a passkeyPATCH/identity-mfas/:idJWT (logged in)
Password loginPOST/identity-sessionsnone
Request passkey login optionsPOST/session-otpsnone (uses session_token)
Complete passkey loginPATCH/identity-sessionsnone (uses session_token)
Modified at 2026-09-28 19:35:15
Previous
Rename an entity sandbox
Next
Enroll a new MFA method for the authenticated identity
Built with