API Reference
AuthenSee auth server REST API -- sessions, personas, factors, challenges, and verification.
API Reference
The AuthenSee auth server exposes a REST API for session management, persona identity, factor enrollment, challenge generation, and proof verification. All endpoints are prefixed with /v1/.
Authentication
SDK-facing endpoints are authenticated via session token (passed in the Authorization header). Server-to-server endpoints (like session creation) use your provider secret key.
Base URL
V1 scheme model
V1 collapses human authentication to a single scheme: passkey_question_v1, backed by the passkey_question_auth circuit. The server stores one aggregate commitment per enrolment and derives factor / circuit semantics from the schemeId. Clients no longer send factorType or circuitType on /v1/verify — the server rejects those keys to prevent verifier downgrade or factor-claim swap.
The enrolment ↔ challenge ↔ verify flow:
- Enrol:
POST /v1/enrollmentswith the aggregate commitment. Server returns anenrollmentId. - Challenge:
POST /v1/challengeswith{ personaId, enrollmentId }. Server returnschallengeBytesfor the WebAuthn ceremony and thepublicInputLayoutthe verifier expects. - Verify:
POST /v1/verifywith the proof and the nullifiers (extracted from the layout the server published).
Legacy
/v1/factors*endpoints are deliberately disabled and return a 410-style error pointing at/v1/enrollments. There is no per-factor record on the server side.
POST /v1/sessions
Create a session token for the SDK. This is a server-to-server call made from your backend using your secret key. The session token scopes all subsequent SDK operations to your provider.
Authentication: Provider secret key (sk_live_... or sk_test_...) via x-api-key header.
Request
Request body
| Field | Type | Required | Description |
|---|---|---|---|
scope | "enroll" | "authenticate" | "full" | Yes | What the session can do |
externalUserId | string | No | Your user ID, surfaced in the session for hosted-pages flows |
providerSubject | string | No | Your stable alias for the persona |
callbackUrl | string (url) | No | Where the hosted flow returns the result. Validated against the provider's allowed callback origins. |
ttl | number | No | Lifetime in seconds (1–86 400, default 3 600) |
Response
| Field | Type | Description |
|---|---|---|
sessionId | string | UUID of the session row |
sessionToken | string | Token with sess_ prefix for SDK initialization |
flowCode | string | One-time flow_-prefixed code for the hosted flow (single-use) |
hostedUrl | string | Ready-to-use hosted-flow URL (https://auth.authensee.com/flow/{flowCode}) for redirect or AuthenSee.open() |
scope | string | The session scope (echoed) |
expiresAt | string | ISO 8601 expiration timestamp |
The sessionToken is for direct SDK use (your own frontend). For the hosted
flow, hand the user hostedUrl — never the raw token.
GET /v1/sessions/current
Read the current session using its session token. Unlike GET /v1/sessions/:id, this is authenticated by the session token itself (Bearer sess_…), not a provider secret key — so the hosted pages can validate a session without that provider's secret key being configured server-side.
Authentication: Session token (Authorization: Bearer sess_…)
Request
Response
Returns the same session shape as GET /v1/sessions/:id — scope, externalUserId, callbackUrl, branding/theme, and expiry — scoped to the session the token belongs to.
POST /v1/personas/identify
Associate an external user ID with an AuthenSee persona. Creates a new persona if one does not exist for this provider + external ID combination. If the user has previously enrolled via another provider, their existing persona is returned.
Authentication: Session token
Request
Request body
| Field | Type | Required | Description |
|---|---|---|---|
externalUserId | string | Yes | Your application's user ID |
isHuman | boolean | No | Default true. Set false for agent personas. Agent personas are subject to your provider policy: they can be blocked outright or rate-limited per persona (see Provider policy). |
Response
| Field | Type | Description |
|---|---|---|
personaId | string | Stable AuthenSee persona ID (UUID v7) |
personaType | string | "human" or "agent" |
enrolledFactors | string[] | Factor types already enrolled |
createdAt | string | ISO 8601 creation timestamp |
POST /v1/enrollments
Register an aggregate scheme commitment. The SDK computes the V1 `auth_commitment = Poseidon2(question_root, passkey_commitment, 0, 0)` on-device; only that single field element ever crosses the wire.
Authentication: Session token (Bearer)
Request
Request body
| Field | Type | Required | Description |
|---|---|---|---|
personaId | string (uuid) | Yes | The persona to enroll |
schemeId | "passkey_question_v1" | Yes | The only V1 scheme |
commitment | string (hex) | Yes | The aggregate scheme commitment |
Response
| Field | Type | Description |
|---|---|---|
enrolled | boolean | Whether enrollment succeeded |
enrollmentId | string (uuid) | Server-side enrollment ID — pass this to /v1/challenges |
schemeId | string | The scheme registered (echoed) |
commitment | string (hex) | The committed aggregate (echoed) |
factors | string[] | Server-derived factor list — what a successful proof attests to |
Enrollment is subject to your provider policy: a schemeId outside your configured factor combination is rejected with 400 VALIDATION_ERROR, and agent personas are rejected with 403 FORBIDDEN when your policy blocks agents. Existing enrollments are unaffected by later policy changes.
Listing enrolled factors is not part of V1. The server stores a single opaque aggregate commitment per enrolment; per-factor records do not exist. The on-device SDK is the only authoritative source for "what's enrolled on this device".
POST /v1/challenges
Generate an authentication challenge bound to an enrolment. Returns a cryptographic nonce, challengeBytes for the WebAuthn ceremony, and the public-input layout the SDK uses to extract nullifiers.
Authentication: Session token (Bearer)
Request
Request body
| Field | Type | Required | Description |
|---|---|---|---|
personaId | string (uuid) | Yes | The persona requesting authentication |
enrollmentId | string (uuid) | Yes | From the /v1/enrollments response |
action | object | No | Action-scoped proof binding (actionType + actionPayloadHash) |
Response
| Field | Type | Description |
|---|---|---|
challengeId | string (uuid) | UUID of the challenge row |
nonce | string (hex) | BN254 field element bound into the proof's challenge_field public input |
enrollmentId | string (uuid) | Echoed |
schemeId | string | The scheme bound to this challenge (e.g. passkey_question_v1) |
challengeBytes | number[32] | 32 random bytes — feed into navigator.credentials.get(...) for the WebAuthn ceremony |
publicInputLayout | object | Indices the verifier expects in the flat public-input array. The SDK uses nullifierIndices to extract the nullifier(s) it sends on /v1/verify so circuit reorderings don't require an SDK release |
factors | string[] | Server-derived factor list — what a successful proof attests to |
expiresAt | string | Challenge expiration (default TTL: 5 minutes) |
POST /v1/verify
Submit a ZK proof for verification. On success, the server returns a signed JWT.
Authentication: Session token (Bearer)
Request
Request body
| Field | Type | Required | Description |
|---|---|---|---|
challengeId | string (uuid) | Yes | From /v1/challenges |
personaId | string (uuid) | Yes | The persona being authenticated |
proof | string (base64) | Yes | The ZK proof bytes generated on-device |
publicInputs | string[] | Yes | Flat array of hex field elements. V1: 100 entries |
nullifiers | string[] | Yes | Non-zero spent nullifiers. V1: [auth_nullifier] (one entry, the value at the index the server published in challenge.publicInputLayout.nullifierIndices) |
action | object | No | Action-scoped proof binding |
No client-supplied factor / circuit labels. The server rejects
factorType,circuitType, andfactorsAttestedkeys to prevent verifier downgrade or factor-claim swap. All three are derived from theschemeIdstored alongside the challenge.
Server-side verification steps
- Look up the challenge and resolve its
schemeId - Resolve the verifier circuit + expected public-input layout from the scheme registry
- Check rate limits (per-IP, per-provider)
- Enforce the provider's agent policy — agent personas are rejected with
403 FORBIDDENwhen your policy blocks agents (the policy is read fresh, so a dashboard change applies to in-flight challenges too) - Assert strict public-input bindings (V1:
auth_commitmentat index 0,challenge_fieldat index 1,auth_nullifierat index 99; total length 100) - Check that no claimed nullifier has been spent
- Verify the ZK proof via Barretenberg UltraHonk against the scheme's pinned VK
- Atomically claim each proof's nullifier (single-use — replay-protected)
- Issue a signed JWT with the scheme's
scheme_id; consumers derive the verified factor list from the scheme registry
Success response
JWT claims
The auth-result JWT is signed with EdDSA (Ed25519), key published at /.well-known/jwks.json. Every claim appears exactly once and uses snake_case to match OIDC convention.
Standard claims (RFC 7519). Standard JWT registered claims carry the identity and lifecycle metadata. Every JWT library decodes these directly — no custom mapping needed.
| Standard claim | Carries | Example |
|---|---|---|
iss (issuer) | The auth server origin that signed the token | "https://auth.authensee.com" |
sub (subject) | The authenticated persona's ID | "01917f8a-6b3e-…" |
aud (audience) | The provider the token is intended for | "01917f8a-…" |
iat (issued at) | Unix timestamp | 1744189200 |
exp (expires at) | Unix timestamp (10 min after iat) | 1744192800 |
jti (JWT ID) | Unique token identifier — art_… prefix | "art_…" |
The persona ID and provider ID are not repeated as separate body claims — sub and aud are the canonical fields. Reading them is one line in any library:
Custom claims. Snake_case, OIDC-style.
| Claim | Type | Description |
|---|---|---|
auth_result_id | string | Server-side row ID — ar_… prefix. Use with GET /v1/auth-results/:id to retrieve the authoritative server record. |
challenge_id | string (uuid) | The challenge this proof was bound to. |
session_id | string (uuid) | The SDK session that produced the proof. Omitted in skipauth/test contexts. |
external_user_id | string | The provider's own user ID for the persona. Omitted if not bound. |
persona_type | "human" | "agent" | Same data as the SDK's persona record. |
scheme_id | string | Primary key for what the proof attested to. For V1 the only value is "passkey_question_v1". Consumers map this → factor list via the scheme registry / OpenAPI spec. |
auth_time | number | Unix timestamp of the underlying authentication (mirrors iat for V1 since auth and token issuance are atomic). |
ℹ️ No
factors_verifiedclaim. Earlier versions of the JWT carried both afactors_verifiedarray AND ascheme_id. The list was redundant — every scheme has a fixed factor set known to the server, and consumers can derive it fromscheme_id. The token is now smaller, the contract is DRY, and adding new schemes never requires re-issuing JWTs with different factor shapes.
ℹ️ No
personaId/providerIdbody claims. The standardsub/audclaims carry the same data. Earlier versions emitted both for convenience; that was removed for the same DRY reason.
For agent personas the JWT additionally includes an agent_id claim alongside persona_type: "agent".
Error response
Error codes
| Code | HTTP Status | Description |
|---|---|---|
INVALID_PROOF | 400 | ZK proof verification failed |
FACTOR_NOT_ENROLLED | 400 | Required factor is not enrolled |
CHALLENGE_EXPIRED | 400 | Challenge TTL exceeded |
MERKLE_ROOT_STALE | 400 | Proof references an outdated Merkle root |
NULLIFIER_SPENT | 400 | Proof has already been used (replay detected) |
RATE_LIMITED | 429 | Too many requests — including an agent persona exceeding your provider policy's per-agent budget |
UNAUTHORIZED | 401 | Invalid or expired session token |
FORBIDDEN | 403 | Access denied by policy — e.g. an agent persona when your provider policy blocks agents |
Hosted pages endpoints
Hosted pages reuse POST /v1/sessions — there's no separate /v1/hosted/sessions. After minting a session, send the user to the returned hostedUrl:
The one-time flowCode is redeemed once via POST /v1/hosted/flow-code/redeem, which returns the sessionToken the hosted pages hold in memory; the pages then validate the session via GET /v1/sessions/current and run the V1 enrol or auth flow, redirecting the user to the provider's callbackUrl (with a one-time authResultCode) on success. See the hosted pages guide and the embed guide for the full integration flow.
Demo session shortcut
For demos and integration prototyping, the hosted pages also expose a self-service launcher at https://auth.authensee.com/. Click "Demo session →" to mint a fresh session against your local auth server (no callback configured), or visit /demo/{externalUserId} for a persistent demo URL where the persona is reused across visits.