Skip to content

Integrator Cookbook

Polyglot · Six recipes · Production-shaped

The complete integrator journey — authorize, verify, observe, export, investigate — written against the live API at api.humanauth.ai. Every recipe ships in the four languages most backends are written in. If you’re on TypeScript, the @humanauth/sdk and @humanauth/verifier packages are the higher-leverage door; everything else is plain HTTP.

  • cURL
  • TypeScript
  • Python
  • Java

The journey · click any node

HA_API_KEY
Tenant API key. Dashboard → Settings → API Keys. Sent as Authorization: Bearer $HA_API_KEY.
HA_TENANT_ID
The receipt aud claim your verifier expects. From GET /v1/auth/me or the dashboard.
HA_BASE
https://api.humanauth.ai in prod, http://localhost:8787 against wrangler dev.
Timestamps
Seconds since epoch unless suffixed _ms.
01 · Authorize

1 — Send your first authorization request

Section titled “1 — Send your first authorization request”

Outcome A pending request_id and a push notification on the human’s phone, within milliseconds.

POST /v1/authorize is the agent → human handoff. The platform fans the request out to all registered devices for the target human (or group), waits for the response, and (on approval) mints a Receipt v2 JWS. This call returns immediately with a request_id; the receipt arrives later via webhook or GET /v1/receipts/{id}.

Terminal window
curl -X POST "$HA_BASE/v1/authorize" \
-H "Authorization: Bearer $HA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"human_id": "hu_a1b2c3d4e5f6a7b8",
"action": "github:delete_repo",
"description": "Delete acme/legacy-api",
"intent": "authorize",
"severity": "high",
"ttl": 300,
"plan": {
"repo": "acme/legacy-api",
"confirm_string": "legacy-api"
}
}'

The response is 202 Accepted because the human hasn’t decided yet. Pick one of:

  • Subscribe to request.decided webhooks (Recipe 3) — the recommended path.
  • Poll GET /v1/requests/{request_id} — fine for short-lived scripts; not for production agents.
  • Wait on the SSE stream (Recipe 6) — useful for dashboards.
02 · Verify

Outcome A typed verified receipt — or a typed error. The action does not execute unless five invariants hold.

This is the trust boundary that makes “no receipt, no execution” real. Verification is offline — no platform round-trip on the hot path, only a cached JWKS fetch (default 1h). The recipe enforces:

  1. JWS signature against a key in the platform’s JWKS.
  2. aud equals your tenant ID, iss equals https://api.humanauth.ai, exp is in the future.
  3. ha.plan_hash byte-matches sha256(rfc8785(your_plan)) — guarantees the human approved exactly this plan.
  4. action matches what you’re about to execute.
  5. (jti, idempotency_key) claimed atomically against your replay store.

Use the first-party verifier — it bakes the plan-hash check, JWKS caching, and atomic replay reservation into one call.

import { HumanAuthVerifier } from "@humanauth/verifier";
import { RedisReplayStore } from "@humanauth/verifier/stores/redis";
import Redis from "ioredis";
const verifier = new HumanAuthVerifier({
audience: process.env.HA_TENANT_ID!,
jwksUri: `${process.env.HA_BASE}/.well-known/jwks.json`,
replayStore: new RedisReplayStore(new Redis(process.env.REDIS_URL!)),
});
const verified = await verifier.requireReceipt(receiptJws, {
action: "github:delete_repo",
plan: { repo: "acme/legacy-api", confirm_string: "legacy-api" },
idempotencyKey: req.headers["idempotency-key"] as string,
});
// verified.subject — huid of the approver
// verified.approvers — [{ device_id, assurance, decided_at }]
// verified.replay — true on idempotent retry of the same (jti, idem)

See the Verifier guide for typed errors, Express middleware, MCP tool wrapper, and replay-store options.

03 · Observe

3 — Subscribe to webhooks and verify the signature

Section titled “3 — Subscribe to webhooks and verify the signature”

Outcome Every decision and every report arrives at your endpoint, HMAC-signed, deduped, retried.

Subscriptions are the canonical integration pattern for downstream systems — SIEM, dashboards, audit sinks. Two events ship in v1: request.decided (approved/denied/expired) and request.reported (a user flagged the request as suspicious).

Terminal window
curl -X POST "$HA_BASE/v1/webhooks/subscriptions" \
-H "Authorization: Bearer $HA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.acme.com/humanauth",
"events": ["request.decided", "request.reported"],
"description": "Production approval pipeline"
}'
# Response includes "secret": "whsec_…" — store it now, it is returned only once.

HMAC-SHA256 over <timestamp>.<body> with the raw secret. 300-second replay window. Dedupe on X-HumanAuth-Event-Id.

import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyHumanAuthWebhook(
headers: Record<string, string>,
rawBody: string,
secret: string,
): boolean {
const sigHeader = headers["x-humanauth-signature"];
if (!sigHeader) return false;
const parts = Object.fromEntries(sigHeader.split(",").map((p) => p.split("=")));
const ts = parseInt(parts.t, 10);
if (Number.isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > 300) return false;
const expected = createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
return a.length === b.length && timingSafeEqual(a, b);
}

