1. Docs
  2. API Reference
  3. Complete WebAuthn factor enrollment

Complete WebAuthn factor enrollment

POST/v1/identity/auth/mfa/webauthn/enroll/verify

Authentication

  • Bearer Token Authorization

    JWT access token

Request body

  • enrollment_tokenstring*

    The sealed enrollment_token returned by /mfa/webauthn/enroll/options. Carries the WebAuthn challenge nonce server-side.

  • responseany object*

    The PublicKeyCredential JSON returned by `navigator.credentials.create()` — id, rawId, type, response { clientDataJSON, attestationObject, transports }, clientExtensionResults.

  • labelstring*

    User-facing nickname for the factor ("YubiKey 5C", "MacBook passkey").

Code samples

cURLJavaScriptPythonGo
curl -X POST "https://api.canopy.dev/v1/identity/auth/mfa/webauthn/enroll/verify" \
  -H "Authorization: Bearer $CANOPY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "enrollment_token": "string",
    "response": {},
    "label": "MacBook passkey"
  }'

Responses

200 Verifies the attestation against the stored challenge nonce, persists the WebAuthn factor (credential id, public key, counter, transports, aaguid), and returns the 10 single-use recovery codes when this is the identity's first factor.
{
  "factor": {
    "id": "00000000-0000-0000-0000-000000000000",
    "type": "totp",
    "label": "iPhone 15",
    "enrolled_at": "2026-04-20T12:00:00.000Z",
    "last_used_at": "2026-04-20T12:00:00.000Z"
  },
  "recovery_codes": [
    "ABCD-EFGH-IJKL-MNOP",
    "QRST-UVWX-YZ23-4567",
    "..."
  ],
  "recovery_codes_generation": 0
}

application/json

  • factorMfaFactorResponseDto*
  • recovery_codesstring[]

    Ten freshly-minted single-use recovery codes — **only present on the first factor enrollment**. Subsequent enrollments return `null` here because the prior batch is still valid; the user already saw it the first time around.

  • recovery_codes_generationnumber

    Generation number of the active batch — `null` when no codes were issued from this enrollment (subsequent factor).

400 WebAuthn attestation failed to verify (challenge mismatch, RP-ID mismatch, signature invalid, or credential ineligible)
401 Invalid or expired token
403 This token is not authorized for this endpoint (wrong principal type — e.g., admin token on identity-only endpoint, or vice versa)

Returned object

On this page

Related endpoints

GETList enrolled MFA factors for the caller
POSTBegin TOTP factor enrollment
POSTComplete TOTP factor enrollment
POSTBegin WebAuthn factor enrollment
POSTProve a fresh factor to authorise a sensitive MFA mutation
POSTBegin a WebAuthn-backed step-up ceremony
POSTComplete a WebAuthn step-up ceremony
DELETERemove an enrolled MFA factor
POSTRegenerate the identity's single-use recovery codes
GETList the identity's active 'remember this device' records
DELETERevoke every trusted device for the caller
DELETERevoke a single trusted device