Status: Design v1
Owner: software-architect
Date: 2026-05-09 UTC
Milestone: #6 — raxx.app v1 — first non-operator user
Refs: design.md (this directory), ADR-0066, ADR-0067
All responses are JSON unless noted. All timestamps in ISO 8601 UTC. All errors follow:
{
"error": {
"code": "<machine_readable_code>",
"message": "<human readable>",
"detail": {}
}
}
/api/v1/authAll auth endpoints are public (no session required) unless noted. Feature flag: FLAG_QUEUE_V1.
POST /api/v1/auth/webauthn/register/beginPurpose: Begin passkey registration. Returns a WebAuthn credential creation challenge.
Body:
{
"email": "user@example.com",
"display_name": "Ada Lovelace",
"jurisdiction": "US"
}
Invariant check: If jurisdiction == CA-QC, returns 422 Unprocessable Entity with code: geo_block_qc.
Response 200:
{
"challenge_id": "<uuid>",
"webauthn_options": { /* PublicKeyCredentialCreationOptions */ }
}
Errors: 400 invalid_email, 409 email_already_registered, 422 geo_block_qc, 429 rate_limited.
POST /api/v1/auth/webauthn/register/completePurpose: Complete passkey registration. Creates customer record and passkey credential.
Body:
{
"challenge_id": "<uuid>",
"attestation": { /* AuthenticatorAttestationResponse */ }
}
Response 201:
{
"customer_id": "<uuid>",
"needs_email_verification": true
}
Side effects:
- Inserts queue_customers, queue_webauthn_credentials, queue_customer_roles (antlers-user).
- Emits customer_audit_events (action: customer.registered).
- Dispatches verification email via Postmark.
Errors: 400 invalid_attestation, 409 credential_already_registered, 422 challenge_expired.
POST /api/v1/auth/webauthn/login/beginPurpose: Begin usernameless passkey login. Returns a WebAuthn assertion challenge.
Body: {} (empty — discoverable credential flow)
Response 200:
{
"challenge_id": "<uuid>",
"webauthn_options": { /* PublicKeyCredentialRequestOptions, allowCredentials:[] */ }
}
POST /api/v1/auth/webauthn/login/completePurpose: Complete passkey login. Mints a session + issues a signed JWT.
Body:
{
"challenge_id": "<uuid>",
"assertion": { /* AuthenticatorAssertionResponse */ }
}
Response 200:
{
"customer_id": "<uuid>",
"jwt": "<signed RS256 JWT, 15-min TTL>",
"session_id": "<uuid>",
"expires_at": "2026-05-09T12:15:00Z"
}
Cookie: Set-Cookie: raxx_session=<opaque_token>; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=43200
JWT claims: { "sub": "<customer_id>", "sid": "<session_id>", "tier": "free|founders|pro", "roles": ["antlers-user"], "paper_first_gate": false, "fresh_until": "<ts>", "iat": <ts>, "exp": <ts> }
Side effects: Inserts queue_sessions. Emits customer_audit_events (action: session.issued).
Errors: 400 invalid_assertion, 401 credential_not_found, 403 email_not_verified, 422 challenge_expired, 429 rate_limited.
POST /api/v1/auth/sessions/refreshPurpose: Slide the session idle window. Returns a new JWT. Does not require step-up unless fresh_until has lapsed.
Auth: Session cookie or Authorization: Bearer <jwt>.
Body: {}
Response 200:
{
"jwt": "<new JWT>",
"expires_at": "2026-05-09T12:15:00Z"
}
Errors: 401 session_expired, 401 session_revoked, 403 step_up_required (if fresh_until lapsed — caller must step up first).
POST /api/v1/auth/sessions/revokePurpose: Revoke the current session (logout).
Auth: Session cookie or Authorization: Bearer <jwt>.
Body: {} or { "session_id": "<uuid>" } (to revoke a specific session; requires step-up for remote-device revocation).
Response 204: No content.
Side effects: Stamps revoked_at on queue_sessions. Emits customer_audit_events (action: session.revoked). Clears session cookie.
POST /api/v1/auth/sessions/step-upPurpose: Refresh fresh_until on the current session via a new WebAuthn assertion.
Auth: Session cookie or JWT.
Body:
{
"challenge_id": "<uuid>",
"assertion": { /* AuthenticatorAssertionResponse */ }
}
Response 200:
{
"jwt": "<new JWT with updated fresh_until>",
"fresh_until": "2026-05-09T12:10:00Z"
}
GET /api/v1/sessions (step-up required)Purpose: List all active sessions for the current customer (multi-device view).
Auth: Session cookie or JWT + fresh_until not lapsed.
Response 200:
{
"sessions": [
{
"session_id": "<uuid>",
"created_at": "...",
"last_seen_at": "...",
"expires_at": "...",
"ip_prefix": "192.168.1.0/24",
"user_agent": "Mozilla/5.0 ...",
"is_current": true
}
]
}
GET /api/v1/auth/backup-codes/generate (authenticated, step-up required)Purpose: Generate a fresh batch of 10 single-use backup codes. Invalidates any prior batch. Returns codes once only — they are never retrievable again.
Auth: Session cookie or JWT + step-up assertion within 5 min.
Response 200:
{
"batch_id": "<uuid>",
"codes": ["ABCD-1234", ...],
"generated_at": "2026-05-09T10:00:00Z"
}
Note: Codes stored as HMAC-SHA-256 only. Queue never returns them again. The response body is the only time the customer sees them.
POST /api/v1/auth/backup-codes/redeemPurpose: Redeem one backup code (unauthenticated — this is the account recovery path). Mints a session on success.
Body:
{
"email": "user@example.com",
"code": "ABCD-1234"
}
Response 200: Same shape as login/complete. Emits customer_audit_events (action: session.issued_via_backup_code).
Rate limit: 5 attempts / 60s per IP.
Errors: 400 invalid_code, 429 rate_limited.
GET /api/v1/auth/backup-codes/status (authenticated)Purpose: Check how many backup codes remain in the current batch.
Response 200:
{ "remaining": 7, "total": 10, "batch_id": "<uuid>" }
POST /api/v1/auth/email/send-verificationPurpose: (Re)send a verification code to the customer's email.
Auth: Session cookie or JWT, or { email } body for unauthenticated resend.
Body (unauthenticated): { "email": "user@example.com" }
Body (authenticated): {}
Response 202: Always — no account enumeration.
Rate limit: 3 sends / 5 min per IP, 3 sends / 5 min per email.
POST /api/v1/auth/email/verifyPurpose: Validate the 6-digit code and stamp email_verified_at.
Body:
{ "email": "user@example.com", "code": "123456" }
Response 200:
{ "verified": true, "verified_at": "2026-05-09T10:05:00Z" }
Side effects: Stamps queue_customers.email_verified_at. Emits customer_audit_events (action: email.verified).
Errors: 400 invalid_code, 422 code_expired, 429 rate_limited.
GET /api/v1/auth/email/status (authenticated)Response 200: { "verified": true, "verified_at": "..." }
/api/v1/meGET /api/v1/mePurpose: Return current customer identity, tier, effective roles, and permission set. This is the one-stop session context endpoint for Antlers and Raptor.
Auth: Session cookie or JWT.
Response 200:
{
"customer_id": "<uuid>",
"email": "user@example.com",
"display_name": "Ada Lovelace",
"email_verified": true,
"tier": "free",
"roles": ["antlers-user"],
"permissions": ["antlers:portfolio:read", "antlers:backtest:run"],
"paper_first_gate": false,
"paper_first_cycles": 3,
"session": {
"session_id": "<uuid>",
"fresh_until": "2026-05-09T10:10:00Z",
"absolute_expires_at": "2026-05-09T22:00:00Z"
}
}
Caching: Response is derived from the JWT claims + a DB lookup for role expansion. Queue caches the permission set in the session row (JSONB effective_permissions) and invalidates on any RBAC mutation for the customer.
/api/v1/rbacThese endpoints are operator-facing (Console) unless noted. Auth: session cookie or JWT with console-invite-admin permission for mutations.
POST /api/v1/rbac/grantsPurpose: Grant group membership or direct role to a user.
Body:
{
"target_user_id": "<uuid>",
"grant_type": "group_membership",
"group_id": "<uuid>",
"justification": "Onboarding support agent"
}
Pre-write invariant: Audit row in rbac_grants_audit is written before the membership row. If audit write fails, mutation is rejected (ADR-0055).
Response 201:
{ "grant_id": "<uuid>", "granted_at": "..." }
DELETE /api/v1/rbac/grants/<grant_id>Purpose: Revoke a grant by audit-row ID.
Response 204. Pre-write audit of the revoke event.
POST /api/v1/rbac/grants/ticket-scopedPurpose: Grant a temporary, ticket-scoped role that auto-revokes when the FreeScout ticket closes.
Body:
{
"target_user_id": "<uuid>",
"role": "raptor-audit-support",
"ticket_id": "<freescout_ticket_id>",
"justification": "Customer support investigation #4521"
}
Response 201:
{
"grant_id": "<uuid>",
"auto_revoke_on_ticket_close": true,
"granted_at": "..."
}
GET /api/v1/rbac/permissions/checkPurpose: Check whether the calling session (or a specified user) has a given permission.
Query params: ?permission=antlers:portfolio:read or ?user_id=<uuid>&permission=raptor-audit:read
Auth: Session cookie or JWT (for self-check); console-invite-admin for cross-user check.
Response 200:
{ "allowed": true, "resolved_via": ["antlers-user → antlers:portfolio:read"] }
/api/v1/auditThese are Queue-internal endpoints (service-to-service), not customer-facing.
POST /api/internal/v1/audit/eventPurpose: Write a customer audit event. Called by Raptor, Velvet, Reasonator.
Auth: Authorization: Bearer <QUEUE_SERVICE_TOKEN_<SERVICE>>
Body:
{
"dimension": "system_automated",
"customer_id": "<uuid>",
"actor_id": "<service_name>",
"actor_type": "service",
"action": "trade.order_placed",
"target_resource": { "type": "order", "id": "<uuid>" },
"before_state": null,
"after_state": { "status": "filled" },
"replay_uuid": "<uuid>",
"severity": "info",
"ticket_id": null
}
Response 201:
{ "event_id": "<uuid>", "event_hash": "<hmac_hex>" }
HMAC chain: Queue calls AWS KMS GenerateMac with the event payload to produce event_hash. Stores prev_event_hash from the last event for the same customer_id. Chain is per-customer.
GET /api/v1/audit/customer/<customer_id>Purpose: Read customer audit events for a given customer. RBAC-gated.
Auth: JWT with one of: antlers-audit-self (own records only), raptor-audit-support (ticket-scoped), raptor-audit-admin (all).
Query params: ?dimension=customer_self&from=<ts>&to=<ts>&limit=50
Response 200:
{
"events": [
{
"id": "<uuid>",
"dimension": "customer_self",
"action": "session.issued",
"at_utc": "2026-05-09T10:00:00Z",
"severity": "info"
}
],
"next_cursor": "<opaque>",
"total_count": 47
}
/api/v1/admin (operator-only)These endpoints require a Console-issued JWT with console-invite-admin or console-audit-user permission.
GET /api/v1/admin/customersList customers. Returns minimal PII. Pagination required.
GET /api/v1/admin/customers/<customer_id>Customer detail. Returns all fields including jurisdiction, paper_first_gate, deleted_at.
POST /api/v1/admin/customers/<customer_id>/eraseInitiate DSR erasure. Requires step-up. Emits customer.erased audit event.
GET /api/v1/admin/sessionsList active sessions across all customers (paginated, filtered by customer_id).
POST /api/v1/admin/sessions/revoke-allEmergency: revoke all customer sessions. Requires step-up + break-glass role. Emits one audit event per session revoked.