Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.humanos.id/llms.txt

Use this file to discover all available pages before exploring further.

This guide walks through a complete Humanos integration over the HTTP API. Every snippet uses fetch and assumes API_KEY, SIGNATURE_SECRET, and a small signedFetch() wrapper are in scope — see Authentication for how to grab the credentials from the dashboard and sign each request.
If you’re integrating from Node.js or TypeScript, the Humanos SDK wraps every call here, including auto-signing and webhook decryption. The two surfaces are equivalent — pick whichever fits your stack.

Step 1 — Define an action

In the dashboard, create an Action. An action has three parts:
  • executionParams — values the agent supplies at verify time
  • userParams — values the user pins at decision time
  • Rules — deterministic CEL expressions comparing the two
Example shape:
PartFieldType
executionParamsamountnumber
categorystring
userParamsmaxAmountnumber
allowedCategoriesarray<string>
Rules then express things like executionParams.amount <= userParams.maxAmount and executionParams.category in userParams.allowedCategories. Once defined, publish the action and copy its ID (urn:via:action:<uuid>). Hold the ID as a constant in your code, or store it alongside the rule it represents in your database.

Step 2 — Issue a mandate request

Ask Humanos to issue a mandate to a user. The request bundles the contact, the action ID, and the userParams values the user is being asked to approve.
const actionId = "urn:via:action:<uuid>"; // from step 1

const res = await signedFetch("/request", {
  method: "POST",
  body: {
    contacts: ["[email protected]"],
    securityLevel: "CONTACT",
    credentials: [
      {
        scope: "agent.execute",
        type: "POLICY",
        name: "Action mandate", // required, shown to the user
        action: {
          id: actionId,
          userParams: {
            maxAmount: 10000,
            allowedCategories: ["BOOKS", "OFFICE"],
          },
        },
      },
    ],
  },
});

const { data: request } = await res.json();
console.log("Request ID:", request.id);
Persist request.id if you want to track pending approvals. The mandate ID itself becomes available once the user approves (next step).

Step 3 — User approval and mandate issuance

Humanos handles the user-facing approval flow. The user reviews the userParams from step 2 and either approves or rejects. Identity verification. The security code (OTP) is always delivered by email or SMS. Where the approval UI shows up. Two options:
  • Hosted (default) — the email or SMS message contains a link to the Humanos-hosted approval page. The user clicks through, reviews, decides.
  • Embedded iframe — your application embeds the Humanos approval UI directly. The user never leaves your app. See the iframe integration guide for setup.
If KYC is required on the action, the user completes that first. If the user rejects (or KYC fails), no mandate is issued. On accept, Humanos issues the mandate and emits a credential webhook event. The mandate ID — urn:via:credential:… — is the durable artifact: persist it on the rule record in your database (e.g., ruleId → mandateUrn). See Webhooks for the full signature verification + decryption pipeline. Once the payload is decoded, a credential event handler looks like this:
function handleWebhookEvent(payload) {
  switch (payload.eventType) {
    case "credential": {
      if (payload.decision.action !== "accept") {
        // user rejected — mark rule unenforceable, notify operator
        return;
      }
      const mandateUrn = payload.credential.id;
      // persist: ruleId → mandateUrn
      break;
    }
    case "identity":
      // KYC / identity verification completed
      break;
    case "otp.failed":
      // user failed OTP — possible fraud signal
      break;
    case "test":
      // "Send test event" from the dashboard
      break;
  }
}
The credential payload shape:
{
  "eventType": "credential",
  "requestId": "<request id>",
  "internalId": "<your reference, if provided>",
  "issuerDid": "did:via:org-...",
  "user": {
    "contact": "[email protected]",
    "did": "did:via:user-...",
    "internalId": "<your reference>"
  },
  "credential": { "/* full W3C VC; .id is the mandate URN */": null },
  "decision": { "action": "accept", "date": "2026-..." }
}
If you’re using the iframe channel, the same credential payload is also delivered via window.postMessage to the parent window — useful for live UI updates without a backend round-trip.
Dev shortcut: during development you can copy the mandate ID directly from the dashboard’s activity table (look for the MANDATE_ISSUE entry) instead of wiring a webhook.

Step 4 — Agent issues a VP

When the agent wants to act, your backend asks Humanos for a fresh Verifiable Presentation bound to the mandate ID captured in step 3. VPs are short-lived and single-purpose — issue a new one for every verify.
const mandateId = "urn:via:credential:<id>"; // from step 3

const res = await signedFetch(
  `/credential/${encodeURIComponent(mandateId)}/presentation`,
  {
    method: "POST",
    body: {
      // targetVerifier: "did:web:your-verifier.example", // optional
    },
  },
);

