# Get Actions Source: https://docs.humanos.tech/api-reference/latest/actions/get-actions /versions/2026-09-01.yaml get /actions Retrieve a paginated list of actions for the authenticated organization. Only published versions are ever exposed here; drafts are hidden from public clients. Each action includes up to its 10 most recent published versions (newest first); use GET /actions/version/{id} to page through the full version history. Supports `search` (case-insensitive partial match on the action name) and pagination via `pageIndex` and `pageSize`. # Get Versions Source: https://docs.humanos.tech/api-reference/latest/actions/get-versions /versions/2026-09-01.yaml get /actions/version/{id} Retrieve a paginated list of an action's published versions, newest first, for the action identified by the `id` path parameter. Each version includes its full JSON `content`, its `digestSRI`, and publication metadata; drafts are hidden from public clients. Supports pagination via `pageIndex` and `pageSize`. If the action does not exist or does not belong to the authenticated organization, an empty page is returned (`data: []`, `totalPages: 0`) rather than a 404. # List Approvals Source: https://docs.humanos.tech/api-reference/latest/approval/list-approvals /versions/2026-09-01.yaml get /approvals Retrieve a paginated list of your approvals. Approvals can be of type `FORM`, `DOCUMENT`, `JSON`, or `POLICY`. Filter with the optional query parameters: `search` (case-insensitive match on the approval name), `active` (active or inactive approvals only), `types` (one or more resource types), and `internalId` (your own identifier for a specific approval). Use `pageIndex` and `pageSize` to paginate. # List Workflows Source: https://docs.humanos.tech/api-reference/latest/approval/list-workflows /versions/2026-09-01.yaml get /approvals/workflow Retrieve a paginated list of your workflows, including their approvals. Workflows let you pack approvals together and request them easily. Filter with the optional query parameters: `search` (matches the workflow name or the name of a contained approval), `active` (active or inactive workflows only), `types` (only workflows containing approvals of the given resource types), and `internalId` (only the workflow containing the approval with your identifier). Use `pageIndex` and `pageSize` to paginate. # Audit Credential Event Chain Source: https://docs.humanos.tech/api-reference/latest/credentials/audit-credential-event-chain /versions/2026-09-01.yaml get /credential/{credentialId}/events Walk a v0.2 credential's VIAEvent chain and re-verify every event, returning a tamper-evidence report. For each event it reports whether its signature verifies (against the signer’s own key) and whether its `previousEvent` chain link is intact; `ok` is true only when the whole chain walks and every event verifies. Audit-side only — this does not affect verification. A legacy (pre-0.2) credential has no event chain and returns an empty list. # Get Credential Source: https://docs.humanos.tech/api-reference/latest/credentials/get-credential /versions/2026-09-01.yaml get /credential/{credentialId} Retrieve a credential by its identifier, including the full W3C Verifiable Credential, and the latest status of all grantor decisions. The `credentialId` must be a credential URN (`urn:via:credential:`); a malformed URN returns 400. # Get Evidence Source: https://docs.humanos.tech/api-reference/latest/credentials/get-evidence /versions/2026-09-01.yaml get /credential/evidence/{evidenceId} Fetch the raw content of an evidence file attached to a credential. `evidenceId` must be an evidence URN (`urn:via:evidence:`); a malformed URN returns 400. The response body is the evidence content (e.g. base64-encoded file, JSON). # Issue VP Source: https://docs.humanos.tech/api-reference/latest/credentials/issue-vp /versions/2026-09-01.yaml post /credential/vp/{vcId} Build and sign a W3C Verifiable Presentation for a stored credential. The API key must belong to the credential owner (its DID must be on the mandate authorizedDIDs). Optionally pass `targetVerifier` in the request body — the verifier domain, e.g. "organization.com" — to bind the VP to that audience via proof.domain and a challenge nonce; omit it for an unbound VP. # Revoke Credential Source: https://docs.humanos.tech/api-reference/latest/credentials/revoke-credential /versions/2026-09-01.yaml delete /credential/revoke/{credentialId} Permanently revoke an active credential. Flips status to INACTIVE, records inactiveAt and inactiveReason, appends a REVOKE proof to the credential, and emits a MANDATE_REVOKED activity event. Optionally include a `reason` (max 200 chars) in the request body to record why the credential was revoked; it is stored as the credential inactiveReason, prefixed as `revoked:`, and included in the MANDATE_REVOKED event. Subsequent /vp and /verify calls for this credential are denied with reason credential_revoked. # Verify VP Source: https://docs.humanos.tech/api-reference/latest/credentials/verify-vp /versions/2026-09-01.yaml post /credential/verify Evaluate a VP against runtime context. On allow, returns 201 with the decision. On deny, returns 403 with the reason and evaluations. Either way a VERIFICATION_APPROVED/DENIED event is appended to the credential chain. If the mandate has no constraint schema, only signatures, verifier access, and validity are checked. Supply the presentation exactly as returned by POST /credential/vp/:vcId — do not re-serialize or modify it. Pass it as either `presentation` (the signed VP object) or `presentationEncoded` (its base64 string); at least one is required — sending neither, a `presentationEncoded` that is not valid base64-encoded JSON, or a presentation that fails shape validation each return 400 — and if both are sent `presentationEncoded` wins. `presentationEncoded` is the recommended field: being an opaque string it is immune to JSON re-serialization / formatting changes (e.g. some HTTP clients reformatting date strings) that would otherwise break the signature. Supply runtime action arguments in `executionParams` (an object; defaults to `{}`). They are validated against the action's declared executionParams shape and exposed to CEL rules as `executionParams.`; a mismatch denies with reason execution_params_invalid. # Resolve DID Source: https://docs.humanos.tech/api-reference/latest/did/resolve-did /versions/2026-09-01.yaml get /did/resolve/{did} Resolve a DID to its W3C DID 1.1 Document. Only DIDs with ACTIVE status resolve; an unknown or non-active DID returns 404. The document lists every registered key under `verificationMethod`, while `authentication` and `assertionMethod` reference only non-revoked keys. # Agent trust snapshot Source: https://docs.humanos.tech/api-reference/latest/intelligence/agent-trust-snapshot /versions/2026-09-01.yaml get /intelligence/snapshot Get the risk picture of one agent at a single moment in time. Answers questions like: was this agent registered, did it hold a valid mandate, who consented to its authority, and does its audit trail verify. Use your own organization DID in `orgDid` to check your agents. Use a partner organization's DID to check theirs; this requires an active sharing grant from that organization, otherwise the request returns 404. `at` defaults to now. Set it to a past instant to see what was true at that moment: the answer is rebuilt from the signed event history, not from current state. See [Risk metrics](/essentials/risk-metrics) for how sharing grants let you consult another organization's intelligence. # Agent trust window Source: https://docs.humanos.tech/api-reference/latest/intelligence/agent-trust-window /versions/2026-09-01.yaml get /intelligence/window Get what one agent did over a period of time. Answers questions like: how active was the agent, how often was it approved or denied and why, who did it transact with, and did it keep trying after its authority ended. Use your own organization DID in `orgDid` to check your agents. Use a partner organization's DID to check theirs; this requires an active sharing grant from that organization, otherwise the request returns 404. The window runs from `from` (included) to `to` (excluded), in UTC, and can cover at most 366 days. See [Risk metrics](/essentials/risk-metrics) for how sharing grants let you consult another organization's intelligence. # Resolve an action Source: https://docs.humanos.tech/api-reference/latest/intelligence/resolve-an-action /versions/2026-09-01.yaml get /intelligence/action Resolve an action URN into its definition: name, description, declared parameters and the policy rules behind it. Snapshot and window bodies report per-action figures keyed by URN. The URN alone is a pointer; this endpoint tells you what an execution of that action actually permits. Use your own organization DID in `orgDid` to resolve your actions. Use a partner organization's DID to resolve theirs; this requires an active sharing grant from that organization, otherwise the request returns 404. See [Risk metrics](/essentials/risk-metrics) for how sharing grants let you consult another organization's intelligence. # Cancel Request Source: https://docs.humanos.tech/api-reference/latest/requests/cancel-request /versions/2026-09-01.yaml delete /request/{requestId} This action will cancel a credential request. Once a request is canceled, it cannot be undone and will prevent the request from being completed by any subjects. Any draft credentials are marked canceled and any already-active credentials in the request are revoked. # Create Request Source: https://docs.humanos.tech/api-reference/latest/requests/create-request /versions/2026-09-01.yaml post /request Create a new request for one or more subjects (users). This endpoint will: - Create credentials from resources (via group IDs, resource IDs) or inline JSON data - Associate those credentials with subjects - Generate and send OTPs via email or SMS # Get Detail Source: https://docs.humanos.tech/api-reference/latest/requests/get-detail /versions/2026-09-01.yaml get /request/{requestId} Retrieve detailed information about a specific credential request Just like the list requests endpoint, this endpoint includes information about the request, credentials, subjects (users) and chosen security level However, this endpoint includes the full W3C Verifiable Credential data for each credential, and the latest status of all grantor decisions # List Requests Source: https://docs.humanos.tech/api-reference/latest/requests/list-requests /versions/2026-09-01.yaml get /request Retrieve a paginated list of your credential requests Each request includes information about the request, credentials, subjects (users) and chosen security level Optional query parameters filter the results: search (matches request or credential name), dateFrom/dateTo (creation-date range), subject (by contact, id, or internalId), internalId, and securityLevel Use pageIndex and pageSize to page through the results # Resend OTP Source: https://docs.humanos.tech/api-reference/latest/requests/resend-otp /versions/2026-09-01.yaml patch /request/resend/{requestId} Resend a One-Time Password (OTP) to a subject for credential approval, via email or SMS. Provide exactly one query parameter to identify the subject: contact, id, or internalId. The endpoint validates: - The request is not canceled - Maximum attempts have not been reached, only successful OTP sends count towards resend limits - Sufficient time has passed since the last successful resend - Billing restrictions apply # Create User Source: https://docs.humanos.tech/api-reference/latest/user/create-user /versions/2026-09-01.yaml post /user This endpoint allows organizations to import their existing users' information into Humanos, for better management and control. - Between 1 and 100 users can be created per request. - A user is identified by a Humanos-verified contact (email or phone); if it does not exist, it will be created. - Optionally, additional identity information (KYC) to allow mapping users within your own system. - Optionally, additional contact emails or phone numbers can be provided to allow mapping users within your own system. - Optionally, an internal ID can be provided to allow mapping users within your own system. - Optionally, provide an 'Idempotency-Key' header to safely retry the same request without creating duplicate users. # Get User Source: https://docs.humanos.tech/api-reference/latest/user/get-user /versions/2026-09-01.yaml get /user Retrieves detailed information about a user. Search by exactly one parameter: contact, id, or internalId. Only one must be provided. # Credential Event Source: https://docs.humanos.tech/api-reference/latest/webhooks/credential-event /versions/2026-09-01.yaml post /_webhooks/credential Fired when a credential request is accepted or rejected by the subject. **Inbound webhook callback** — Humanos POSTs this payload to your registered webhook URL. This is not an endpoint you call; it is documented as a path so SDK code generators emit a typed payload model. # Identity Event Source: https://docs.humanos.tech/api-reference/latest/webhooks/identity-event /versions/2026-09-01.yaml post /_webhooks/identity Fired when an identity verification (KYC) decision is reached. **Inbound webhook callback** — Humanos POSTs this payload to your registered webhook URL. This is not an endpoint you call; it is documented as a path so SDK code generators emit a typed payload model. # OTP Failed Event Source: https://docs.humanos.tech/api-reference/latest/webhooks/otp-failed-event /versions/2026-09-01.yaml post /_webhooks/otp-failed Fired when an OTP **SMS** could not be delivered: every eligible SMS provider was tried (synchronously or via carrier delivery reports) and none succeeded. Email OTP failures do not emit this event. Carrier delivery reports are asynchronous and occasionally corrected to "delivered" afterwards, so treat this event as *Humanos stopped retrying*, not as a guarantee the SMS never arrived. The safe reaction is to `PATCH` the `otp.resendEndpoint` URL exactly as delivered — a resend issues a fresh code and invalidates the old one, so it is harmless even if the original message did arrive late. **Inbound webhook callback** — Humanos POSTs this payload to your registered webhook URL. This is not an endpoint you call; it is documented as a path so SDK code generators emit a typed payload model. # Test Event Source: https://docs.humanos.tech/api-reference/latest/webhooks/test-event /versions/2026-09-01.yaml post /_webhooks/test Fired when a webhook test is triggered from the dashboard. **Inbound webhook callback** — Humanos POSTs this payload to your registered webhook URL. This is not an endpoint you call; it is documented as a path so SDK code generators emit a typed payload model. # Authentication Source: https://docs.humanos.tech/essentials/authentication Learn how to authenticate with the Humanos API using API keys and request signing ## API Key The **Humanos API** uses multiple layers of authentication and request validation. The first is the API key. Navigate to the [Humanos Dashboard](https://app.humanos.tech), click **API keys** in the sidebar, and create a new key. * **Name** *required*: A label to identify the API key. * **Description** *optional*: A short note to document the purpose of the API key. * **Expiration** *optional*: The number of days until the key expires. Once the API key is created, copy and securely store the following values: * API Key * Signature Secret You can rotate the secrets associated with an API key. Only the **Signature Secret** changes; the API Key stays the same. *** ## Request Signing Each request must be signed using **HMAC-SHA256** with a timestamp. `X-Timestamp` is epoch **milliseconds** from `Date.now()`, and requests are accepted only within a **5-minute** window. For a request with no body, such as a `GET`, the signed string is just the timestamp. ```javascript theme={"dark"} import crypto from "crypto"; function generateSignature(data, secret, timestamp) { const hmac = crypto.createHmac("sha256", secret); hmac.update(data ? `${timestamp}.${data}` : timestamp.toString()); return hmac.digest("hex"); } ``` ### Signing a request with no body A `GET` carries no body, so you sign the timestamp on its own. Send it in the `X-Timestamp` header alongside the resulting `X-Signature`. ```javascript theme={"dark"} import fetch from "node-fetch"; const apiKey = "YOUR_API_KEY"; const signatureSecret = "YOUR_SIGNATURE_SECRET"; const timestamp = Date.now(); const signature = generateSignature("", signatureSecret, timestamp); const response = await fetch("https://api.humanos.tech/request", { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, "X-Timestamp": timestamp.toString(), "X-Signature": signature, }, }); console.log(await response.json()); ``` ### Signing a request with a body A `POST` sends a JSON body, so you sign the timestamp and the body together. Set `Content-Type: application/json` and send the same body you signed. ```javascript theme={"dark"} import fetch from "node-fetch"; const apiKey = "YOUR_API_KEY"; const signatureSecret = "YOUR_SIGNATURE_SECRET"; const body = JSON.stringify({ users: [{ contact: "+351912345678" }], securityLevel: "CONTACT", credentials: [{ type: "JSON", name: "Welcome Pack" }], }); const timestamp = Date.now(); const signature = generateSignature(body, signatureSecret, timestamp); const response = await fetch("https://api.humanos.tech/request", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "X-Timestamp": timestamp.toString(), "X-Signature": signature, }, body, }); console.log(await response.json()); ``` # Collecting Approvals Source: https://docs.humanos.tech/essentials/guides/collecting-approvals Request signed approvals, such as consents, forms, documents, or JSON, from your users, delivered by link or embedded iframe, with optional identity verification. Use `POST /request` to ask one or more people to review and approve something. They can agree to a consent, fill a form, sign a document, or accept a block of structured data, and you receive the signed result as a W3C Verifiable Credential. This guide covers everything except agent-action mandates; for those, see [Agent Actions](/essentials/guides/verifying-agent-actions).
Every approval a user makes is captured as a signed, tamper-evident credential. You don't store the signature or manage the signing UI. Humanos does, and streams you the outcome over [webhooks](/essentials/webhooks-intro).
## What you can request Each request carries one or more **credentials**, the things you are asking the subject to approve. Every credential has a `type`: | Type | The subject… | `data` | | ---------- | ---------------------------------- | --------------------------------------------------------- | | `CONSENT` | Agrees to a statement | `{ text, url? }`: the consent text, plus an optional link | | `DOCUMENT` | Signs a PDF | A base64-encoded PDF, or a stored resource | | `FORM` | Fills a form | Reference a stored form resource, not inline | | `JSON` | Accepts a block of structured data | Any JSON, the general-purpose type |
The `POLICY` type is for agent mandates and is intentionally out of scope here. It's covered in [Verifying Agent Actions](/essentials/guides/verifying-agent-actions).
You supply credentials two ways, and you can mix them in one request: * **Inline**: put them in `credentials[]` with a `data` field. Best for one-off, dynamic content. * **From the library**: reference resources built in the dashboard by id with `resourcesIds`, or whole **workflows** by id with `groupIds`. A workflow, also called a group, bundles several resources, so a single request can collect **multiple approvals at once**. ## Making the request `POST /request`, signed per [Authentication](/essentials/authentication). ```javascript theme={"dark"} const res = await signedFetch("/request", { method: "POST", body: { name: "Onboarding: terms & NDA", internalId: "signup-4471", securityLevel: "CONTACT", users: [{ contact: "user@example.com" }], credentials: [ { type: "CONSENT", name: "Terms of Service", internalId: "tos-v3", data: { text: "I agree to the Terms of Service.", url: "https://example.com/tos", }, }, { type: "DOCUMENT", name: "Mutual NDA", data: "JVBERi0xLjQK...", // base64-encoded PDF placements: [ { contact: "user@example.com", type: "SIGNATURE", x: 12, y: 80, width: 30, height: 8, pageIndex: 0, }, ], }, ], }, }); const { data: request } = await res.json(); ``` ### Request fields The people who must approve. Up to 20 per request. Email or phone number. Determines the delivery channel: email if it contains `@`, otherwise SMS. A second email/phone. When set, the request runs in **2FA mode**: the one-time code goes to `contact` and the signing link goes to `secondaryContact`. A document ID you expect identity verification to return, used to match the person against your records. OTP/notification language: `ENG`, `PRT`, `SPA`, or `FRA`. Defaults to the phone's country, else `ENG`. Free-text label for the signer's role, max 60 chars, e.g. `Administrator`. Inline credentials to collect. Up to 30. Each has `type`, a required `name` shown to the user, optional `internalId`, `description`, `data`, and, for `DOCUMENT`, `placements`. Ids of existing dashboard resources to include as credentials. Ids of existing workflows, also called groups. Each workflow expands to all of its resources, so one request collects several approvals. Whether identity verification is required. See [Identity verification](#identity-verification). Your own reference for the whole request. Echoed back on the webhook so you can correlate. Each credential also takes its own `internalId`. Embed the flow instead of sending a link. See [Embedding & trusted sessions](#embedding--trusted-sessions). ## Signing placements Only applicable to a `DOCUMENT` credential, `placements[]` positions signature and field boxes on the PDF pages. These placements allow users to fill information, draw, sign, select dates, auto-place identity fields, and more. Upload your PDF and select the placements and get your custom API payload [here](https://app.humanos.tech/link/placement-builder). Which subject this box belongs to; it must match a `users[].contact`. A document with placements supports a **single** contact. `SIGNATURE`, `INITIALS`, `IDENTITY_FIELD`, `OPEN_TEXT`, `NUMERIC`, `DATE`, or `CHECKBOX`. For `IDENTITY_FIELD` boxes, which identity value to stamp: `FULL_NAME`, `BIRTH`, `DOC_ID`, `COUNTRY_ALPHA3`, `HEALTH_NUMBER`, `TAX_NUMBER`, `SOCIAL_SECURITY_NUMBER`, `EMAIL`, `PHONE`, or `SIGNATURE_DATE`. Position and size as a percentage of the page, from 0 to 100. Zero-based page the box appears on. ## How the user receives it The subject gets a **one-time code** and a **signing link**. The channel is chosen per contact: email if the contact contains `@`, otherwise SMS. There are three delivery modes:
The code and the link both go to the subject's primary `contact`. They open the link, enter the code, and approve. Set `users[].secondaryContact` to split delivery: the **code** goes to `contact`, the **link** goes to `secondaryContact`. The subject needs both to proceed. In iframe mode the code is sent to `contact` and the link is returned in the API response for you to embed. See [Embedding & trusted sessions](#embedding--trusted-sessions).
Need to send a fresh code? Call `PATCH /request/resend/{requestId}` with exactly one selector query param: `contact`, `id`, or `internalId`. Resends are rate-limited to a minimum of 30 seconds apart, up to 10 attempts. ## Embedding & trusted sessions Instead of sending a link, embed the approval flow directly in your product with `iframe.pubKey`, a P-256 public key, and receive the result over an encrypted `postMessage`. Set `iframe.cookie.allow` to opt into a **trusted session** so a returning subject can skip the OTP on later approvals; the lifetime, `iframe.cookie.duration`, is 1–24 hours. Full setup, including key generation, embedding, and decryption, is in the [Iframe Integration](/essentials/iframe-integration) guide. ## Identity verification `securityLevel` controls whether the subject must verify their identity before approving: | Level | Identity check | | ---------------------- | -------------------------------------------------------------------- | | `CONTACT` *(default)* | None; the subject only proves control of the contact | | `ORGANIZATION_KYC` | Identity verified against your organization's records | | `HUMANOS_KYC` | Full Humanos identity verification | | `HUMANOS_REVALIDATION` | Re-verify an already-known subject, which may carry zero credentials | When a KYC level is set, the subject completes verification first; if it fails, no credential is issued. The KYC outcome arrives as a separate [`identity` webhook event](/essentials/webhooks-intro). ## One request, many approvals Combine `credentials`, `resourcesIds`, and `groupIds` in a single request and each subject in `users[]` receives all of them to approve together. Workflows, referenced by `groupIds`, are the easiest way to bundle a fixed set of approvals you reuse across requests. ## Getting the result You don't poll. When a subject decides, Humanos sends a **`credential` webhook event** with the signed credential and a `decision` of `accept` or `reject`; KYC results arrive as an **`identity` event**. Match them back to your system with the `internalId` you set on the request or per credential. See the [Webhooks](/essentials/webhooks-intro) guide for the event payloads and how to verify and decrypt them. # Agent Actions Source: https://docs.humanos.tech/essentials/guides/verifying-agent-actions Let an AI agent act under a scoped, user-approved policy: publish an action, request a mandate, issue a verifiable presentation, and verify it at execution time. When an autonomous agent wants to do something on a user's behalf, Humanos lets you prove, cryptographically and at the moment of action, that the user approved *this* kind of action within *these* limits. The flow has four moving parts: 1. **Action**: a reusable policy template you publish. It defines the parameters and rules. 2. **Mandate**: a signed credential the user issues against an action, pinning the values they approve. 3. **Verifiable Presentation (VP)**: a short-lived proof the agent derives from the mandate for a single verification. 4. **Verify**: Humanos checks the VP and evaluates the agent's requested execution against the user-approved rules, returning **allow** or **deny**.
The user approves the mandate *once*. The agent then verifies *every* action against it, with no repeated user prompts. Revoke the mandate and every future action is denied.
## 1. Define and publish an action Actions are authored and published in the [dashboard](/dashboard/actions); they're read-only over the API. An action's content has these parts: | Part | What it is | | ----------------- | ------------------------------------------------------------------------------------------------------------------------- | | `userParams` | Values the **user** approves at mandate time, e.g. a spending cap. Each declares a type and a human-readable description. | | `executionParams` | Values the **agent** supplies at verify time, e.g. the amount it wants to spend. | | `rules` | Named **CEL** expressions that must all evaluate `true`, comparing `executionParams` against `userParams`. | | `target` | *Optional.* The `{ method, domain, path }` the action pertains to. | | `mapper` | *Optional.* Maps params to outbound wire-field names. | A rule looks like `executionParams.amount <= userParams.maxAmount`. Publishing computes an integrity digest, the `digestSRI`, and freezes that version. Mandates always reference the exact version they were issued against, so republishing never changes an existing mandate. Copy the published action's id, `urn:via:action:`. You'll reference it when requesting mandates. ## 2. Request a mandate Ask a user to grant a mandate with `POST /request`, using a credential of `type: POLICY` that references the action and the `userParams` the user is approving. ```javascript theme={"dark"} const res = await signedFetch("/request", { method: "POST", body: { securityLevel: "CONTACT", users: [{ contact: "user@example.com" }], authorizedDIDs: ["did:web:your-agent.example.com"], credentials: [ { type: "POLICY", name: "Purchasing mandate", // shown to the user action: { id: "urn:via:action:", userParams: { maxAmount: 10000, allowedCategories: ["BOOKS", "OFFICE"], }, }, }, ], }, }); ``` Must be `POLICY` for a mandate. A `POLICY` credential carries an `action` and no `data`. The published action URN, `urn:via:action:`. The values the user is approving. Validated against the action's declared `userParams` shape and stored on the mandate. DIDs allowed to derive VPs from, and verify, the resulting mandate, typically your agent's DID. The requester's own DID is always included. The user approval flow is identical to [Collecting Approvals](/essentials/guides/collecting-approvals), using link/OTP with optional KYC. On accept, Humanos issues the mandate and sends a `credential` webhook event. **Persist the mandate URN**, `urn:via:credential:`. It's the durable handle the agent uses from here on. ## 3. Agent issues a VP When the agent is about to act, derive a fresh, short-lived Verifiable Presentation from the mandate. Issue a new one per verification. ```javascript theme={"dark"} const mandateUrn = "urn:via:credential:"; // from the credential webhook const res = await signedFetch( `/credential/vp/${encodeURIComponent(mandateUrn)}`, { method: "POST", body: { // targetVerifier: "verifier.example.com", // optional, binds the VP to this audience }, }, ); const { data } = await res.json(); // data.presentationEncoded, pass this to verify ``` The verifier's domain. When set, the VP is bound to that audience via `proof.domain` and a single-use challenge nonce, valid for about 5 minutes. Omit for an unbound VP, which instead relies on the verifier's DID being in the mandate's `authorizedDIDs`. The response includes `presentation`, `presentationEncoded` as base64, and a signed receipt. The caller's DID must be authorized on the mandate, or issuance is denied. ## 4. Verify Send the VP plus the agent's `executionParams` to the verify endpoint. Humanos runs the full pipeline: signature, status, validity window, audience binding, `executionParams` validation, then the action's CEL rules. ```javascript theme={"dark"} const res = await signedFetch("/credential/verify", { method: "POST", body: { presentationEncoded: data.presentationEncoded, executionParams: { amount: 5000, category: "BOOKS" }, }, }); const result = await res.json(); if (result.decision === "allow") { // agent may act } else { console.error("Denied:", result.reason, result.evaluations); } ``` The base64 VP from step 3. Takes precedence over `presentation` if both are sent; at least one is required. What the agent wants to do. Field names must match the action's declared `executionParams` and are referenced in rules as `executionParams.`. ### `201` Allow Always `allow`. URN of the mandate that was verified. Per-rule results, each `{ rule, result: "pass" | "fail" | "error", reason? }`. Signed `VERIFICATION_APPROVED` receipt. ### `403` Deny Always `deny`. Machine-readable denial reason; see the table below. `403`. `Verification denied: `. URN of the mandate that was evaluated. Per-rule results, populated when a rule failed. Signed `VERIFICATION_DENIED` receipt. | `reason` | Meaning | | ---------------------------------------------- | --------------------------------------------------------------------------------------------- | | `vp_signature_invalid` | The VP's signature failed verification | | `vp_signer_unknown` | The VP was not signed by Humanos | | `credential_revoked` | The mandate was revoked; other `credential_` values appear for canceled/rejected/etc. | | `expired` / `not_yet_valid` | The mandate is outside its validity window | | `verifier_not_authorized` | For an unbound VP, the verifier's DID is not on the mandate's `authorizedDIDs` | | `vp_domain_mismatch` | The VP's bound `targetVerifier` domain doesn't match the verifier | | `vp_challenge_expired` / `vp_challenge_replay` | The challenge on a bound VP is stale or already used | | `action_not_found` / `action_invalid` | The referenced action version is missing or malformed | | `execution_params_invalid` | `executionParams` don't match the action's declared shape | | `rule_failed` | A CEL rule evaluated to `false`; check `evaluations` |
A `403` with `rule_failed` is a legitimate policy outcome, not an error: the agent asked to do something the user didn't authorize. Inspect `evaluations` to see which rule blocked it.
## 5. Revoke a mandate To retire a mandate, whether the user withdraws consent or the underlying rule changes, revoke it. Afterwards, VP issuance and verification both fail with `credential_revoked`, so any VP an agent still holds stops working. ```javascript theme={"dark"} await signedFetch(`/credential/revoke/${encodeURIComponent(mandateUrn)}`, { method: "DELETE", body: { reason: "user_initiated" }, }); ``` Optional label, max 200 chars, recorded on the credential and the revocation receipt. # Iframe Integration Source: https://docs.humanos.tech/essentials/iframe-integration Embed the Humanos Link flow in an iframe, receive encrypted real-time results via postMessage, and optionally skip the OTP for high-frequency approvals with trusted sessions You can embed the Humanos Link flow inside an ` ``` ```javascript theme={"dark"} // `link` is the subjects[].link value returned in Step 2. document.getElementById("humanos-frame").src = link; ```
The `allow` attribute delegates the browser permissions the flow needs inside a cross-origin iframe: * `camera` is required if the flow includes identity verification, since KYC needs camera access for the selfie and document scan steps. * `publickey-credentials-get` lets a returning subject verify with a passkey (device biometrics) instead of typing the one-time code. * `publickey-credentials-create` lets a subject save a passkey after confirming the code, so later approvals can use biometrics. If one of these is missing, or the browser does not support delegating it, the Link app hides the corresponding option and falls back to the one-time code. Browsers ignore `allow` entries they do not recognize, so including all three is safe.
*** ### Step 4: Listen for the Encrypted PostMessage Register a `message` event listener on the window. Always verify that `event.origin` matches the origin of the `link` you embedded, which is the Humanos app origin such as `https://app.humanos.tech` or `https://app.humanos.id`. ```javascript theme={"dark"} // Derive the expected origin from the link you embedded in Step 3. const HUMANOS_ORIGIN = new URL(link).origin; window.addEventListener("message", async (event) => { // Only accept messages from the Humanos app origin if (event.origin !== HUMANOS_ORIGIN) return; // Only process encrypted payloads if (!event.data?.encrypted) return; const { ephemeralPublicKey, iv, ciphertext } = event.data; const events = await decryptPayload( ephemeralPublicKey, iv, ciphertext, keyPair.privateKey, ); console.log("Humanos events:", events); // an array of events, see Payload Structure }); ``` *** ### Step 5: Decrypt the Payload The payload is encrypted using **ECDH P-256 + HKDF-SHA256 + AES-256-GCM**. The decryption derives a shared secret from the backend's ephemeral public key and your private key, then uses HKDF to produce the AES key. ```javascript theme={"dark"} const HKDF_INFO = new TextEncoder().encode("humanos-postmessage-v1"); async function decryptPayload(ephemeralPublicKey, iv, ciphertext, privateKey) { // 1. Import the backend's ephemeral public key const epkBytes = Uint8Array.from(atob(ephemeralPublicKey), (c) => c.charCodeAt(0), ); const epk = await crypto.subtle.importKey( "spki", epkBytes, { name: "ECDH", namedCurve: "P-256" }, false, [], ); // 2. Derive shared secret via ECDH const sharedBits = await crypto.subtle.deriveBits( { name: "ECDH", public: epk }, privateKey, 256, ); // 3. Derive AES-256-GCM key via HKDF-SHA256 (empty salt, fixed info string) const hkdfKey = await crypto.subtle.importKey( "raw", sharedBits, "HKDF", false, ["deriveKey"], ); const aesKey = await crypto.subtle.deriveKey( { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: HKDF_INFO }, hkdfKey, { name: "AES-GCM", length: 256 }, false, ["decrypt"], ); // 4. Decrypt (ciphertext includes the 16-byte GCM auth tag) const ctBytes = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0)); const ivBytes = Uint8Array.from(atob(iv), (c) => c.charCodeAt(0)); const plaintext = await crypto.subtle.decrypt( { name: "AES-GCM", iv: ivBytes, tagLength: 128 }, aesKey, ctBytes, ); return JSON.parse(new TextDecoder().decode(plaintext)); } ``` *** ## Trusted Sessions: skip the OTP For high-frequency approvals, where the same subject approves repeatedly for the same issuer, you can opt a request into a **trusted browser session** so the subject only completes the one-time code **once**. Subsequent approvals in the same browser skip the OTP automatically. ### Opting in Set `iframe.cookie` when you create the request, as in Step 2: ```jsonc theme={"dark"} "iframe": { "pubKey": "", "cookie": { "allow": true, // opt into trusted sessions (default false) "duration": 3600 // session lifetime in seconds; clamped to [3600, 86400] } } ``` | Field | Type | Default | Notes | | ---------- | --------- | ------- | ------------------------------------------------------------------------------------------------ | | `allow` | `boolean` | `false` | When `true`, a valid session lets the subject skip the OTP. | | `duration` | `integer` | `3600` | Session lifetime in seconds. Only used when `allow` is `true`. Clamped to **1 hour – 24 hours**. | ### How it works 1. On the **first** approval, the subject completes the OTP as usual. On success, Humanos sets the **`HttpOnly`, `Secure`, `SameSite=None`** cookie `humanos_trusted_session` in the iframe, scoped to that **subject + issuer** and valid for `duration` seconds. 2. Because the iframe is third-party, the browser must grant it cookie access via the **[Storage Access API](https://developer.mozilla.org/docs/Web/API/Storage_Access_API)**. The Link app requests this on a user gesture; the user may see a one-time browser prompt. 3. On **subsequent** approvals for the same subject + issuer, embedding the `link` auto-authenticates the session and the OTP step is skipped. If the cookie is missing, expired, revoked, or storage access is denied, the flow falls back to the normal OTP.
The trusted-session cookie is `HttpOnly`, so it is never readable by your JavaScript, and it is never included in the postMessage payload. You only need to set `iframe.cookie.allow`; the Link app manages the cookie and the OTP-skip for you.
### Constraints * Same browser, same subject, same issuer, within the configured `duration`. * HTTPS only, since the cookie is `Secure`, and the embedding origin must still pass the allowlist check; trusted sessions do **not** bypass origin validation. * If the browser blocks third-party storage and the user denies the Storage Access prompt, the subject completes the OTP normally. *** ## Payload Structure The decrypted plaintext is a **JSON array of events**, not a single object. Each event is shaped like a Humanos webhook event. A single completion typically contains one `identity` event and/or one `credential` event per credential decided. ```json theme={"dark"} [ { "eventType": "identity", "api_version": "2026-07-06", "requestId": "507f1f77bcf86cd799439011", "issuerDid": "did:web:humanos.tech:org:550e8400-e29b-41d4-a716-446655440000", "user": { "contact": "user@example.com", "id": "did:web:humanos.tech:user:73ebefdd-..." }, "identity": { "fullName": "Jane Doe", "gender": "F", "birth": "1990-05-15", "docId": "AB1234567", "fullDocId": "AB1234567", "countryAlpha3": "USA", "documentType": "PASSPORT" }, "decision": { "success": true, "message": "Verified", "date": "2026-07-06T14:30:00.000Z" } }, { "eventType": "credential", "api_version": "2026-07-06", "requestId": "507f1f77bcf86cd799439011", "issuerDid": "did:web:humanos.tech:org:550e8400-e29b-41d4-a716-446655440000", "user": { "contact": "user@example.com", "id": "did:web:humanos.tech:user:73ebefdd-..." }, "credential": { "id": "urn:via:credential:550e8400-...", "name": "Terms of Service", "resourceType": "CONSENT", "status": "ACTIVE" }, "decision": { "action": "accept", "date": "2026-07-06T14:30:00.000Z" } } ] ```
The exact field names depend on the **API version pinned to the API key that created the request**. The shapes here are for `2026-07-06`, the latest. A request created with an API key pinned to an older version receives the **same events transformed to that version's shape**. For example, `2026-05-17` flattens `decision.action`/`date` to top-level `action`/`decisionDate` and returns `user.did` instead of `user.id`. Pin your API version and test against it.
Every event shares these fields: `identity` or `credential`. API version the payload is formatted for, as `YYYY-MM-DD`. Identifier of the request. Your organization's DID. The subject the event concerns. The subject's contact, an email or phone number. Subject identifier: a DID, such as `did:web:…`. Then, keyed by `eventType`, each event adds its own fields:
The KYC identity-verification outcome. Whether verification succeeded. ISO 8601 timestamp of the decision. Human-readable detail, such as a rejection reason. Data extracted from the verified document. Present on success; omitted when verification did not complete. Full name as printed on the document. Date of birth in ISO 8601 format. Document number. Extended document number. ISO 3166-1 alpha-3 country code. Gender as recorded on the document. Nationality. Place of birth. Place the document was issued. Document type, such as `PASSPORT` or `ID_CARD`. Document issue date. Document expiry date. Addresses on record. Height. Weight. The subject's decision. The accept/reject outcome lives here, not on `credential.status`. Either `accept` or `reject`. ISO 8601 timestamp of when the decision was made. The credential that was decided. URN of the credential. `DOCUMENT`, `CONSENT`, `FORM`, `JSON`, or `POLICY`. `DRAFT`, `ACTIVE`, `REJECTED`, `CANCELED`, `REVOKED`, or `EXPIRED`. Human-readable name of the credential. Free-text description of the credential. Your own identifier for the credential. Free-form labels attached to the credential. The signed W3C Verifiable Credential, using the VIA protocol. See its schema on any credential endpoint in the API reference. Per-grantor decision records for the credential.
*** ## Wire Format The encrypted message posted via `postMessage` is the outer envelope you receive on the `message` event. Decrypt its `ciphertext` to get the payload array above. Envelope version, currently `1`. Always `true`; check it before processing the message. Encryption scheme: `ECDH-P256-AES-256-GCM`. The backend's ephemeral public key for the ECDH exchange, as base64 SPKI DER. Initialization vector: a 12-byte nonce, base64-encoded. AES-GCM ciphertext plus the 16-byte auth tag, base64-encoded. *** ## Complete Example A self-contained page that generates a key pair, asks **your** backend to create the request, passing the public key, embeds the returned `link`, listens for the encrypted message, and decrypts the array of events. Replace `createHumanosRequest` with a call to your own backend endpoint, which holds your API key and calls `POST /request`. ```html theme={"dark"}
Waiting for result...
``` *** ## Security Considerations * **Origin validation**: The backend validates the browser-detected parent origin against your configured allowlist. If the origin is not registered, and `*` is not set, no encrypted payload is generated. The origin is never read from a query parameter. * **Non-extractable private key**: Generate the key with `extractable: false` so no script can read the raw key material. * **Ephemeral keys**: The backend generates a fresh ephemeral key pair for each payload, so compromising one message does not compromise others. * **Authenticated encryption**: AES-256-GCM provides confidentiality and integrity. Tampered ciphertext fails decryption. * **Trusted-session cookies**: The `humanos_trusted_session` cookie is `HttpOnly` + `Secure` + `SameSite=None`, only the SHA-256 hash of the token is stored server-side, and it is strictly scoped to a single subject + issuer with a bounded lifetime. It does not bypass origin validation. *** ## Dashboard Configuration To enable the iframe postMessage channel: 1. Open your organization's **notification / embed settings** in the [Humanos Dashboard](https://app.humanos.tech). 2. Enable **iframe notifications**. 3. Add your allowed origins: **HTTPS only, up to 10**.
You can set `*` to allow **all** origins, but this disables clickjacking protection for the embed and is strongly discouraged in production. Prefer an explicit allowlist of your own HTTPS origins.
When iframe notifications are disabled, the iframe will not send any `postMessage` events regardless of the request configuration. # Introduction Source: https://docs.humanos.tech/essentials/introduction A cryptographic permission slip for each agent action, issued by the user, scoped to one action, and verified at runtime. ## Overview **Humanos** brings human authorization into automated workflows. Digital systems like APIs, workflows, and autonomous agents can verify in real time whether a human has approved an action. If an authorization is missing, Humanos can request it immediately through a secure, unified flow, so processes never stall on compliance or pending approvals. By turning human decisions into programmable, verifiable events, organizations automate more confidently, reduce operational friction, and maintain full traceability of every authorization in a business process.
**Base URL:** `https://api.humanos.tech`
The Humanos flow: an agent prepares an action, calls verify() against a mandate's scope and limits, requests user approval when out of scope, executes on the venue, and emits an anchored proof. ## SDKs Skip the raw HTTP calls. Our official SDKs wrap every endpoint in idiomatic bindings for your stack, including auto-signing and webhook decryption.
```bash theme={"dark"} npm install humanos ```

