SDK Reference
@humanauth/sdk is the agent-side half of HumanAuth. Your agent asks a human (or a group) to authorize a specific action, waits for the decision, and — on approval — pulls a verifiable receipt whose JWS it hands to your resource server for enforcement with @humanauth/verifier.
The whole surface is one class, HumanAuth, plus the AuthorizationHandle it returns and three admin namespaces (humans, groups, receipts).
Installation
Section titled “Installation”npm install @humanauth/sdkThe package ships as ESM with bundled type declarations and has no runtime dependencies.
Authenticating
Section titled “Authenticating”import { HumanAuth } from "@humanauth/sdk";
const auth = new HumanAuth({ apiKey: process.env.HUMANAUTH_API_KEY! });The constructor takes a single options object. apiKey is required — the constructor throws apiKey is required if it is missing or empty. Every request is sent with an Authorization: Bearer <apiKey> header.
HumanAuthOptions
| Field | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Your tenant API key (ha_live_… / ha_test_…). |
baseUrl | string | No | Platform base URL. Defaults to https://api.humanauth.ai. Point this at your own host for self-managed deployments. |
fetch | typeof fetch | No | Custom fetch implementation. Defaults to the global fetch. Useful for testing or for runtimes that need an explicit fetch. |
Quickstart
Section titled “Quickstart”The end-to-end flow: request authorization, wait for the human, then hand the receipt to your resource server.
import { HumanAuth } from "@humanauth/sdk";
const auth = new HumanAuth({ apiKey: process.env.HUMANAUTH_API_KEY! });
// 1. Ask a human to authorize one specific action.const handle = await auth.authorize("hu_karthick_7x2f", { action: "github:delete_repo", description: "Delete acme/legacy-service — no longer deployed", severity: "high",});
// 2. Block until the human decides (or the request times out).const result = await handle.wait();if (result.result !== "approved") { throw new Error(`Not authorized (${result.status})`);}
// 3. Pull the receipt JWS and hand it to your resource server to enforce.const jws = await auth.receipts.getJws(result.receipt_id!);// Send `jws` as the `x-humanauth-receipt` header to your service, which calls// verifier.requireReceipt(jws, { action, plan, idempotencyKey }) before acting.authorize(target, options)
Section titled “authorize(target, options)”authorize(target: string, options: AuthorizeOptions): Promise<AuthorizationHandle>Creates an authorization request and returns an AuthorizationHandle immediately — it does not block. Call .wait() on the handle to await the decision.
The target selects who is asked:
- A human id (
hu_…) asks that one person. - A group id (
grp_…) asks the group under its policy.
The prefix decides the routing; there is no separate flag.
AuthorizeOptions
| Field | Type | Required | Description |
|---|---|---|---|
action | string | Yes | Canonical service:operation label for the action being authorized (e.g. github:delete_repo, transfer:create). The verifier enforces this exact string, so keep it stable and specific. |
description | string | No | Human-readable summary shown to the approver. |
severity | "low" | "medium" | "high" | "critical" | No | Risk level surfaced in the approval UI. |
intent | "authorize" | "collect" | "inform" | No | The kind of interaction requested — approve/deny, gather input, or notify only. |
ttl | number | No | Seconds the request stays open before it expires. Omit to use the platform default. |
policy | GroupPolicy | No | Group targets only. Inline policy override for this one request (ignored for single-human targets). |
callbackUrl | string | No | Webhook URL notified when the request is decided. |
callbackSecret | string | No | HMAC secret used to sign the callbackUrl delivery. |
GroupPolicy
interface GroupPolicy { type: "quorum" | "unanimous"; quorum?: number; // number of approvals required when type is "quorum"}AuthorizationHandle
Section titled “AuthorizationHandle”Returned by authorize(). Wraps the in-flight request so you can await, poll, or cancel it.
Properties
| Property | Type | Description |
|---|---|---|
requestId | string | The platform request id — poll or cancel it yourself if you don’t use wait(). |
targetType | "human" | "group" | Whether the request went to a single human or a group. |
expiresAt | Date | When the request expires. |
wait(options?)
Section titled “wait(options?)”wait(options?: WaitOptions): Promise<RequestDetail>Polls the platform until the request reaches a terminal state — decided, expired, or cancelled — and resolves with the RequestDetail shown below. Throws HumanAuthTimeoutError if the timeout elapses first (the request itself keeps running platform-side; only the local wait gives up).
WaitOptions
| Field | Type | Description |
|---|---|---|
pollIntervalMs | number | Interval between status polls. Defaults to 1000. |
timeoutMs | number | How long to wait before throwing. Defaults to the request TTL plus 5 seconds. |
const result = await handle.wait({ pollIntervalMs: 2000, timeoutMs: 120_000 });
switch (result.result) { case "approved": // result.receipt_id is set — fetch the JWS and proceed. break; case "denied": // result.decisions carries each approver's reason. break; case "expired": // no one decided in time. break;}cancel()
Section titled “cancel()”cancel(): Promise<void>Cancels the request while it is still pending or delivered.
RequestDetail
interface RequestDetail { request_id: string; status: "pending" | "delivered" | "decided" | "expired" | "cancelled"; action: string; target_type: "human" | "group"; target_id: string; ttl: number; created_at: string; decided_at: string | null; result?: "approved" | "denied" | "expired"; receipt_id?: string; // present once an approved request has minted a receipt decisions: DecisionDetail[];}
interface DecisionDetail { human_id: string; decision: "approved" | "denied"; device_id: string; decided_at: string; reason: string | null;}Working with a request id directly
Section titled “Working with a request id directly”If you persist handle.requestId and want to resume elsewhere, the client exposes the same operations by id:
const detail = await auth.getRequest("req_abc123"); // → RequestDetailawait auth.cancelRequest("req_abc123");Receipts
Section titled “Receipts”auth.receipts reads the receipts your approved requests mint. A receipt’s JWS is the offline-verifiable artifact your resource server enforces — see the Verifier SDK.
receipts.getJws(receiptId)
Section titled “receipts.getJws(receiptId)”getJws(receiptId: string): Promise<string>The common case: return just the compact v2 JWS, ready to hand to @humanauth/verifier. Throws HumanAuthError with code: "no_v2_receipt" if the receipt predates the v2 cutover and has no JWS to verify offline.
const jws = await auth.receipts.getJws(result.receipt_id!);receipts.get(receiptId)
Section titled “receipts.get(receiptId)”get(receiptId: string): Promise<Receipt>Fetches the full receipt record.
interface Receipt { receipt_id: string; request_id: string; tenant_id: string; result: "approved" | "denied" | "expired"; jws?: string; // v2 compact JWS — hand this to @humanauth/verifier plan_hash?: string; issued_at?: number; expires_at?: number; approvers_count?: number; v?: 2; // Legacy v1 receipts (pre-v2) instead carry: // action, policy, decisions, decided_at, ha_signature}receipts.list(options?)
Section titled “receipts.list(options?)”list(options?: ListReceiptsOptions): Promise<ListReceiptsResponse>Lists receipts for the tenant, newest first, with cursor pagination — the audit view.
const page = await auth.receipts.list({ since: "2026-04-01", limit: 50 });for (const r of page.receipts) { console.log(r.receipt_id, r.action, r.result, r.decided_at);}if (page.has_more) { const next = await auth.receipts.list({ cursor: page.cursor! });}interface ListReceiptsOptions { since?: string; // ISO 8601 lower bound on decided_at limit?: number; cursor?: string; // opaque cursor from a previous page}
interface ListReceiptsResponse { receipts: Array<{ receipt_id: string; request_id: string; action: string; result: "approved" | "denied" | "expired"; decided_at: string; }>; has_more: boolean; cursor: string | null;}Humans
Section titled “Humans”auth.humans manages the roster of people in your tenant.
// Invite someone (role defaults server-side; pass "admin" or "member").const invited = await auth.humans.invite({ email: "alice@example.com", role: "admin" });// → { human_id, email, status, is_new }
// List the roster.const roster = await auth.humans.list(); // → HumanRosterEntry[]
// Remove a human from the tenant.await auth.humans.remove("hu_abc123");interface InviteHumanRequest { email: string; role?: "admin" | "member";}
interface InviteHumanResponse { human_id: string; email: string; status: string; is_new: boolean; // false if the email was already on the roster}
interface HumanRosterEntry { human_id: string; email: string; status: string; role: "admin" | "member"; devices: number; added_at: string;}Groups
Section titled “Groups”auth.groups manages approval groups and their policies. Pass a group id (grp_…) to authorize() to route a request through the group.
// Create a group that needs 2 of 3 approvals.const { group_id } = await auth.groups.create({ name: "deployers", members: ["hu_a", "hu_b", "hu_c"], policy: { type: "quorum", quorum: 2 },});
// List groups.const groups = await auth.groups.list(); // → GroupDetail[]
// Delete a group.await auth.groups.delete("grp_xyz");interface CreateGroupRequest { name: string; members: string[]; // human ids policy: GroupPolicy; // { type: "quorum" | "unanimous", quorum? }}
interface GroupDetail { group_id: string; name: string; policy_type: "quorum" | "unanimous"; policy_quorum?: number;}Error types
Section titled “Error types”Both error classes are exported from the package.
HumanAuthError
Section titled “HumanAuthError”Thrown for any non-2xx API response.
| Property | Type | Description |
|---|---|---|
status | number | HTTP status code from the platform. |
code | string | Stable machine-readable error code (e.g. forbidden, no_v2_receipt). |
message | string | Human-readable detail. |
HumanAuthTimeoutError
Section titled “HumanAuthTimeoutError”Subclass of HumanAuthError, thrown by handle.wait() when the local timeout elapses before a terminal decision. Its status is 408 and its code is "timeout".
import { HumanAuth, HumanAuthError, HumanAuthTimeoutError } from "@humanauth/sdk";
try { const result = await handle.wait({ timeoutMs: 120_000 }); // ...} catch (err) { if (err instanceof HumanAuthTimeoutError) { await handle.cancel(); // no decision in the window — give up cleanly } else if (err instanceof HumanAuthError) { console.error(`API error ${err.status} (${err.code}): ${err.message}`); } else { throw err; }}Exports
Section titled “Exports”Everything the package exports, all verifiable against packages/sdk/src:
- Classes —
HumanAuth,AuthorizationHandle,HumanAuthError,HumanAuthTimeoutError. - Types —
HumanAuthOptions,AuthorizeOptions,AuthorizeResponse,RequestDetail,DecisionDetail,Receipt,HumanRosterEntry,InviteHumanRequest,InviteHumanResponse,CreateGroupRequest,GroupDetail,GroupPolicy,ListReceiptsOptions,ListReceiptsResponse,WaitOptions. - Enums (string unions) —
Plan,Intent,Severity,RequestStatus,Decision,RequestResult,PolicyType,TenantRole.
Non-TypeScript languages
Section titled “Non-TypeScript languages”Building in Python, Go, Java, Ruby, Rust, or another language? The platform publishes an OpenAPI 3.1 specification you can use to generate a typed client. Receipt verification is offline JWS — the Verifier SDK documents how to reproduce the verification fence in any language.
See also
Section titled “See also”- Verifier SDK — the enforcement half, run at the resource server; where the receipt JWS goes.
- CLI Reference — the same operations as scriptable commands.
- Webhook Subscriptions — tenant-level delivery for decided requests.