AuthenSeeDocs

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.

Authorization: Bearer sess_abc123def456    # SDK endpoints
Authorization: Bearer sk_live_your_key     # Server-to-server endpoints

Base URL

https://api.authensee.com/v1

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:

  1. Enrol: POST /v1/enrollments with the aggregate commitment. Server returns an enrollmentId.
  2. Challenge: POST /v1/challenges with { personaId, enrollmentId }. Server returns challengeBytes for the WebAuthn ceremony and the publicInputLayout the verifier expects.
  3. Verify: POST /v1/verify with 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

curl -X POST https://api.authensee.com/v1/sessions \
  -H "x-api-key: sk_live_your_secret_key" \
  -H "Content-Type: application/json" \
  -d '{
    "scope": "full",
    "externalUserId": "user_12345",
    "callbackUrl": "https://myapp.com/auth/callback",
    "ttl": 3600
  }'

Request body

FieldTypeRequiredDescription
scope"enroll" | "authenticate" | "full"YesWhat the session can do
externalUserIdstringNoYour user ID, surfaced in the session for hosted-pages flows
providerSubjectstringNoYour stable alias for the persona
callbackUrlstring (url)NoWhere the hosted flow returns the result. Validated against the provider's allowed callback origins.
ttlnumberNoLifetime in seconds (1–86 400, default 3 600)

Response

{
  "sessionId": "01917f8a-…",
  "sessionToken": "sess_abc123def456",
  "flowCode": "flow_8s2k…",
  "hostedUrl": "https://auth.authensee.com/flow/flow_8s2k…",
  "scope": "full",
  "expiresAt": "2026-04-09T12:00:00Z"
}
FieldTypeDescription
sessionIdstringUUID of the session row
sessionTokenstringToken with sess_ prefix for SDK initialization
flowCodestringOne-time flow_-prefixed code for the hosted flow (single-use)
hostedUrlstringReady-to-use hosted-flow URL (https://auth.authensee.com/flow/{flowCode}) for redirect or AuthenSee.open()
scopestringThe session scope (echoed)
expiresAtstringISO 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

curl https://api.authensee.com/v1/sessions/current \
  -H "Authorization: Bearer sess_abc123def456"

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

curl -X POST https://api.authensee.com/v1/personas/identify \
  -H "Authorization: Bearer sess_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "externalUserId": "user_12345",
    "isHuman": true
  }'

Request body

FieldTypeRequiredDescription
externalUserIdstringYesYour application's user ID
isHumanbooleanNoDefault 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

{
  "personaId": "01917f8a-6b3e-7d4c-9a1f-2e5b8c4d6f0a",
  "personaType": "human",
  "enrolledFactors": ["security_questions", "passkey"],
  "createdAt": "2026-03-15T10:30:00Z"
}
FieldTypeDescription
personaIdstringStable AuthenSee persona ID (UUID v7)
personaTypestring"human" or "agent"
enrolledFactorsstring[]Factor types already enrolled
createdAtstringISO 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

curl -X POST https://api.authensee.com/v1/enrollments \
  -H "Authorization: Bearer sess_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "personaId": "01917f8a-6b3e-7d4c-9a1f-2e5b8c4d6f0a",
    "schemeId": "passkey_question_v1",
    "commitment": "0x1a2b3c4d5e6f..."
  }'

Request body

FieldTypeRequiredDescription
personaIdstring (uuid)YesThe persona to enroll
schemeId"passkey_question_v1"YesThe only V1 scheme
commitmentstring (hex)YesThe aggregate scheme commitment

Response

{
  "enrolled": true,
  "enrollmentId": "01917f8a-cccc-7d4c-9a1f-dddddddddddd",
  "schemeId": "passkey_question_v1",
  "commitment": "0x1a2b3c4d5e6f...",
  "factors": ["security_questions", "passkey"]
}
FieldTypeDescription
enrolledbooleanWhether enrollment succeeded
enrollmentIdstring (uuid)Server-side enrollment ID — pass this to /v1/challenges
schemeIdstringThe scheme registered (echoed)
commitmentstring (hex)The committed aggregate (echoed)
factorsstring[]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

curl -X POST https://api.authensee.com/v1/challenges \
  -H "Authorization: Bearer sess_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "personaId": "01917f8a-6b3e-7d4c-9a1f-2e5b8c4d6f0a",
    "enrollmentId": "01917f8a-cccc-7d4c-9a1f-dddddddddddd"
  }'

Request body

FieldTypeRequiredDescription
personaIdstring (uuid)YesThe persona requesting authentication
enrollmentIdstring (uuid)YesFrom the /v1/enrollments response
actionobjectNoAction-scoped proof binding (actionType + actionPayloadHash)

Response

{
  "challengeId": "01917f8a-…",
  "nonce": "0x1abc...",
  "enrollmentId": "01917f8a-cccc-7d4c-9a1f-dddddddddddd",
  "schemeId": "passkey_question_v1",
  "challengeBytes": [12, 240, 91, "..."],
  "publicInputLayout": {
    "authCommitmentIndex": 0,
    "challengeFieldIndex": 1,
    "nullifierIndices": [99],
    "totalLength": 100
  },
  "factors": ["security_questions", "passkey"],
  "expiresAt": "2026-04-09T10:05:00Z"
}
FieldTypeDescription
challengeIdstring (uuid)UUID of the challenge row
noncestring (hex)BN254 field element bound into the proof's challenge_field public input
enrollmentIdstring (uuid)Echoed
schemeIdstringThe scheme bound to this challenge (e.g. passkey_question_v1)
challengeBytesnumber[32]32 random bytes — feed into navigator.credentials.get(...) for the WebAuthn ceremony
publicInputLayoutobjectIndices 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
factorsstring[]Server-derived factor list — what a successful proof attests to
expiresAtstringChallenge 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