After verifying, dedupe on X-HumanAuth-Event-Id (the same logical event may retry under a new X-HumanAuth-Delivery-Id). See the Webhooks guide for retry semantics and the rotate-secret overlap window.

04 · Export

Outcome An RFC 4180 CSV stream of every audit event matching your filter — up to 100 000 rows per pull.

GET /v1/audit with Accept: text/csv streams every audit row matching your filter (up to 100 000) as RFC 4180 CSV. Useful for compliance handoffs, quarterly reviews, and offline analysis. Auth is the WorkOS dashboard session, not the tenant API key.

Terminal window
# The session cookie or bearer is from dashboard sign-in (WorkOS).
curl "$HA_BASE/v1/audit?event_type=api_key.created&since=2026-01-01" \
-H "Accept: text/csv" \
-H "Authorization: Bearer $HA_SESSION_JWT" \
-o audit-2026-q1.csv
# If the file ends with "# truncated at 100000 rows", split your filter
# (e.g. by month) and re-run for each window.

For paginated JSON instead of CSV — when you want to ingest into a SIEM rather than archive — drop the Accept: text/csv header and walk next_cursor:

Terminal window
curl "$HA_BASE/v1/audit?limit=100&cursor=$NEXT_CURSOR" \
-H "Authorization: Bearer $HA_SESSION_JWT"

CSV header is created_at,event_id,event_type,human_id,api_key_id,resource_type,resource_id,outcome,metadata. Fields with ,, ", CR, or LF are RFC 4180 quoted.

05 · Investigate

5 — Investigate an authorization incident

Section titled “5 — Investigate an authorization incident”

Outcome The full forensic picture for a single request_id — request shape, decisions, receipt, webhook delivery history.

Something looks wrong — a payment went through that shouldn’t have, or a user reported a request as phishing. This recipe fans out from a single request_id to the full forensic picture: request shape, per-approver decisions, the signed receipt, and webhook delivery history.

Terminal window
REQ=req_01HZQR3F7K8N2P5T9V1X3Y5Z7A
AUTH="-H Authorization:Bearer $HA_API_KEY"
# 1. The request itself — current state, decisions array, target, plan_hash.
curl "$HA_BASE/v1/requests/$REQ" $AUTH
# 2. The receipt JWS — if the request was approved/denied.
curl "$HA_BASE/v1/receipts/$REQ" $AUTH
# 3. Webhook deliveries that touched this request.
SUB=whsub_01HZQR3F7K8N2P5T9V1X3Y5Z7A
curl "$HA_BASE/v1/webhooks/subscriptions/$SUB/deliveries?event_type=request.decided" $AUTH
# 4. (Dashboard-auth) Audit events around the request time window.
curl "$HA_BASE/v1/audit?resource_id=$REQ" \
-H "Authorization: Bearer $HA_SESSION_JWT"

What to look for in each:

  • GET /v1/requests/{id}status (approved/denied/expired/cancelled), decisions[] (per-human, with assurance level), plan_hash, policy_id, rule_satisfied. If plan_hash doesn’t match what your backend hashed, the approval was for a different plan than what executed — that’s the incident root cause.
  • GET /v1/receipts/{id} — the JWS your backend should have received. Decode it (any JWT library) and compare ha.approvers[].device_id against the human’s registered devices.
  • Webhook deliveriesattempt, response_code, response_body (1024-byte truncated). Confirms whether your receiver actually got the event or whether it stalled.
  • Audit events filtered by resource_id — every read, write, redelivery, and policy decision the platform recorded against this request.

If the user reported the request as phishing, also pull GET /v1/requests/{id} for the report block and check whether the reporter_huid matches the request’s target_id.

06 · Tail

Outcome A live SSE stream of every tenant-scoped event, with a documented catch-up path for reconnects.

GET /v1/events is a Server-Sent Events stream of audit-significant events for the active tenant. Use it for live dashboards, on-call incident triage, or piping into stdout-tailing log shippers. Heartbeat every 25s, max lifetime 1h — reconnect and re-fetch missed events via GET /v1/audit?cursor=….

Terminal window
# Stream all event types. Filter with ?stream=request.decided,policy.created
curl -N "$HA_BASE/v1/events" \
-H "Authorization: Bearer $HA_SESSION_JWT" \
-H "Accept: text/event-stream"

Each frame is a standard SSE block:

event: request.decided
id: evt_8c9af2…
data: {"request_id":"req_…","action":"payments:transfer","result":"approved",…}

On reconnect, the dashboard pattern is: remember the last event_id you processed, then catch up via GET /v1/audit?cursor=<base64url of {created_at, event_id}> before resuming the SSE stream.

  • Verifier guide — typed errors, Express middleware, MCP tool wrapper, replay-store adapters.
  • Webhooks guide — retry semantics, signature rotation, debugging deliveries.
  • OpenAPI spec — every endpoint, every parameter, every response shape. Codegen typed clients in Go, Ruby, Rust, C#, etc.
  • Security model — threat model and cryptographic primitives behind these recipes.