REST API reference

For integrators with no JavaScript asset — a native mobile app or a separate single-page app. Every operation performed by the shipped UI and the headless JS API goes through these whitelisted endpoints; this page is the raw contract.

If you can load a script, prefer custom-ui.md — the headless API handles the base64url encoding, the L3 JSON shapes, CSRF, and the retry contracts for you. Read security.md and the invariants before you build; a native app additionally needs mobile-apps.md.


Conventions


Guest — first-factor (passwordless) login

The ceremony is discoverable-credential only: no username is ever sent, so there is no account-enumeration oracle. begin_login → navigator.credentials.get() on the client → verify_login.

passkeys.passkey.begin_login

Mint assertion options + a single-use ceremony and set the binder cookie. This is also the client’s config channel. state_id/options require login_with_passkey and an operational RP. Source: passkeys/passkey.py:begin_login. Rate limit: 30 / 60 s / IP. Guest.

passkeys.passkey.verify_login

Verify the assertion, resolve the account from its userHandle + credential id, enforce the user-verification policy, mint the session. Source: passkeys/passkey.py:verify_login. Rate limit: 10 / 60 s / IP. Guest.

passkeys.passkey.complete_uv_setup

Finish a passkey whose UVSetupRequired was raised: a one-time password check authorises the user-verification flip, then the session is minted. Stays available even under disable_user_pass_login (the password is a factor for the flip, not a password login). Source: passkeys/passkey.py:complete_uv_setup. Rate limit: 5 / 300 s / IP. Guest.


Guest — passkey as a second factor

Active only when passkey_as_second_factor is on. Password first (leg 1), passkey step-up second (leg 2), speaking Frappe’s own two-factor envelope so core paints the result natively. These mirror a lot of core’s login() / authenticate_for_2factor; integrate them only if you are replacing the login page wholesale. Enrolled users get the passkey step even when core’s should_run_2fa(user) is false. Core’s role/IP coverage controls OTP for passkey-less users and whether OTP fallback can be offered to enrolled users.

passkeys.passkey.login_with_password

Leg 1: verify the password via core authenticate, then return a passkey assertion challenge inside core’s verification / tmp_id envelope (or transparently fall back to core OTP / plain login for users without usable passkeys, including stale-only users). Source: passkeys/passkey.py:login_with_password. Rate limit: 10 / 60 s / IP. Guest.

passkeys.passkey.verify_second_factor

Leg 2: verify the step-up assertion, re-authenticate, mint the session. Source: passkeys/passkey.py:verify_second_factor. Rate limit: 10 / 60 s / IP. Guest.

The password leg records a keyed, non-reversible password-hash version and leg 2 compares it again before minting the session. An RP change returns CeremonyExpired without state_id or verification (no re-arm). Password rotation or user disable during the ceremony is terminal and fails closed. For enrolled users, the final login hook also vetoes alternate/core login paths unless this app completed the passkey leg or issued the one-time OTP fallback marker.

passkeys.passkey.fallback_to_otp

Abandon the passkey step-up mid-flow and hand off to core’s one-time-code path (re-checks the knob server-side). Source: passkeys/passkey.py:fallback_to_otp. Rate limit: 5 / 300 s / IP. Guest.


Authenticated — registration (add a passkey)

Sudo-gated: a fresh confirmation (or password re-auth) must have seeded a live management window. Source: passkeys/api/registration.py.

passkeys.api.registration.begin_registration

Issue creation options + cache the challenge. Rate limit: 20 / 3600 s / user.

passkeys.api.registration.verify_registration

Verify the creation response and persist the WebAuthn Credential. The global credential_id_sha256 unique index is the final arbiter of duplicate/hijack. Rate limit: 20 / 3600 s / user.


Authenticated — manage credentials

Source: passkeys/api/credentials.py. All resolve identity from the session user — a name you don’t own returns a uniform DoesNotExistError (no cross-user oracle).

passkeys.api.credentials.list_credentials

The caller’s own passkeys. A read — not sudo-gated. Rate limit: 60 / 60 s / user.

site_rp_id is a string or null (no operational path). stale means a different/blank stored RP or no operational site RP; usable means enabled and not stale. Disabled current-RP rows are not stale. All rows, including stale and disabled ones, remain in the inventory and cap. The stored passkey_only_login flag is not a statement that its login veto currently applies.

passkeys.api.credentials.rename_credential

Display-only; no sudo. Rate limit: 20 / 3600 s / user.

passkeys.api.credentials.delete_credential

Sudo-gated + last-credential guard. Rate limit: 10 / 3600 s / user.

Stale removal is allowed by the last-passkey guard; ownership and sudo still apply.

passkeys.api.credentials.set_passkey_only_login

Turn password login off/on for the account. Gated on a passkey grant only — never a password/sudo window (“a password must never disable the password-is-not-sufficient flag”). Enabling additionally needs ≥2 usable passkeys and at least one passkey login mode on. Rate limit: 20 / 3600 s / user.


Authenticated — action confirmation (“passkey signing”)

The re-auth engine that mints the single-use grants the sudo-gated endpoints above consume, and that any app’s @passkey_protected method uses. Source: passkeys/confirm.py. Rate limits: 30 / 300 s / user (reauth_password: 5 / 300).

passkeys.confirm.begin_confirmation

Mint UV-required assertion options bound to an action (+ optional payload).

passkeys.confirm.verify_confirmation

Verify the UV assertion, mint the grant.

passkeys.confirm.reauth_password

Password fallback: seed a sudo window (bare pwd) or mint an action grant (pwd + action + payload_fingerprint, only if the action allows password fallback). Rate limit: 5 / 300 s / user.

Confirmation offers passkeys only when the user has usable credentials. Password re-auth is refused under site-wide password disable only while a usable credential exists; a signed-in stale-only user can re-authenticate with a password where the action permits it. This does not permit password sign-in or change a passkey-only switch without a passkey grant.


Authenticated — supporting endpoints


System Manager — RP settings and stale removal

All endpoints below use POST under passkeys.passkeys.doctype.passkey_settings.passkey_settings (append the method name). They require the System Manager role, accept normal administrator API authentication, and return data under message. Limits are per calling user; exhaustion returns 429.

get_rp_id_change_impact

Read-only preview. 30 / 60 s / user; no sudo required.

remove_stale_passkeys

Delete stale, unflagged rows site-wide, including disabled rows. 3 / 60 s / user; no sudo required. Impersonated sessions receive ImpersonatedSessionRefused (403).

Existing settings reads

Boot contract

Desk and portal boot add site_rp_id; credential_count now counts usable keys. Existing rp_id remains the resolved configuration value. Only System Managers receive rp_alert: null when both modes are off, otherwise {reason, status, rp_id, site_host, suggested_rp_id, working_origins}. status is off, app_only or web_works; reason is no_rp_id, host_outside_rp, no_valid_origins or null. A null reason needs no banner. Origins and the site host come from configuration, never a caller’s headers. site_rp_id describes operational scope separately from the login mode switches.


See also