curl -X POST https://api.authensee.com/v1/verify \
  -H "Authorization: Bearer sess_abc123def456" \
  -H "Content-Type: application/json" \
  -d '{
    "challengeId": "01917f8a-…",
    "personaId": "01917f8a-6b3e-7d4c-9a1f-2e5b8c4d6f0a",
    "proof": "<base64>",
    "publicInputs": ["0x...", "0x...", "..."],
    "nullifiers": ["0xauth_nullifier..."]
  }'

Request body

FieldTypeRequiredDescription
challengeIdstring (uuid)YesFrom /v1/challenges
personaIdstring (uuid)YesThe persona being authenticated
proofstring (base64)YesThe ZK proof bytes generated on-device
publicInputsstring[]YesFlat array of hex field elements. V1: 100 entries
nullifiersstring[]YesNon-zero spent nullifiers. V1: [auth_nullifier] (one entry, the value at the index the server published in challenge.publicInputLayout.nullifierIndices)
actionobjectNoAction-scoped proof binding

No client-supplied factor / circuit labels. The server rejects factorType, circuitType, and factorsAttested keys to prevent verifier downgrade or factor-claim swap. All three are derived from the schemeId stored alongside the challenge.

Server-side verification steps

  1. Look up the challenge and resolve its schemeId
  2. Resolve the verifier circuit + expected public-input layout from the scheme registry
  3. Check rate limits (per-IP, per-provider)
  4. Enforce the provider's agent policy — agent personas are rejected with 403 FORBIDDEN when your policy blocks agents (the policy is read fresh, so a dashboard change applies to in-flight challenges too)
  5. Assert strict public-input bindings (V1: auth_commitment at index 0, challenge_field at index 1, auth_nullifier at index 99; total length 100)
  6. Check that no claimed nullifier has been spent
  7. Verify the ZK proof via Barretenberg UltraHonk against the scheme's pinned VK
  8. Atomically claim each proof's nullifier (single-use — replay-protected)
  9. Issue a signed JWT with the scheme's scheme_id; consumers derive the verified factor list from the scheme registry

Success response

{
  "token": "eyJhbGci...",
  "personaId": "01917f8a-…",
  "providerId": "01917f8a-…",
  "personaType": "human",
  "factorsVerified": ["security_questions", "passkey"],
  "challengeId": "01917f8a-…",
  "actionType": "auth"
}

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.

{
  "iss": "https://auth.authensee.com",
  "sub": "01917f8a-6b3e-7d4c-9a1f-2e5b8c4d6f0a",
  "aud": "01917f8a-…",
  "iat": 1744189200,
  "exp": 1744192800,
  "jti": "art_…",
  "auth_result_id": "ar_…",
  "challenge_id": "…",
  "session_id": "…",
  "external_user_id": "user_12345",
  "persona_type": "human",
  "scheme_id": "passkey_question_v1",
  "auth_time": 1744189200
}

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 claimCarriesExample
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 timestamp1744189200
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:

import { jwtVerify, createRemoteJWKSet } from 'jose';
 
const jwks = createRemoteJWKSet(new URL('https://auth.authensee.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(token, jwks, {
  issuer: 'https://auth.authensee.com',
  audience: yourProviderId,
});
 
console.log(payload.sub);          // persona ID
console.log(payload.aud);          // provider ID
console.log(payload.scheme_id);    // 'passkey_question_v1'
console.log(payload.persona_type); // 'human' | 'agent'

Custom claims. Snake_case, OIDC-style.

ClaimTypeDescription
auth_result_idstringServer-side row ID — ar_… prefix. Use with GET /v1/auth-results/:id to retrieve the authoritative server record.
challenge_idstring (uuid)The challenge this proof was bound to.
session_idstring (uuid)The SDK session that produced the proof. Omitted in skipauth/test contexts.
external_user_idstringThe 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_idstringPrimary 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_timenumberUnix timestamp of the underlying authentication (mirrors iat for V1 since auth and token issuance are atomic).

ℹ️ No factors_verified claim. Earlier versions of the JWT carried both a factors_verified array AND a scheme_id. The list was redundant — every scheme has a fixed factor set known to the server, and consumers can derive it from scheme_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 / providerId body claims. The standard sub / aud claims 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

{
  "valid": false,
  "error": {
    "code": "INVALID_PROOF",
    "message": "ZK proof verification failed"
  }
}

Error codes

CodeHTTP StatusDescription
INVALID_PROOF400ZK proof verification failed
FACTOR_NOT_ENROLLED400Required factor is not enrolled
CHALLENGE_EXPIRED400Challenge TTL exceeded
MERKLE_ROOT_STALE400Proof references an outdated Merkle root
NULLIFIER_SPENT400Proof has already been used (replay detected)
RATE_LIMITED429Too many requests — including an agent persona exceeding your provider policy's per-agent budget
UNAUTHORIZED401Invalid or expired session token
FORBIDDEN403Access 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:

https://auth.authensee.com/flow/{flowCode}

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.