Updated on 3 July, 2026
WP 2FA Passkeys (WebAuthn) REST API Documentation
This documentation covers WP 2FA’s core REST API endpoint for validating a user’s second-factor authentication token, including TOTP, email codes, backup codes, SMS codes, and other supported token-based providers.
Note: depending on configuration, you may still need to complete additional 2FA steps after passkey sign-in. For token-based verification flows, refer to the WP 2FA REST API documentation.
Namespace: wp-2fa-passkeys/v1
How it works: The plugin bundles @simplewebauthn/browser and calls the API with WordPress’s wp.apiFetch(), which handles authentication automatically via cookies and nonces — no Bearer tokens or API keys. startRegistration() and startAuthentication() convert the challenge and binary fields to and from base64url for you, so you rarely touch encoding directly.
Table of contents
Key rules
- All binary fields (challenges,
rawId,clientDataJSON,attestationObject, etc.) are base64url-encoded (RFC 4648 §5): charactersA-Z a-z 0-9 - _, no+or/. - Challenges are short-lived (sign-in challenges expire after 60 seconds).
- Registration endpoints require an authenticated user; sign-in endpoints are public.
- All passkey endpoints are
POST.
Endpoint summary
| Flow | Endpoint | Auth |
|---|---|---|
| Registration – request challenge | /register/request | Required |
| Registration – complete | /register/response | Required |
| Revoke a passkey | /register/revoke | Required |
| Enable / disable a passkey | /register/enable | Required |
| Sign-in – request challenge | /singin/request | Public |
| Sign-in – complete | /singin/response | Public |
Note: the sign-in path is spelledsingin(notsignin) in the plugin. Use it exactly as shown.
Registration flow
Registration is two calls: request a challenge, then send the authenticator’s response back to verify it.
1. Request challenge
POST: /wp-json/wp-2fa-passkeys/v1/register/request
Optional body: is_usb (boolean) – whether to target a cross-platform/USB authenticator. Returns a publicKeyCredentialCreationOptions object to pass straight into startRegistration().
2. Complete registration
POST: /wp-json/wp-2fa-passkeys/v1/register/response
| Parameter | Type | Required | Description |
|---|---|---|---|
attResp | object | Yes | The attestation object returned by startRegistration(). |
passkey_name | string | No | Friendly label for the passkey. If omitted, a name is generated from the platform. |
Success and error responses:
// Success
{ "status": "verified", "message": "Successfully registered." }
// Error
{ "code": "public_key_validation_failed",
"message": "Public key validation failed.",
"data": { "status": 400 } }
Minimal registration example
// Step 1 - get the challenge
const options = await wp.apiFetch( {
path: '/wp-2fa-passkeys/v1/register/request',
method: 'POST',
data: { is_usb: false },
} );
// Step 2 - let the browser create the credential
const attResp = await startRegistration( options );
// Step 3 - send it back to finish
const res = await wp.apiFetch( {
path: '/wp-2fa-passkeys/v1/register/response',
method: 'POST',
data: { attResp, passkey_name: 'My laptop' },
} );
if ( res.status === 'verified' ) {
window.location.reload();
}
Revoke a passkey
POST: /wp-json/wp-2fa-passkeys/v1/register/revoke
Body: fingerprint (string, required) – the base64url credential ID to delete.
// Success
{ "status": "success", "message": "Successfully revoked." }
Enable / disable a passkey
POST: /wp-json/wp-2fa-passkeys/v1/register/enable
Toggles a passkey’s enabled state without deleting it. The caller must be the credential owner or have the edit_user capability.
| Parameter | Type | Required | Description |
|---|---|---|---|
info.fingerprint | string | Yes | Base64url credential ID. |
info.user_id | integer | Yes | ID of the user who owns the credential. |
// Success (status toggles between enabled and disabled)
{ "status": "success", "message": "Successfully enabled/disabled." }
Sign-in flow
Sign-in mirrors registration: request a challenge for the username, get an assertion from the authenticator, then send it back to complete the login.
1. Request challenge
POST/wp-json/wp-2fa-passkeys/v1/singin/request
Body: user (string, required) – username or email. The response holds an options object (challenge, allowCredentials, etc.) plus a request_id you must echo back in step 2. The challenge is stored server-side with a 60-second TTL.
2. Complete authentication
POST: /wp-json/wp-2fa-passkeys/v1/singin/response
| Parameter | Type | Required | Description |
|---|---|---|---|
request_id | string | Yes | The ID returned by the challenge request. |
asseResp | object | Yes | The assertion returned by startAuthentication(). |
user | string | Yes | Username or email address. |
redirect_to | string | No | URL to redirect to after a successful sign-in. |
// Success
{ "status": "verified",
"message": "Successfully signin with Passkey.",
"redirect_to": "https://example.com/wp-admin/" }
// Error
{ "code": "invalid_credentials",
"message": "Invalid credentials.",
"data": { "status": 400 } }
On success, the endpoint sets the WordPress auth cookie via wp_set_auth_cookie() and updates the credential’s last_used timestamp. Depending on the configuration, the user may still need to complete additional 2FA steps.
Minimal sign-in example
// Step 1 - challenge for this user
const { options, request_id } = await wp.apiFetch( {
path: '/wp-2fa-passkeys/v1/singin/request',
method: 'POST',
data: { user: username },
} );
// Step 2 - get the assertion from the authenticator
const asseResp = await startAuthentication( options );
// Step 3 - complete the login
const res = await wp.apiFetch( {
path: '/wp-2fa-passkeys/v1/singin/response',
method: 'POST',
data: { request_id, asseResp, user: username, redirect_to: redirectTo },
} );
if ( res.status === 'verified' ) {
window.location.href = res.redirect_to || '/wp-admin';
}
Storage
Passkey credentials are stored in WordPress user meta. Each credential lives under its own key and records the device name, timestamps, enabled flag, and the public key (the private key never leaves the user’s device).
| Item | Storage | Key pattern | TTL |
|---|---|---|---|
| Credential | User meta | wp_2fa_passkey_<credential_id> | Persistent |
| Registration challenge | User meta | wp_2fa_passkey_challenge | Until used |
| Sign-in challenge | Transient | wp_2fa_passkey_<request_id> | 60 seconds |
Server-side, all base64url inputs are validated with this pattern; invalid encoding returns a 400:
$b64url_re = '/^[A-Za-z0-9\-_]+=*$/';
Security notes
- Challenge expiry: sign-in challenges expire after 60 seconds – complete the flow promptly.
- No user enumeration: sign-in endpoints return a generic “Invalid credentials” error regardless of whether the user or passkey exists.
- HTTPS required: WebAuthn needs HTTPS in production (localhost is exempt for development).
- Origin binding: the Relying Party ID must match the site’s domain.
- Rate limiting: not built in – consider adding it on the sign-in endpoints to deter brute-force attempts.