Raxx · internal docs

internal · gated ↑ index

Queue — API Contract v1

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": {}
  }
}

Auth prefix: /api/v1/auth

All auth endpoints are public (no session required) unless noted. Feature flag: FLAG_QUEUE_V1.


WebAuthn Registration

POST /api/v1/auth/webauthn/register/begin

Purpose: 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/complete

Purpose: 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.


WebAuthn Login

POST /api/v1/auth/webauthn/login/begin

Purpose: 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/complete

Purpose: 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.


Sessions

POST /api/v1/auth/sessions/refresh

Purpose: 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/revoke

Purpose: 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-up

Purpose: 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
    }
  ]
}

Backup / Recovery Codes

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/redeem

Purpose: 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>" }

Email Verification

POST /api/v1/auth/email/send-verification

Purpose: (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/verify

Purpose: 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": "..." }


Customer prefix: /api/v1/me

GET /api/v1/me

Purpose: 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.


RBAC prefix: /api/v1/rbac

These endpoints are operator-facing (Console) unless noted. Auth: session cookie or JWT with console-invite-admin permission for mutations.

POST /api/v1/rbac/grants

Purpose: 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-scoped

Purpose: 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/check

Purpose: 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"] }

Audit prefix: /api/v1/audit

These are Queue-internal endpoints (service-to-service), not customer-facing.

POST /api/internal/v1/audit/event

Purpose: 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
}

Admin prefix: /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/customers

List 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>/erase

Initiate DSR erasure. Requires step-up. Emits customer.erased audit event.

GET /api/v1/admin/sessions

List active sessions across all customers (paginated, filtered by customer_id).

POST /api/v1/admin/sessions/revoke-all

Emergency: revoke all customer sessions. Requires step-up + break-glass role. Emits one audit event per session revoked.