For the full SDK documentation click here.

```bash theme={"dark"} pip install humanos ```

For the full SDK documentation click here.

```bash theme={"dark"} dotnet add package Humanos ```

For the full SDK documentation click here.

## Concepts | Concept | Description | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Action** | A reusable policy template you publish from the [dashboard](/dashboard/actions). Declares `userParams`, the values the user approves; `executionParams`, the values the agent supplies at execution time; and the rules that compare them. | | **Mandate** | A signed W3C credential of type `POLICY` that the user issues to your agent. It references one action plus the `userParams` values that bind this specific approval. | | **Verifiable Presentation (VP)** | A short-lived, signed proof derived from a mandate. The agent presents one each time it wants to act. | | **Verify** | Humanos checks the VP's signatures, expiry, revocation status, and the action's rules against the agent's `executionParams`. Returns allow or deny. | | **Revoke** | Kills a mandate. Any further VP issuance or verify call is denied. | ## Lifecycle A mandate's lifetime breaks into two flows: * **Request and approval** happens once. You define an action, request a mandate, and the user accepts or rejects. On accept, Humanos issues the mandate and you persist its ID. * **Issuance and verification** happens on every action. The agent issues a fresh VP from the mandate, Humanos verifies it against the agent's `executionParams`, and returns allow or deny. Walk each flow end to end in the [Guides](/essentials/guides/collecting-approvals). ## Audit trail Every consequential event in a mandate's lifecycle, whether issuance, approval, verification, or revocation, is persisted as an immutable, cryptographically signed record that you can query through the activity API. You get a complete, tamper-evident history without logging anything yourself. ## W3C standards Every entity in the Humanos ecosystem, whether an automated system, an organization, or an end user, is identified by a **Decentralized Identifier**, or DID, that complies with the [W3C DID standard](https://www.w3.org/TR/did-1.1/). All authorizations are encapsulated as [W3C Verifiable Credentials 2.0](https://www.w3.org/TR/vc-data-model-2.0/), giving each authorization a cryptographic proof, a tamper-evident structure, and a complete audit trail.
W3C standard for verifiable identities with no central registry. W3C data model for the credentials every authorization uses.
# Quick Start Source: https://docs.humanos.tech/essentials/quick-start Get set up, then pick the guide for what you're building: collecting approvals or verifying agent actions. Humanos turns human authorization into programmable, verifiable events. This page gets you set up and points you at the right walkthrough. ## Before you start 1. Grab your **API Key** and **Signature Secret** from the dashboard and sign every request. See [Authentication](/essentials/authentication). 2. Point a webhook endpoint at Humanos to receive outcomes. See [Webhooks](/essentials/webhooks-intro). 3. Pin an API version with the `API-Version` header. See [Versioning](/essentials/versioning).
On Node.js or TypeScript? The [Humanos SDK](https://www.npmjs.com/package/humanos) wraps every call, including request signing and webhook decryption. The HTTP API and the SDK are equivalent; pick whichever fits your stack.
## Pick your path
Ask people to consent, fill forms, or sign documents, and get the signed result back. Let an agent act under a scoped policy the user approves, then verify every action.
New to the model? The [Introduction](/essentials/introduction) covers the core concepts: actions, mandates, verifiable presentations, and the audit trail. # Risk Metrics Source: https://docs.humanos.tech/essentials/risk-metrics How to access agent risk intelligence, for your own organization or a partner's. The intelligence endpoints expose risk metrics for AI agents operating under Humanos mandates. Instead of sharing raw events or credentials, the API answers business questions: was this agent authorized, who consented to its authority, how does it behave, and can its record be trusted. Every figure is computed from signed, tamper-evident data: the agent's mandates, the lifecycle events chained to them, the identity verification (KYC) of the humans who granted authority, and the agent's own registration (KYA). Responses carry aggregates only; user identities, mandate identifiers and contacts never cross organizations. ## Accessing intelligence data All intelligence endpoints take an `orgDid` parameter naming the organization whose data is consulted. * **Your own organization**: pass your organization's DID. No sharing setup is needed; you can always consult your own agents. * **Another organization**: pass that organization's DID. This works only while that organization has an active sharing grant to yours, managed by them in the dashboard under Settings. Without a grant, the request returns `404 sharing_not_enabled`. The same 404 is returned whether the organization does not exist, never shared with you, or revoked its grant, so organizations cannot be discovered by probing. The `did` parameter names the agent to measure. Every figure is scoped to the pair: the agent's mandates and events within the named organization's data. ## The endpoints * [Agent trust snapshot](/api-reference/latest/intelligence/agent-trust-snapshot): the risk picture of one agent at a single instant. Registration, live mandates, bounds and execution headroom, human consent, and chain integrity. * [Agent trust window](/api-reference/latest/intelligence/agent-trust-window): what one agent did over a period. The request funnel, verification activity, counterparties, burn rate, and incidents. * [Resolve an action](/api-reference/latest/intelligence/resolve-an-action): both bodies report per-action figures keyed by action URN. This endpoint resolves a URN into its definition, so you can see what an execution of that action actually permits. # Versioning Source: https://docs.humanos.tech/essentials/versioning How Humanos API versions works, how to pin a version, and what happens when you don't The Humanos API uses **date-based versioning** in `YYYY-MM-DD` format. A new version is created whenever the API changes: this includes breaking changes, new endpoints, new optional fields, and new response fields. Any modification to the API specification triggers a new dated version. ## The `API-Version` header Include the `API-Version` header in your requests to pin your integration to a specific version: ```javascript theme={"dark"} const response = await fetch("https://api.humanos.tech/request", { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, "X-Timestamp": timestamp.toString(), "X-Signature": signature, "API-Version": "2026-07-06", }, }); ``` ### Version resolution The API resolves the version for each request using the following priority: | Priority | Source | Description | | -------- | -------------------- | ----------------------------------------------------------------------- | | 1 | `API-Version` header | Explicitly set per request | | 2 | Pinned version | Automatically set to the latest version available at your first request | On your first API request, the latest available version is pinned to your organization. All subsequent requests use that pinned version unless you explicitly send an `API-Version` header. To upgrade, send the desired version in the header. Requests are served only for versions in the **supported range**, from your organization's floor version up to today. An `API-Version` in the **future**, or **older than the floor** and no longer supported, returns a **400 Bad Request** whose message states the current supported range.
When your pinned version falls below the supported floor, responses include a **`Deprecation`** header, defined in [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745). Watch for it and upgrade before the version leaves the supported range.
## Webhook versioning Webhooks are versioned independently from the API. Each organization has its own webhook version, which you can manage in the [Humanos Dashboard](https://app.humanos.tech/webhooks). Every webhook payload includes an `api_version` field indicating which version format it uses: ```json theme={"dark"} { "eventType": "credential", "api_version": "2026-07-06", "requestId": "68c42ec3e47c9a7f9241e0ba", ... } ```
Pin your webhook version to avoid unexpected payload changes. Update it on your own schedule after reviewing the [changelog](/essentials/changelog).
## Best practices * **Pin a version** via the `API-Version` header when you go to production. Your organization is automatically pinned to the version available at your first request, but explicitly setting it in the header gives you full control. * **Test version upgrades** before switching. Use the `API-Version` header to test against a newer version without affecting your default pinned version. * **Keep webhook and API versions in sync** unless you have a specific reason not to. This avoids confusion when correlating API responses with webhook payloads. # Webhooks Source: https://docs.humanos.tech/essentials/webhooks-intro Receive real-time notifications about request events and user actions Humanos provides a comprehensive webhook system that allows organizations to receive real-time notifications when specific events occur in their processes. All webhook events are delivered to a single configured endpoint, providing a unified interface for handling all event types. | Event Type | Description | | ------------ | ---------------------------------------------------------------------------------- | | `credential` | A credential such as consent, form, signature, or mandate was accepted or rejected | | `identity` | An identity verification (KYC) was completed | | `otp.failed` | OTP delivery failed | | `test` | Delivered when you send a test webhook from the dashboard | ## Event Payload All webhook events share a common base structure: ```json theme={"dark"} { "eventType": "credential", "api_version": "2026-07-06", "requestId": "68c42ec3e47c9a7f9241e0ba", "internalId": "req_internal_001", "issuerDid": "did:web:humanos.tech:org:550e8400-e29b-41d4-a716-446655440000", "user": { "contact": "+351912345678", "id": "did:web:humanos.tech:user:73ebefdd-a549-4873-aad9-def80c40c8c8", "internalId": "user_98765" } } ``` The event type: `credential`, `identity`, `otp.failed`, or `test`. API version the payload is formatted for, given as `YYYY-MM-DD`. Identifier of the request this event belongs to. DID of your organization, the issuer. Your internal identifier for the request, if you set one. The subject the event concerns. The subject's contact, an email or phone number. Subject identifier, a DID such as `did:web:…`. Your internal identifier for the user, if you set one. Then, keyed by `eventType`, each event adds its own fields. Pick the event you are handling:
The credential that was decided. URN of the credential. `DOCUMENT`, `CONSENT`, `FORM`, `JSON`, or `POLICY`. `DRAFT`, `ACTIVE`, `REJECTED`, `CANCELED`, `REVOKED`, or `EXPIRED`. Human-readable name of the credential. Free-text description of the credential. Your own identifier for the credential. Free-form labels attached to the credential. The signed W3C Verifiable Credential, in the VIA protocol, is the full credential document. See its schema on any credential endpoint in the API reference. Per-grantor decision records for the credential. The subject's decision on the request. Either `accept` or `reject`. ISO 8601 timestamp of when the decision was made. The identity-verification (KYC) outcome. Whether verification succeeded. ISO 8601 timestamp of the decision. Human-readable detail, such as a rejection reason. Data extracted from the verified document. Present on success; omitted when verification did not complete. Full name as printed on the document. Date of birth, in ISO 8601. Document number. Extended document number. ISO 3166-1 alpha-3 country code. Gender as recorded on the document. Nationality. Place of birth. Place the document was issued. Document type, such as `passport` or `id_card`. Document issue date. Document expiry date. Addresses on record. Height. Weight. Details of the failed OTP delivery. `PATCH` this URL exactly as delivered to issue a fresh code. Doing so invalidates the old one. Machine-readable error code. Human-readable error detail. A static confirmation message, sent when you trigger a test webhook from the dashboard.
## Versioning Webhook payloads are versioned independently from the API. Each organization has its own webhook version, which determines the format of the payloads you receive. You can manage your webhook version in the [Humanos Dashboard](https://app.humanos.tech/webhooks). Every webhook payload includes an `api_version` field so you always know which format it uses. Your webhook version is automatically set on the first delivery and stays pinned until you explicitly change it. For more details on how versioning works across the API and webhooks, see [API Versioning](/essentials/versioning). ## Configuration To configure the webhook system, organization administrators can navigate to [Humanos Admin – Webhooks](https://app.humanos.tech/webhooks). The following settings are available: * **Webhook URL** is the endpoint where all webhook events will be delivered, for example `https://your.domain.com/webhook`. * **Webhook Signature Secret** is used for validating that the webhooks were sent by Humanos. * **Webhook Encryption Secret** is used for decrypting the payloads, ensuring security on transit. * **Webhook Encryption Salt** is used together with the Webhook Encryption Secret to derive the final encryption key. The salt adds uniqueness and protects against dictionary or pre-computed attacks, ensuring stronger security for each payload. ```javascript theme={"dark"} import crypto from "crypto"; function generateSignature(data, secret, timestamp) { const hmac = crypto.createHmac("sha256", secret); hmac.update(data ? `${timestamp}.${data}` : timestamp.toString()); return hmac.digest("hex"); } ``` ```javascript theme={"dark"} const express = require("express"); const crypto = require("crypto"); const app = express(); app.use(express.json()); const SIGNATURE_SECRET = "your-signature-secret"; const ENCRYPTION_SECRET = "your-encryption-secret"; const ENCRYPTION_SALT = "your-encryption-salt"; app.post("/webhook", (req, res) => { try { const signature = req.headers["x-signature"]; const timestamp = req.headers["x-timestamp"]; // 1. Verify signature: HMAC over the raw JSON body string (the {iv,data,tag} envelope) const expected = generateSignature( JSON.stringify(req.body), SIGNATURE_SECRET, timestamp, ); if (signature !== expected) return res.status(401).send("Invalid signature"); // 2. Decrypt payload const { iv, data, tag } = req.body; const key = crypto.pbkdf2Sync( Buffer.from(ENCRYPTION_SECRET, "base64"), ENCRYPTION_SALT, 10000, 32, "sha256", ); const decipher = crypto.createDecipheriv( "aes-256-gcm", key, Buffer.from(iv, "base64"), ); decipher.setAuthTag(Buffer.from(tag, "base64")); const decrypted = Buffer.concat([ decipher.update(Buffer.from(data, "base64")), decipher.final(), ]).toString("utf8"); const webhook = JSON.parse(decrypted); console.log("Webhook received:", webhook); res.sendStatus(200); } catch (err) { res.status(500).send(err.message); } }); app.listen(5000, () => console.log("Listening on port 5000")); ``` The provided code snippet implements an express API containing an endpoint to deal with Humanos Webhook notifications. The request should be handled as follows: 1. **Receive the request** → Your endpoint will be called with an encrypted payload. 2. **Verify authenticity** → Check the x-signature header using your **Webhook Signature Secret**. 3. **Decrypt the payload** → Use the **Webhook Encryption Secret** and **Webhook Encryption Salt** to decrypt the message and read the event data. 4. **Process the event** → Store it in your system, update statuses, or trigger business logic. 5. **Respond quickly** → Always return **200 OK** after successful processing. Humanos retries failed deliveries automatically. # Authentication Source: https://docs.humanos.tech/essentials/authentication Learn how to authenticate with the Humanos API using API keys and request signing ## API Key The **Humanos API** uses multiple layers of authentication and request validation. The first is the API key. Navigate to the [Humanos Dashboard](https://app.humanos.tech), click **API keys** in the sidebar, and create a new key. * **Name** *required*: A label to identify the API key. * **Description** *optional*: A short note to document the purpose of the API key. * **Expiration** *optional*: The number of days until the key expires. Once the API key is created, copy and securely store the following values: * API Key * Signature Secret You can rotate the secrets associated with an API key. Only the **Signature Secret** changes; the API Key stays the same. *** ## Request Signing Each request must be signed using **HMAC-SHA256** with a timestamp. `X-Timestamp` is epoch **milliseconds** from `Date.now()`, and requests are accepted only within a **5-minute** window. For a request with no body, such as a `GET`, the signed string is just the timestamp. ```javascript theme={"dark"} import crypto from "crypto"; function generateSignature(data, secret, timestamp) { const hmac = crypto.createHmac("sha256", secret); hmac.update(data ? `${timestamp}.${data}` : timestamp.toString()); return hmac.digest("hex"); } ``` ### Signing a request with no body A `GET` carries no body, so you sign the timestamp on its own. Send it in the `X-Timestamp` header alongside the resulting `X-Signature`. ```javascript theme={"dark"} import fetch from "node-fetch"; const apiKey = "YOUR_API_KEY"; const signatureSecret = "YOUR_SIGNATURE_SECRET"; const timestamp = Date.now(); const signature = generateSignature("", signatureSecret, timestamp); const response = await fetch("https://api.humanos.tech/request", { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, "X-Timestamp": timestamp.toString(), "X-Signature": signature, }, }); console.log(await response.json()); ``` ### Signing a request with a body A `POST` sends a JSON body, so you sign the timestamp and the body together. Set `Content-Type: application/json` and send the same body you signed. ```javascript theme={"dark"} import fetch from "node-fetch"; const apiKey = "YOUR_API_KEY"; const signatureSecret = "YOUR_SIGNATURE_SECRET"; const body = JSON.stringify({ users: [{ contact: "+351912345678" }], securityLevel: "CONTACT", credentials: [{ type: "JSON", name: "Welcome Pack" }], }); const timestamp = Date.now(); const signature = generateSignature(body, signatureSecret, timestamp); const response = await fetch("https://api.humanos.tech/request", { method: "POST", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", "X-Timestamp": timestamp.toString(), "X-Signature": signature, }, body, }); console.log(await response.json()); ``` # Collecting Approvals Source: https://docs.humanos.tech/essentials/guides/collecting-approvals Request signed approvals, such as consents, forms, documents, or JSON, from your users, delivered by link or embedded iframe, with optional identity verification. Use `POST /request` to ask one or more people to review and approve something. They can agree to a consent, fill a form, sign a document, or accept a block of structured data, and you receive the signed result as a W3C Verifiable Credential. This guide covers everything except agent-action mandates; for those, see [Agent Actions](/essentials/guides/verifying-agent-actions).
Every approval a user makes is captured as a signed, tamper-evident credential. You don't store the signature or manage the signing UI. Humanos does, and streams you the outcome over [webhooks](/essentials/webhooks-intro).
## What you can request Each request carries one or more **credentials**, the things you are asking the subject to approve. Every credential has a `type`: | Type | The subject… | `data` | | ---------- | ---------------------------------- | --------------------------------------------------------- | | `CONSENT` | Agrees to a statement | `{ text, url? }`: the consent text, plus an optional link | | `DOCUMENT` | Signs a PDF | A base64-encoded PDF, or a stored resource | | `FORM` | Fills a form | Reference a stored form resource, not inline | | `JSON` | Accepts a block of structured data | Any JSON, the general-purpose type |
The `POLICY` type is for agent mandates and is intentionally out of scope here. It's covered in [Verifying Agent Actions](/essentials/guides/verifying-agent-actions).
You supply credentials two ways, and you can mix them in one request: * **Inline**: put them in `credentials[]` with a `data` field. Best for one-off, dynamic content. * **From the library**: reference resources built in the dashboard by id with `resourcesIds`, or whole **workflows** by id with `groupIds`. A workflow, also called a group, bundles several resources, so a single request can collect **multiple approvals at once**. ## Making the request `POST /request`, signed per [Authentication](/essentials/authentication). ```javascript theme={"dark"} const res = await signedFetch("/request", { method: "POST", body: { name: "Onboarding: terms & NDA", internalId: "signup-4471", securityLevel: "CONTACT", users: [{ contact: "user@example.com" }], credentials: [ { type: "CONSENT", name: "Terms of Service", internalId: "tos-v3", data: { text: "I agree to the Terms of Service.", url: "https://example.com/tos", }, }, { type: "DOCUMENT", name: "Mutual NDA", data: "JVBERi0xLjQK...", // base64-encoded PDF placements: [ { contact: "user@example.com", type: "SIGNATURE", x: 12, y: 80, width: 30, height: 8, pageIndex: 0, }, ], }, ], }, }); const { data: request } = await res.json(); ``` ### Request fields The people who must approve. Up to 20 per request. Email or phone number. Determines the delivery channel: email if it contains `@`, otherwise SMS. A second email/phone. When set, the request runs in **2FA mode**: the one-time code goes to `contact` and the signing link goes to `secondaryContact`. A document ID you expect identity verification to return, used to match the person against your records. OTP/notification language: `ENG`, `PRT`, `SPA`, or `FRA`. Defaults to the phone's country, else `ENG`. Free-text label for the signer's role, max 60 chars, e.g. `Administrator`. Inline credentials to collect. Up to 30. Each has `type`, a required `name` shown to the user, optional `internalId`, `description`, `data`, and, for `DOCUMENT`, `placements`. Ids of existing dashboard resources to include as credentials. Ids of existing workflows, also called groups. Each workflow expands to all of its resources, so one request collects several approvals. Whether identity verification is required. See [Identity verification](#identity-verification). Your own reference for the whole request. Echoed back on the webhook so you can correlate. Each credential also takes its own `internalId`. Embed the flow instead of sending a link. See [Embedding & trusted sessions](#embedding--trusted-sessions). ## Signing placements Only applicable to a `DOCUMENT` credential, `placements[]` positions signature and field boxes on the PDF pages. These placements allow users to fill information, draw, sign, select dates, auto-place identity fields, and more. Upload your PDF and select the placements and get your custom API payload [here](https://app.humanos.tech/link/placement-builder). Which subject this box belongs to; it must match a `users[].contact`. A document with placements supports a **single** contact. `SIGNATURE`, `INITIALS`, `IDENTITY_FIELD`, `OPEN_TEXT`, `NUMERIC`, `DATE`, or `CHECKBOX`. For `IDENTITY_FIELD` boxes, which identity value to stamp: `FULL_NAME`, `BIRTH`, `DOC_ID`, `COUNTRY_ALPHA3`, `HEALTH_NUMBER`, `TAX_NUMBER`, `SOCIAL_SECURITY_NUMBER`, `EMAIL`, `PHONE`, or `SIGNATURE_DATE`. Position and size as a percentage of the page, from 0 to 100. Zero-based page the box appears on. ## How the user receives it The subject gets a **one-time code** and a **signing link**. The channel is chosen per contact: email if the contact contains `@`, otherwise SMS. There are three delivery modes:
The code and the link both go to the subject's primary `contact`. They open the link, enter the code, and approve. Set `users[].secondaryContact` to split delivery: the **code** goes to `contact`, the **link** goes to `secondaryContact`. The subject needs both to proceed. In iframe mode the code is sent to `contact` and the link is returned in the API response for you to embed. See [Embedding & trusted sessions](#embedding--trusted-sessions).
Need to send a fresh code? Call `PATCH /request/resend/{requestId}` with exactly one selector query param: `contact`, `id`, or `internalId`. Resends are rate-limited to a minimum of 30 seconds apart, up to 10 attempts. ## Embedding & trusted sessions Instead of sending a link, embed the approval flow directly in your product with `iframe.pubKey`, a P-256 public key, and receive the result over an encrypted `postMessage`. Set `iframe.cookie.allow` to opt into a **trusted session** so a returning subject can skip the OTP on later approvals; the lifetime, `iframe.cookie.duration`, is 1–24 hours. Full setup, including key generation, embedding, and decryption, is in the [Iframe Integration](/essentials/iframe-integration) guide. ## Identity verification `securityLevel` controls whether the subject must verify their identity before approving: | Level | Identity check | | ---------------------- | -------------------------------------------------------------------- | | `CONTACT` *(default)* | None; the subject only proves control of the contact | | `ORGANIZATION_KYC` | Identity verified against your organization's records | | `HUMANOS_KYC` | Full Humanos identity verification | | `HUMANOS_REVALIDATION` | Re-verify an already-known subject, which may carry zero credentials | When a KYC level is set, the subject completes verification first; if it fails, no credential is issued. The KYC outcome arrives as a separate [`identity` webhook event](/essentials/webhooks-intro). ## One request, many approvals Combine `credentials`, `resourcesIds`, and `groupIds` in a single request and each subject in `users[]` receives all of them to approve together. Workflows, referenced by `groupIds`, are the easiest way to bundle a fixed set of approvals you reuse across requests. ## Getting the result You don't poll. When a subject decides, Humanos sends a **`credential` webhook event** with the signed credential and a `decision` of `accept` or `reject`; KYC results arrive as an **`identity` event**. Match them back to your system with the `internalId` you set on the request or per credential. See the [Webhooks](/essentials/webhooks-intro) guide for the event payloads and how to verify and decrypt them. # Agent Actions Source: https://docs.humanos.tech/essentials/guides/verifying-agent-actions Let an AI agent act under a scoped, user-approved policy: publish an action, request a mandate, issue a verifiable presentation, and verify it at execution time. When an autonomous agent wants to do something on a user's behalf, Humanos lets you prove, cryptographically and at the moment of action, that the user approved *this* kind of action within *these* limits. The flow has four moving parts: 1. **Action**: a reusable policy template you publish. It defines the parameters and rules. 2. **Mandate**: a signed credential the user issues against an action, pinning the values they approve. 3. **Verifiable Presentation (VP)**: a short-lived proof the agent derives from the mandate for a single verification. 4. **Verify**: Humanos checks the VP and evaluates the agent's requested execution against the user-approved rules, returning **allow** or **deny**.
The user approves the mandate *once*. The agent then verifies *every* action against it, with no repeated user prompts. Revoke the mandate and every future action is denied.
## 1. Define and publish an action Actions are authored and published in the [dashboard](/dashboard/actions); they're read-only over the API. An action's content has these parts: | Part | What it is | | ----------------- | ------------------------------------------------------------------------------------------------------------------------- | | `userParams` | Values the **user** approves at mandate time, e.g. a spending cap. Each declares a type and a human-readable description. | | `executionParams` | Values the **agent** supplies at verify time, e.g. the amount it wants to spend. | | `rules` | Named **CEL** expressions that must all evaluate `true`, comparing `executionParams` against `userParams`. | | `target` | *Optional.* The `{ method, domain, path }` the action pertains to. | | `mapper` | *Optional.* Maps params to outbound wire-field names. | A rule looks like `executionParams.amount <= userParams.maxAmount`. Publishing computes an integrity digest, the `digestSRI`, and freezes that version. Mandates always reference the exact version they were issued against, so republishing never changes an existing mandate. Copy the published action's id, `urn:via:action:`. You'll reference it when requesting mandates. ## 2. Request a mandate Ask a user to grant a mandate with `POST /request`, using a credential of `type: POLICY` that references the action and the `userParams` the user is approving. ```javascript theme={"dark"} const res = await signedFetch("/request", { method: "POST", body: { securityLevel: "CONTACT", users: [{ contact: "user@example.com" }], authorizedDIDs: ["did:web:your-agent.example.com"], credentials: [ { type: "POLICY", name: "Purchasing mandate", // shown to the user action: { id: "urn:via:action:", userParams: { maxAmount: 10000, allowedCategories: ["BOOKS", "OFFICE"], }, }, }, ], }, }); ``` Must be `POLICY` for a mandate. A `POLICY` credential carries an `action` and no `data`. The published action URN, `urn:via:action:`. The values the user is approving. Validated against the action's declared `userParams` shape and stored on the mandate. DIDs allowed to derive VPs from, and verify, the resulting mandate, typically your agent's DID. The requester's own DID is always included. The user approval flow is identical to [Collecting Approvals](/essentials/guides/collecting-approvals), using link/OTP with optional KYC. On accept, Humanos issues the mandate and sends a `credential` webhook event. **Persist the mandate URN**, `urn:via:credential:`. It's the durable handle the agent uses from here on. ## 3. Agent issues a VP When the agent is about to act, derive a fresh, short-lived Verifiable Presentation from the mandate. Issue a new one per verification. ```javascript theme={"dark"} const mandateUrn = "urn:via:credential:"; // from the credential webhook const res = await signedFetch( `/credential/vp/${encodeURIComponent(mandateUrn)}`, { method: "POST", body: { // targetVerifier: "verifier.example.com", // optional, binds the VP to this audience }, }, ); const { data } = await res.json(); // data.presentationEncoded, pass this to verify ``` The verifier's domain. When set, the VP is bound to that audience via `proof.domain` and a single-use challenge nonce, valid for about 5 minutes. Omit for an unbound VP, which instead relies on the verifier's DID being in the mandate's `authorizedDIDs`. The response includes `presentation`, `presentationEncoded` as base64, and a signed receipt. The caller's DID must be authorized on the mandate, or issuance is denied. ## 4. Verify Send the VP plus the agent's `executionParams` to the verify endpoint. Humanos runs the full pipeline: signature, status, validity window, audience binding, `executionParams` validation, then the action's CEL rules. ```javascript theme={"dark"} const res = await signedFetch("/credential/verify", { method: "POST", body: { presentationEncoded: data.presentationEncoded, executionParams: { amount: 5000, category: "BOOKS" }, }, }); const result = await res.json(); if (result.decision === "allow") { // agent may act } else { console.error("Denied:", result.reason, result.evaluations); } ``` The base64 VP from step 3. Takes precedence over `presentation` if both are sent; at least one is required. What the agent wants to do. Field names must match the action's declared `executionParams` and are referenced in rules as `executionParams.`. ### `201` Allow Always `allow`. URN of the mandate that was verified. Per-rule results, each `{ rule, result: "pass" | "fail" | "error", reason? }`. Signed `VERIFICATION_APPROVED` receipt. ### `403` Deny Always `deny`. Machine-readable denial reason; see the table below. `403`. `Verification denied: `. URN of the mandate that was evaluated. Per-rule results, populated when a rule failed. Signed `VERIFICATION_DENIED` receipt. | `reason` | Meaning | | ---------------------------------------------- | --------------------------------------------------------------------------------------------- | | `vp_signature_invalid` | The VP's signature failed verification | | `vp_signer_unknown` | The VP was not signed by Humanos | | `credential_revoked` | The mandate was revoked; other `credential_` values appear for canceled/rejected/etc. | | `expired` / `not_yet_valid` | The mandate is outside its validity window | | `verifier_not_authorized` | For an unbound VP, the verifier's DID is not on the mandate's `authorizedDIDs` | | `vp_domain_mismatch` | The VP's bound `targetVerifier` domain doesn't match the verifier | | `vp_challenge_expired` / `vp_challenge_replay` | The challenge on a bound VP is stale or already used | | `action_not_found` / `action_invalid` | The referenced action version is missing or malformed | | `execution_params_invalid` | `executionParams` don't match the action's declared shape | | `rule_failed` | A CEL rule evaluated to `false`; check `evaluations` |
A `403` with `rule_failed` is a legitimate policy outcome, not an error: the agent asked to do something the user didn't authorize. Inspect `evaluations` to see which rule blocked it.
## 5. Revoke a mandate To retire a mandate, whether the user withdraws consent or the underlying rule changes, revoke it. Afterwards, VP issuance and verification both fail with `credential_revoked`, so any VP an agent still holds stops working. ```javascript theme={"dark"} await signedFetch(`/credential/revoke/${encodeURIComponent(mandateUrn)}`, { method: "DELETE", body: { reason: "user_initiated" }, }); ``` Optional label, max 200 chars, recorded on the credential and the revocation receipt. # Iframe Integration Source: https://docs.humanos.tech/essentials/iframe-integration Embed the Humanos Link flow in an iframe, receive encrypted real-time results via postMessage, and optionally skip the OTP for high-frequency approvals with trusted sessions You can embed the Humanos Link flow inside an ` ``` ```javascript theme={"dark"} // `link` is the subjects[].link value returned in Step 2. document.getElementById("humanos-frame").src = link; ```
The `allow` attribute delegates the browser permissions the flow needs inside a cross-origin iframe: * `camera` is required if the flow includes identity verification, since KYC needs camera access for the selfie and document scan steps. * `publickey-credentials-get` lets a returning subject verify with a passkey (device biometrics) instead of typing the one-time code. * `publickey-credentials-create` lets a subject save a passkey after confirming the code, so later approvals can use biometrics. If one of these is missing, or the browser does not support delegating it, the Link app hides the corresponding option and falls back to the one-time code. Browsers ignore `allow` entries they do not recognize, so including all three is safe.
*** ### Step 4: Listen for the Encrypted PostMessage Register a `message` event listener on the window. Always verify that `event.origin` matches the origin of the `link` you embedded, which is the Humanos app origin such as `https://app.humanos.tech` or `https://app.humanos.id`. ```javascript theme={"dark"} // Derive the expected origin from the link you embedded in Step 3. const HUMANOS_ORIGIN = new URL(link).origin; window.addEventListener("message", async (event) => { // Only accept messages from the Humanos app origin if (event.origin !== HUMANOS_ORIGIN) return; // Only process encrypted payloads if (!event.data?.encrypted) return; const { ephemeralPublicKey, iv, ciphertext } = event.data; const events = await decryptPayload( ephemeralPublicKey, iv, ciphertext, keyPair.privateKey, ); console.log("Humanos events:", events); // an array of events, see Payload Structure }); ``` *** ### Step 5: Decrypt the Payload The payload is encrypted using **ECDH P-256 + HKDF-SHA256 + AES-256-GCM**. The decryption derives a shared secret from the backend's ephemeral public key and your private key, then uses HKDF to produce the AES key. ```javascript theme={"dark"} const HKDF_INFO = new TextEncoder().encode("humanos-postmessage-v1"); async function decryptPayload(ephemeralPublicKey, iv, ciphertext, privateKey) { // 1. Import the backend's ephemeral public key const epkBytes = Uint8Array.from(atob(ephemeralPublicKey), (c) => c.charCodeAt(0), ); const epk = await crypto.subtle.importKey( "spki", epkBytes, { name: "ECDH", namedCurve: "P-256" }, false, [], ); // 2. Derive shared secret via ECDH const sharedBits = await crypto.subtle.deriveBits( { name: "ECDH", public: epk }, privateKey, 256, ); // 3. Derive AES-256-GCM key via HKDF-SHA256 (empty salt, fixed info string) const hkdfKey = await crypto.subtle.importKey( "raw", sharedBits, "HKDF", false, ["deriveKey"], ); const aesKey = await crypto.subtle.deriveKey( { name: "HKDF", hash: "SHA-256", salt: new Uint8Array(0), info: HKDF_INFO }, hkdfKey, { name: "AES-GCM", length: 256 }, false, ["decrypt"], ); // 4. Decrypt (ciphertext includes the 16-byte GCM auth tag) const ctBytes = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0)); const ivBytes = Uint8Array.from(atob(iv), (c) => c.charCodeAt(0)); const plaintext = await crypto.subtle.decrypt( { name: "AES-GCM", iv: ivBytes, tagLength: 128 }, aesKey, ctBytes, ); return JSON.parse(new TextDecoder().decode(plaintext)); } ``` *** ## Trusted Sessions: skip the OTP For high-frequency approvals, where the same subject approves repeatedly for the same issuer, you can opt a request into a **trusted browser session** so the subject only completes the one-time code **once**. Subsequent approvals in the same browser skip the OTP automatically. ### Opting in Set `iframe.cookie` when you create the request, as in Step 2: ```jsonc theme={"dark"} "iframe": { "pubKey": "", "cookie": { "allow": true, // opt into trusted sessions (default false) "duration": 3600 // session lifetime in seconds; clamped to [3600, 86400] } } ``` | Field | Type | Default | Notes | | ---------- | --------- | ------- | ------------------------------------------------------------------------------------------------ | | `allow` | `boolean` | `false` | When `true`, a valid session lets the subject skip the OTP. | | `duration` | `integer` | `3600` | Session lifetime in seconds. Only used when `allow` is `true`. Clamped to **1 hour – 24 hours**. | ### How it works 1. On the **first** approval, the subject completes the OTP as usual. On success, Humanos sets the **`HttpOnly`, `Secure`, `SameSite=None`** cookie `humanos_trusted_session` in the iframe, scoped to that **subject + issuer** and valid for `duration` seconds. 2. Because the iframe is third-party, the browser must grant it cookie access via the **[Storage Access API](https://developer.mozilla.org/docs/Web/API/Storage_Access_API)**. The Link app requests this on a user gesture; the user may see a one-time browser prompt. 3. On **subsequent** approvals for the same subject + issuer, embedding the `link` auto-authenticates the session and the OTP step is skipped. If the cookie is missing, expired, revoked, or storage access is denied, the flow falls back to the normal OTP.
The trusted-session cookie is `HttpOnly`, so it is never readable by your JavaScript, and it is never included in the postMessage payload. You only need to set `iframe.cookie.allow`; the Link app manages the cookie and the OTP-skip for you.
### Constraints * Same browser, same subject, same issuer, within the configured `duration`. * HTTPS only, since the cookie is `Secure`, and the embedding origin must still pass the allowlist check; trusted sessions do **not** bypass origin validation. * If the browser blocks third-party storage and the user denies the Storage Access prompt, the subject completes the OTP normally. *** ## Payload Structure The decrypted plaintext is a **JSON array of events**, not a single object. Each event is shaped like a Humanos webhook event. A single completion typically contains one `identity` event and/or one `credential` event per credential decided. ```json theme={"dark"} [ { "eventType": "identity", "api_version": "2026-07-06", "requestId": "507f1f77bcf86cd799439011", "issuerDid": "did:web:humanos.tech:org:550e8400-e29b-41d4-a716-446655440000", "user": { "contact": "user@example.com", "id": "did:web:humanos.tech:user:73ebefdd-..." }, "identity": { "fullName": "Jane Doe", "gender": "F", "birth": "1990-05-15", "docId": "AB1234567", "fullDocId": "AB1234567", "countryAlpha3": "USA", "documentType": "PASSPORT" }, "decision": { "success": true, "message": "Verified", "date": "2026-07-06T14:30:00.000Z" } }, { "eventType": "credential", "api_version": "2026-07-06", "requestId": "507f1f77bcf86cd799439011", "issuerDid": "did:web:humanos.tech:org:550e8400-e29b-41d4-a716-446655440000", "user": { "contact": "user@example.com", "id": "did:web:humanos.tech:user:73ebefdd-..." }, "credential": { "id": "urn:via:credential:550e8400-...", "name": "Terms of Service", "resourceType": "CONSENT", "status": "ACTIVE" }, "decision": { "action": "accept", "date": "2026-07-06T14:30:00.000Z" } } ] ```
The exact field names depend on the **API version pinned to the API key that created the request**. The shapes here are for `2026-07-06`, the latest. A request created with an API key pinned to an older version receives the **same events transformed to that version's shape**. For example, `2026-05-17` flattens `decision.action`/`date` to top-level `action`/`decisionDate` and returns `user.did` instead of `user.id`. Pin your API version and test against it.
Every event shares these fields: `identity` or `credential`. API version the payload is formatted for, as `YYYY-MM-DD`. Identifier of the request. Your organization's DID. The subject the event concerns. The subject's contact, an email or phone number. Subject identifier: a DID, such as `did:web:…`. Then, keyed by `eventType`, each event adds its own fields:
The KYC identity-verification outcome. Whether verification succeeded. ISO 8601 timestamp of the decision. Human-readable detail, such as a rejection reason. Data extracted from the verified document. Present on success; omitted when verification did not complete. Full name as printed on the document. Date of birth in ISO 8601 format. Document number. Extended document number. ISO 3166-1 alpha-3 country code. Gender as recorded on the document. Nationality. Place of birth. Place the document was issued. Document type, such as `PASSPORT` or `ID_CARD`. Document issue date. Document expiry date. Addresses on record. Height. Weight. The subject's decision. The accept/reject outcome lives here, not on `credential.status`. Either `accept` or `reject`. ISO 8601 timestamp of when the decision was made. The credential that was decided. URN of the credential. `DOCUMENT`, `CONSENT`, `FORM`, `JSON`, or `POLICY`. `DRAFT`, `ACTIVE`, `REJECTED`, `CANCELED`, `REVOKED`, or `EXPIRED`. Human-readable name of the credential. Free-text description of the credential. Your own identifier for the credential. Free-form labels attached to the credential. The signed W3C Verifiable Credential, using the VIA protocol. See its schema on any credential endpoint in the API reference. Per-grantor decision records for the credential.
*** ## Wire Format The encrypted message posted via `postMessage` is the outer envelope you receive on the `message` event. Decrypt its `ciphertext` to get the payload array above. Envelope version, currently `1`. Always `true`; check it before processing the message. Encryption scheme: `ECDH-P256-AES-256-GCM`. The backend's ephemeral public key for the ECDH exchange, as base64 SPKI DER. Initialization vector: a 12-byte nonce, base64-encoded. AES-GCM ciphertext plus the 16-byte auth tag, base64-encoded. *** ## Complete Example A self-contained page that generates a key pair, asks **your** backend to create the request, passing the public key, embeds the returned `link`, listens for the encrypted message, and decrypts the array of events. Replace `createHumanosRequest` with a call to your own backend endpoint, which holds your API key and calls `POST /request`. ```html theme={"dark"}
Waiting for result...
``` *** ## Security Considerations * **Origin validation**: The backend validates the browser-detected parent origin against your configured allowlist. If the origin is not registered, and `*` is not set, no encrypted payload is generated. The origin is never read from a query parameter. * **Non-extractable private key**: Generate the key with `extractable: false` so no script can read the raw key material. * **Ephemeral keys**: The backend generates a fresh ephemeral key pair for each payload, so compromising one message does not compromise others. * **Authenticated encryption**: AES-256-GCM provides confidentiality and integrity. Tampered ciphertext fails decryption. * **Trusted-session cookies**: The `humanos_trusted_session` cookie is `HttpOnly` + `Secure` + `SameSite=None`, only the SHA-256 hash of the token is stored server-side, and it is strictly scoped to a single subject + issuer with a bounded lifetime. It does not bypass origin validation. *** ## Dashboard Configuration To enable the iframe postMessage channel: 1. Open your organization's **notification / embed settings** in the [Humanos Dashboard](https://app.humanos.tech). 2. Enable **iframe notifications**. 3. Add your allowed origins: **HTTPS only, up to 10**.
You can set `*` to allow **all** origins, but this disables clickjacking protection for the embed and is strongly discouraged in production. Prefer an explicit allowlist of your own HTTPS origins.
When iframe notifications are disabled, the iframe will not send any `postMessage` events regardless of the request configuration. # Introduction Source: https://docs.humanos.tech/essentials/introduction A cryptographic permission slip for each agent action, issued by the user, scoped to one action, and verified at runtime. ## Overview **Humanos** brings human authorization into automated workflows. Digital systems like APIs, workflows, and autonomous agents can verify in real time whether a human has approved an action. If an authorization is missing, Humanos can request it immediately through a secure, unified flow, so processes never stall on compliance or pending approvals. By turning human decisions into programmable, verifiable events, organizations automate more confidently, reduce operational friction, and maintain full traceability of every authorization in a business process.
**Base URL:** `https://api.humanos.tech`
The Humanos flow: an agent prepares an action, calls verify() against a mandate's scope and limits, requests user approval when out of scope, executes on the venue, and emits an anchored proof. ## SDKs Skip the raw HTTP calls. Our official SDKs wrap every endpoint in idiomatic bindings for your stack, including auto-signing and webhook decryption.
```bash theme={"dark"} npm install humanos ```

For the full SDK documentation click here.

```bash theme={"dark"} pip install humanos ```

For the full SDK documentation click here.

```bash theme={"dark"} dotnet add package Humanos ```

For the full SDK documentation click here.

## Concepts | Concept | Description | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Action** | A reusable policy template you publish from the [dashboard](/dashboard/actions). Declares `userParams`, the values the user approves; `executionParams`, the values the agent supplies at execution time; and the rules that compare them. | | **Mandate** | A signed W3C credential of type `POLICY` that the user issues to your agent. It references one action plus the `userParams` values that bind this specific approval. | | **Verifiable Presentation (VP)** | A short-lived, signed proof derived from a mandate. The agent presents one each time it wants to act. | | **Verify** | Humanos checks the VP's signatures, expiry, revocation status, and the action's rules against the agent's `executionParams`. Returns allow or deny. | | **Revoke** | Kills a mandate. Any further VP issuance or verify call is denied. | ## Lifecycle A mandate's lifetime breaks into two flows: * **Request and approval** happens once. You define an action, request a mandate, and the user accepts or rejects. On accept, Humanos issues the mandate and you persist its ID. * **Issuance and verification** happens on every action. The agent issues a fresh VP from the mandate, Humanos verifies it against the agent's `executionParams`, and returns allow or deny. Walk each flow end to end in the [Guides](/essentials/guides/collecting-approvals). ## Audit trail Every consequential event in a mandate's lifecycle, whether issuance, approval, verification, or revocation, is persisted as an immutable, cryptographically signed record that you can query through the activity API. You get a complete, tamper-evident history without logging anything yourself. ## W3C standards Every entity in the Humanos ecosystem, whether an automated system, an organization, or an end user, is identified by a **Decentralized Identifier**, or DID, that complies with the [W3C DID standard](https://www.w3.org/TR/did-1.1/). All authorizations are encapsulated as [W3C Verifiable Credentials 2.0](https://www.w3.org/TR/vc-data-model-2.0/), giving each authorization a cryptographic proof, a tamper-evident structure, and a complete audit trail.
W3C standard for verifiable identities with no central registry. W3C data model for the credentials every authorization uses.
# Quick Start Source: https://docs.humanos.tech/essentials/quick-start Get set up, then pick the guide for what you're building: collecting approvals or verifying agent actions. Humanos turns human authorization into programmable, verifiable events. This page gets you set up and points you at the right walkthrough. ## Before you start 1. Grab your **API Key** and **Signature Secret** from the dashboard and sign every request. See [Authentication](/essentials/authentication). 2. Point a webhook endpoint at Humanos to receive outcomes. See [Webhooks](/essentials/webhooks-intro). 3. Pin an API version with the `API-Version` header. See [Versioning](/essentials/versioning).
On Node.js or TypeScript? The [Humanos SDK](https://www.npmjs.com/package/humanos) wraps every call, including request signing and webhook decryption. The HTTP API and the SDK are equivalent; pick whichever fits your stack.
## Pick your path
Ask people to consent, fill forms, or sign documents, and get the signed result back. Let an agent act under a scoped policy the user approves, then verify every action.
New to the model? The [Introduction](/essentials/introduction) covers the core concepts: actions, mandates, verifiable presentations, and the audit trail. # Versioning Source: https://docs.humanos.tech/essentials/versioning How Humanos API versions works, how to pin a version, and what happens when you don't The Humanos API uses **date-based versioning** in `YYYY-MM-DD` format. A new version is created whenever the API changes: this includes breaking changes, new endpoints, new optional fields, and new response fields. Any modification to the API specification triggers a new dated version. ## The `API-Version` header Include the `API-Version` header in your requests to pin your integration to a specific version: ```javascript theme={"dark"} const response = await fetch("https://api.humanos.tech/request", { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, "X-Timestamp": timestamp.toString(), "X-Signature": signature, "API-Version": "2026-07-06", }, }); ``` ### Version resolution The API resolves the version for each request using the following priority: | Priority | Source | Description | | -------- | -------------------- | ----------------------------------------------------------------------- | | 1 | `API-Version` header | Explicitly set per request | | 2 | Pinned version | Automatically set to the latest version available at your first request | On your first API request, the latest available version is pinned to your organization. All subsequent requests use that pinned version unless you explicitly send an `API-Version` header. To upgrade, send the desired version in the header. Requests are served only for versions in the **supported range**, from your organization's floor version up to today. An `API-Version` in the **future**, or **older than the floor** and no longer supported, returns a **400 Bad Request** whose message states the current supported range.
When your pinned version falls below the supported floor, responses include a **`Deprecation`** header, defined in [RFC 9745](https://www.rfc-editor.org/rfc/rfc9745). Watch for it and upgrade before the version leaves the supported range.
## Webhook versioning Webhooks are versioned independently from the API. Each organization has its own webhook version, which you can manage in the [Humanos Dashboard](https://app.humanos.tech/webhooks). Every webhook payload includes an `api_version` field indicating which version format it uses: ```json theme={"dark"} { "eventType": "credential", "api_version": "2026-07-06", "requestId": "68c42ec3e47c9a7f9241e0ba", ... } ```
Pin your webhook version to avoid unexpected payload changes. Update it on your own schedule after reviewing the [changelog](/essentials/changelog).
## Best practices * **Pin a version** via the `API-Version` header when you go to production. Your organization is automatically pinned to the version available at your first request, but explicitly setting it in the header gives you full control. * **Test version upgrades** before switching. Use the `API-Version` header to test against a newer version without affecting your default pinned version. * **Keep webhook and API versions in sync** unless you have a specific reason not to. This avoids confusion when correlating API responses with webhook payloads. # Webhooks Source: https://docs.humanos.tech/essentials/webhooks-intro Receive real-time notifications about request events and user actions Humanos provides a comprehensive webhook system that allows organizations to receive real-time notifications when specific events occur in their processes. All webhook events are delivered to a single configured endpoint, providing a unified interface for handling all event types. | Event Type | Description | | ------------ | ---------------------------------------------------------------------------------- | | `credential` | A credential such as consent, form, signature, or mandate was accepted or rejected | | `identity` | An identity verification (KYC) was completed | | `otp.failed` | OTP delivery failed | | `test` | Delivered when you send a test webhook from the dashboard | ## Event Payload All webhook events share a common base structure: ```json theme={"dark"} { "eventType": "credential", "api_version": "2026-07-06", "requestId": "68c42ec3e47c9a7f9241e0ba", "internalId": "req_internal_001", "issuerDid": "did:web:humanos.tech:org:550e8400-e29b-41d4-a716-446655440000", "user": { "contact": "+351912345678", "id": "did:web:humanos.tech:user:73ebefdd-a549-4873-aad9-def80c40c8c8", "internalId": "user_98765" } } ``` The event type: `credential`, `identity`, `otp.failed`, or `test`. API version the payload is formatted for, given as `YYYY-MM-DD`. Identifier of the request this event belongs to. DID of your organization, the issuer. Your internal identifier for the request, if you set one. The subject the event concerns. The subject's contact, an email or phone number. Subject identifier, a DID such as `did:web:…`. Your internal identifier for the user, if you set one. Then, keyed by `eventType`, each event adds its own fields. Pick the event you are handling:
The credential that was decided. URN of the credential. `DOCUMENT`, `CONSENT`, `FORM`, `JSON`, or `POLICY`. `DRAFT`, `ACTIVE`, `REJECTED`, `CANCELED`, `REVOKED`, or `EXPIRED`. Human-readable name of the credential. Free-text description of the credential. Your own identifier for the credential. Free-form labels attached to the credential. The signed W3C Verifiable Credential, in the VIA protocol, is the full credential document. See its schema on any credential endpoint in the API reference. Per-grantor decision records for the credential. The subject's decision on the request. Either `accept` or `reject`. ISO 8601 timestamp of when the decision was made. The identity-verification (KYC) outcome. Whether verification succeeded. ISO 8601 timestamp of the decision. Human-readable detail, such as a rejection reason. Data extracted from the verified document. Present on success; omitted when verification did not complete. Full name as printed on the document. Date of birth, in ISO 8601. Document number. Extended document number. ISO 3166-1 alpha-3 country code. Gender as recorded on the document. Nationality. Place of birth. Place the document was issued. Document type, such as `passport` or `id_card`. Document issue date. Document expiry date. Addresses on record. Height. Weight. Details of the failed OTP delivery. `PATCH` this URL exactly as delivered to issue a fresh code. Doing so invalidates the old one. Machine-readable error code. Human-readable error detail. A static confirmation message, sent when you trigger a test webhook from the dashboard.
## Versioning Webhook payloads are versioned independently from the API. Each organization has its own webhook version, which determines the format of the payloads you receive. You can manage your webhook version in the [Humanos Dashboard](https://app.humanos.tech/webhooks). Every webhook payload includes an `api_version` field so you always know which format it uses. Your webhook version is automatically set on the first delivery and stays pinned until you explicitly change it. For more details on how versioning works across the API and webhooks, see [API Versioning](/essentials/versioning). ## Configuration To configure the webhook system, organization administrators can navigate to [Humanos Admin – Webhooks](https://app.humanos.tech/webhooks). The following settings are available: * **Webhook URL** is the endpoint where all webhook events will be delivered, for example `https://your.domain.com/webhook`. * **Webhook Signature Secret** is used for validating that the webhooks were sent by Humanos. * **Webhook Encryption Secret** is used for decrypting the payloads, ensuring security on transit. * **Webhook Encryption Salt** is used together with the Webhook Encryption Secret to derive the final encryption key. The salt adds uniqueness and protects against dictionary or pre-computed attacks, ensuring stronger security for each payload. ```javascript theme={"dark"} import crypto from "crypto"; function generateSignature(data, secret, timestamp) { const hmac = crypto.createHmac("sha256", secret); hmac.update(data ? `${timestamp}.${data}` : timestamp.toString()); return hmac.digest("hex"); } ``` ```javascript theme={"dark"} const express = require("express"); const crypto = require("crypto"); const app = express(); app.use(express.json()); const SIGNATURE_SECRET = "your-signature-secret"; const ENCRYPTION_SECRET = "your-encryption-secret"; const ENCRYPTION_SALT = "your-encryption-salt"; app.post("/webhook", (req, res) => { try { const signature = req.headers["x-signature"]; const timestamp = req.headers["x-timestamp"]; // 1. Verify signature: HMAC over the raw JSON body string (the {iv,data,tag} envelope) const expected = generateSignature( JSON.stringify(req.body), SIGNATURE_SECRET, timestamp, ); if (signature !== expected) return res.status(401).send("Invalid signature"); // 2. Decrypt payload const { iv, data, tag } = req.body; const key = crypto.pbkdf2Sync( Buffer.from(ENCRYPTION_SECRET, "base64"), ENCRYPTION_SALT, 10000, 32, "sha256", ); const decipher = crypto.createDecipheriv( "aes-256-gcm", key, Buffer.from(iv, "base64"), ); decipher.setAuthTag(Buffer.from(tag, "base64")); const decrypted = Buffer.concat([ decipher.update(Buffer.from(data, "base64")), decipher.final(), ]).toString("utf8"); const webhook = JSON.parse(decrypted); console.log("Webhook received:", webhook); res.sendStatus(200); } catch (err) { res.status(500).send(err.message); } }); app.listen(5000, () => console.log("Listening on port 5000")); ``` The provided code snippet implements an express API containing an endpoint to deal with Humanos Webhook notifications. The request should be handled as follows: 1. **Receive the request** → Your endpoint will be called with an encrypted payload. 2. **Verify authenticity** → Check the x-signature header using your **Webhook Signature Secret**. 3. **Decrypt the payload** → Use the **Webhook Encryption Secret** and **Webhook Encryption Salt** to decrypt the message and read the event data. 4. **Process the event** → Store it in your system, update statuses, or trigger business logic. 5. **Respond quickly** → Always return **200 OK** after successful processing. Humanos retries failed deliveries automatically.