const { data: vp } = await res.json();
Field-by-field:
  • mandateId (path) — the mandate ID captured in step 3.
  • targetVerifier (optional body) — DID of the intended verifier. When provided, the VP is bound to that audience via proof.domain plus a challenge nonce.
The response is a { presentation, receipt } object. Pass vp.presentation to step 5.

Step 5 — Humanos verifies the VP

Hand the VP plus the agent’s executionParams to the verify endpoint. Humanos checks four things: signatures, expiry, revocation status, and rule compliance. 200 OK means allow; 403 means at least one check failed (the response body names the failing rule under evaluations).
const res = await signedFetch("/credential/verify", {
  method: "POST",
  body: {
    presentation: vp.presentation,
    executionParams: { amount: 5000, category: "BOOKS" },
  },
});

if (res.status === 200) {
  // allow — agent may act
} else if (res.status === 403) {
  const body = await res.json();
  console.error("Denied:", body.evaluations);
}
Field-by-field:
  • presentation — the signed VP from step 4.
  • executionParams — what the agent wants to do. Field names must match those declared on the action and are referenced inside rules as executionParams.<field>.
Expected outcomes for the example action:
CaseexecutionParamsResult
In-bounds{ amount: 5000, category: "BOOKS" }allow (200) + signed receipt
Out-of-bounds{ amount: 50000, category: "FLIGHTS" }deny (403) — body names the failing rule(s)

Step 6 — Revoke a mandate

Mandates are immutable — to retire one (rule deletion, rule update, user pulling consent), call the revoke endpoint. Once revoked, the mandate is dead: VP issuance errors with credential_revoked, and any VP still held by an agent fails verify with the same reason. Stale rules cannot be enforced; that’s the safety property.
const res = await signedFetch(
  `/credential/${encodeURIComponent(mandateId)}/revoke`,
  {
    method: "POST",
    body: { reason: "user_initiated" },
  },
);
Field-by-field:
  • reason — free-text label recorded on the credential and in the MANDATE_REVOKED receipt. Recommended values from the VIA protocol: user_initiated, organization_policy, system_expiry.
The response includes the MANDATE_REVOKED receipt. Store it for audit if useful — enforcement uses the revocation status itself, not the receipt.

Audit trail

For compliance, Humanos persists every consequential event in a mandate’s lifecycle as an immutable, cryptographically signed record. You don’t need to log these yourself — they’re queryable through the activity API.
EventTriggerCaptured
Mandate issuedUser accepts a requestAction ID, userParams, signed credential, timestamp
Mandate revokedRevoke endpoint succeedsMandate ID, reason, timestamp
Mandate canceledRequest canceled before approvalRequest ID, canceler, timestamp
Human decision: acceptUser approves a requestUser, request, OTP channel (email or SMS), UI surface (hosted / iframe)
Human decision: rejectUser rejects a requestUser, request, OTP channel, UI surface
VP issueVP successfully issuedMandate ID, VP, target verifier, receipt
VP issue deniedVP issuance rejectedMandate ID, reason code (e.g., credential_revoked)
Verify acceptVerify returns 200VP, executionParams, rule evaluations, signed receipt
Verify denyVerify returns 403VP, executionParams, failing rule(s), signed receipt

API versioning

Humanos uses date-based API versions (e.g., 2026-04-23). Send your pinned version via the API-Version header — see Versioning for the resolution rules and best practices. Action versions are independent of API versions. When you republish an action in the dashboard, mandates already issued against the older version keep referencing that version — they don’t break.

Error handling

Non-2xx responses include a JSON body describing the failure:
{
  "statusCode": 400,
  "message": "<human-readable reason>",
  "error": "Bad Request"
}
Common cases:
  • 401 on every request — signature mismatch. Double-check the signature secret matches the dashboard exactly. See Authentication.
  • 400 on POST /request with “name is required” — each credentials[] entry needs a name field. The example in step 2 uses "Action mandate".
  • 403 on verify with no obvious failing rule — check that executionParams field names exactly match the action’s declared fields. A typo silently fails rule evaluation.
  • 400 on VP issuance after revoke — expected. Once a mandate is revoked, VP issuance is blocked at source with credential_revoked. Existing VPs also fail verify for the same reason.

Troubleshooting

Webhook never fires. Confirm the URL in Settings → Webhooks matches your live endpoint. Send a test event from the dashboard. For local dev, expose your server with ngrok and set the tunnel URL in the dashboard. Webhook fires but the handler errors with a signature mismatch. Your server must read the raw body (not parsed JSON) for HMAC verification. In Express, use express.text({ type: "application/json" })express.json() reformats the body and breaks the signature. Mandate ID is missing from the webhook payload. It lives under payload.credential.id (the full W3C credential), not at the top level.