humanos
v1.2.0
Published
Official Humanos API SDK for TypeScript/JavaScript with automatic request signing and webhook verification
Maintainers
Readme
Humanos SDK for TypeScript/JavaScript
Official TypeScript/JavaScript SDK for the Humanos API. Auto-signs every request, verifies and decrypts webhooks, and ships full type definitions.
Concepts
Humanos gives your agent a cryptographic permission slip — issued by the user, scoped to a specific action, and verified at runtime.
- Action — a reusable policy template you publish through the dashboard. Declares
userParams(values the human approves),executionParams(what the agent must supply at execution time), and the rules that compare them. - Mandate — a signed W3C credential (type
POLICY) the user issues to your agent. References one action plus theuserParamsvalues 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
verifyis denied.
Lifecycle
A mandate's lifetime breaks into two flows: request and approval (one-time, gets you a mandate ID) and issuance and verification (repeats every time the agent acts).
Flow 1 — Request and approval
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐ ┌─ accepts ─▶ mandate issued
│ Action │ ─▶ │ Agent needs │ ─▶ │ Agent │ ─▶ │ User │ ─▶ ─────┤
│ defined │ │ permission │ │ requests │ │ decides │ └─ rejects ─▶ flow ends
└──────────┘ └──────────────┘ └──────────┘ └──────────┘
step 1 step 2 step 3Flow 2 — Issuance and verification
┌──────────┐ ┌──────────────────────────────┐ ┌─ true ─▶ allow (agent acts)
│ Agent │ ─▶ │ Humanos verifies VP against │ ─▶ ─────┤
│issues VP │ │ executionParams │ └─ false ─▶ deny (blocked)
└──────────┘ └──────────────────────────────┘
step 4 step 5Installation
npm install humanosRequires Node 18 or newer.
Configuration
Sign up at humanos.tech, then grab two sets of credentials from the dashboard:
- API credentials — Settings → API Keys. Copy the API Key and Signature Secret.
- Webhook credentials — Settings → Webhooks. Copy the Webhook Signature Secret, Webhook Encryption Secret, and Webhook Encryption Salt.
Add them to your .env:
HUMANOS_API_KEY=<your-api-key>
HUMANOS_SIGNATURE_SECRET=<your-signature-secret>
HUMANOS_WEBHOOK_SIGNATURE_SECRET=<your-webhook-signature-secret>
HUMANOS_WEBHOOK_ENCRYPTION_SECRET=<your-webhook-encryption-secret>
HUMANOS_WEBHOOK_ENCRYPTION_SALT=<your-webhook-encryption-salt>Everything else (action URNs, mandate URNs, etc.) is application data — keep it in code or your database, not in .env.
Quick start
import { HumanosClient } from "humanos";
const client = new HumanosClient({
basePath: "https://api.humanos.tech",
apiKey: process.env.HUMANOS_API_KEY!,
signatureSecret: process.env.HUMANOS_SIGNATURE_SECRET!,
});
// Sanity check — confirms the API key, signature secret, and signing handshake.
const { data } = await client.requests.list();
console.log(data);A 200 OK with a (possibly empty) list means you're wired up. The walkthrough below exercises the rest of the flow.
Optional: proxy and pluggable transport
All connection knobs live on the client config and are optional — omit them to connect directly:
const client = new HumanosClient({
basePath: "https://api.humanos.tech",
apiKey: process.env.HUMANOS_API_KEY!,
signatureSecret: process.env.HUMANOS_SIGNATURE_SECRET!,
// Route through an explicit HTTP/HTTPS proxy (Node only). Implemented with
// CONNECT-tunneling agents, so HTTPS APIs work behind corporate egress
// proxies.
proxy: {
host: "proxy.corp.internal",
port: 8080,
protocol: "http", // optional, defaults to "http"
auth: { username: "user", password: "pass" }, // optional Basic auth
},
// Swap the HTTP transport — axios's built-in "fetch" adapter for
// edge/serverless runtimes, "xhr" for browsers, or your own AxiosAdapter
// function for tests and instrumentation. Requests are signed regardless
// of the transport used.
transport: "fetch",
// Advanced escape hatch — any other axios option. Explicit `proxy`/`transport`
// above take precedence over the same keys set here.
axiosConfig: { timeout: 10_000 },
});Caveats:
- Node only. In a browser the
proxyoption is ignored — browsers proxy at the OS/network layer. - Configure the proxy explicitly. Don't rely on
HTTP_PROXY/HTTPS_PROXYenvironment variables — those go through axios's native proxy path, which cannot CONNECT-tunnel HTTPS targets. - Proxy auth is Basic only, and SOCKS proxies are not supported. For either, supply your own agents via
axiosConfig(httpAgent/httpsAgent, e.g. fromsocks-proxy-agent).
To see the proxy working end-to-end locally: node scripts/demo-proxy.mjs in one terminal (a zero-dependency logging proxy), node scripts/demo-proxy-client.mjs in another (with HUMANOS_* env vars set) — the proxy prints CONNECT api…:443 while the call returns signed results.
Integration walkthrough
Six steps, matching the lifecycle diagram above. Run through them once with a test action to get an end-to-end feel.
1. Define an action
In the dashboard, create an action. An action has three parts:
executionParams— the values the agent supplies atverify()time.userParams— the constants the user pins at decision time.- Rules — deterministic CEL expressions comparing the two above.
Example:
| Part | Field | Type |
| ----------------- | ------------------- | --------------- |
| executionParams | amount | number |
| | category | string |
| userParams | maxAmount | number |
| | allowedCategories | array<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. Hold the ID as a constant in your code or store it alongside the rule it represents.
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 request = await client.requests.create({
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"],
},
},
},
],
});
console.log("Request ID:", request.data.id);Persist request.data.id if you want to track pending approvals. The mandate ID itself becomes available once the user approves (next 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).
The SDK's createWebhookHandler verifies signatures and decrypts payloads automatically:
import express from "express";
import { createWebhookHandler, WebhookConfig } from "humanos";
const app = express();
// IMPORTANT: use express.text() so the raw body is preserved for signature verification.
// express.json() reformats the payload and breaks the HMAC check.
app.use(express.text({ type: "application/json" }));
const webhookConfig: WebhookConfig = {
signatureSecret: process.env.HUMANOS_WEBHOOK_SIGNATURE_SECRET!,
encryptionSecret: process.env.HUMANOS_WEBHOOK_ENCRYPTION_SECRET!,
encryptionSalt: process.env.HUMANOS_WEBHOOK_ENCRYPTION_SALT!,
};
app.post(
"/webhook",
createWebhookHandler(webhookConfig, async (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;
}
}),
);
app.listen(3000);The credential payload shape:
{
eventType: "credential",
requestId: string,
internalId?: string,
issuerDid: string,
user: { contact: string; id: string; internalId?: string },
credential: CredentialEntity, // full W3C VC; .id is the mandate URN
decision: { action: "accept" | "reject", date: string }, // ISO 8601
}For local development, expose your server with ngrok:
ngrok http 3000Set the ngrok URL (e.g. https://xxxx.ngrok-free.app/webhook) under Settings → Webhooks.
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_ISSUEentry) instead of wiring a webhook.
4. Agent issues a VP
When the agent wants to act, your backend (or whichever component enforces the rule) 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>"; // captured in step 3
const vp = await client.credentials.issueVP(mandateId, {
// targetVerifier: "did:web:your-verifier.example", // optional, see below
});Field-by-field:
mandateId(first positional arg) — the mandate ID captured in step 3.targetVerifier(optional body field) — DID of the intended verifier. When provided, the VP is bound to that audience viaproof.domainplus a challenge nonce.
The response is a PresentationResponseEntity — { presentation, receipt }. Pass vp.data.presentation to step 5.
5. Humanos verifies the VP
Hand the VP plus the agent's executionParams to verify(). 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).
await client.credentials.verify({
presentation: vp.data.presentation,
executionParams: { amount: 5000, category: "BOOKS" },
});Field-by-field:
presentation— the signed VP from step 4. Read it fromvp.data.presentation.executionParams— what the agent wants to do. Field names must match those declared on the action and are referenced inside rules asexecutionParams.<field>.
Expected outcomes for the example action:
| Case | executionParams | Result |
| ------------- | ---------------------------------------- | ------------------------------------------- |
| 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) |
6. Revoke a mandate
Mandates are immutable — to retire one (rule deletion, rule update, user pulling consent), call credentials.revoke(). Once revoked, the mandate is dead: issueVP 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.
await client.credentials.revoke(
mandateUrn,
undefined, // aPIVersion — leave undefined to use the SDK default
{ reason: "user_initiated" },
);Field-by-field:
credentialId(first arg) — the mandate ID.reason— free-text label recorded on the credential and in theMANDATE_REVOKEDreceipt. Recommended values from the VIA protocol:user_initiated,organization_policy,system_expiry.
The response is a RevokeCredentialEntity — credentialId, status: "REVOKED", revokedAt, and a MANDATE_REVOKED receipt. Store it for audit if useful. Enforcement uses the revocation status itself, not the receipt.
Humanos 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 available for query at any time via client.monitoring.listActivity().
| Event | Trigger | Captured |
| ---------------------- | ---------------------------------- | ----------------------------------------------------------------------- |
| Mandate issued | User accepts a request | Action ID, userParams, signed credential, timestamp |
| Mandate revoked | credentials.revoke() succeeds | Mandate ID, reason, timestamp |
| Mandate canceled | Request canceled before approval | Request ID, canceler, timestamp |
| Human decision: accept | User approves a request | User, request, OTP channel (email or SMS), UI surface (hosted / iframe) |
| Human decision: reject | User rejects a request | User, request, OTP channel, UI surface |
| VP issue | credentials.issueVP() succeeds | Mandate ID, VP, target verifier, receipt |
| VP issue denied | credentials.issueVP() rejected | Mandate ID, reason code (e.g., credential_revoked) |
| Verify accept | credentials.verify() returns 200 | VP, executionParams, rule evaluations, signed receipt |
| Verify deny | credentials.verify() returns 403 | VP, executionParams, failing rule(s), signed receipt |
Webhook details
createWebhookHandler decrypts and verifies the payload, then dispatches a discriminated union — switch on eventType to narrow:
| Event | Trigger |
| ------------ | --------------------------------------------------------------------------------- |
| credential | A credential request was approved or rejected. credential.id is the mandate ID. |
| identity | An identity / KYC check completed. |
| otp.failed | A user failed OTP entry. |
| test | "Send test event" from the dashboard. |
Operational notes
- Send a test event from Settings → Webhooks before going live — confirms your URL is reachable and your handler decodes the payload.
- Retries: failed deliveries are retried with backoff. Make your handler idempotent (key off
requestId+eventType). - Order: events for unrelated requests may arrive out of order. Within a single request,
credentialprecedes any follow-up events.
API reference
Every method below is fully typed; signatures live in the generated definitions.
// Requests — credential request lifecycle
client.requests.list(); // paginate / filter your requests
client.requests.create({ ... }); // issue a credential request to a user
client.requests.detail(requestId); // full request including credentials and subjects
client.requests.cancel(requestId); // cancel a pending request
client.requests.resendOtp(requestId); // resend OTP via the original channel
// Credentials — mandates and verification
client.credentials.detail(credentialId); // fetch a credential by ID
client.credentials.evidence(evidenceId); // download attached evidence
client.credentials.issueVP(vcId, { targetVerifier }); // issue a fresh VP from a mandate
client.credentials.verify({ presentation, executionParams }); // evaluate a VP at runtime
client.credentials.revoke(credentialId, undefined, { reason }); // permanently revoke a mandate
// Actions — published policy templates
client.actions.list();
client.actions.versions(actionId);
// Activity — audit log
client.monitoring.listActivity();
// Approval workflows (forms, consents, documents)
client.approval.list();
client.approval.workflows();
// Users
client.users.create([{ contact, internalId, identity }]);
client.users.detail({ contact });
// DID resolution
client.did.resolve(did);API versioning
Humanos uses date-based API versions (e.g., 2026-07-07). The SDK pins each release to a default version and sends it as an api-version header on every request — your integration stays isolated from API changes, and you upgrade by bumping the SDK on your own schedule.
The current default is exported as API_VERSION (the exact date is re-pinned on every SDK release):
import { API_VERSION } from "humanos";
console.log(API_VERSION); // e.g. "2026-07-07"To pin a different version globally, override the header via axiosConfig:
const client = new HumanosClient({
basePath: "https://api.humanos.tech",
apiKey: process.env.HUMANOS_API_KEY!,
signatureSecret: process.env.HUMANOS_SIGNATURE_SECRET!,
axiosConfig: {
headers: { "api-version": "2025-12-01" },
},
});Every method also accepts an optional aPIVersion parameter for per-call overrides — useful when testing a new version against a single endpoint before bumping globally:
await client.credentials.verify(
{ presentation: vp.data.presentation, executionParams: { ... } },
"2025-12-01", // api version for this call only
);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. List published versions with client.actions.versions(actionId).
Error handling
The SDK throws on non-2xx responses. The error includes the underlying HTTP response when available:
try {
await client.requests.create({ ... });
} catch (error) {
if (error.response) {
console.error("Status:", error.response.status);
console.error("Body:", error.response.data);
} else {
console.error("Error:", error.message);
}
}Troubleshooting
401 on every request. Signature mismatch. Double-check the signature secret matches the dashboard exactly — no trailing whitespace or stray newline.
400 on requests.create() with "name is required". Each credentials[] entry needs a name field. The example in step 2 uses "Action mandate".
Webhook never fires. Confirm the URL in Settings → Webhooks matches your live endpoint. Send a test event from the dashboard. For local dev, the ngrok tunnel must be open when the user approves.
Webhook fires but the handler errors with a signature mismatch. Make sure your server uses express.text({ type: "application/json" }). express.json() parses + reformats the body, which breaks HMAC verification.
verify() returns 403 with no obvious failing rule. Check that the executionParams field names exactly match the action's declared fields. A typo silently fails rule evaluation.
issueVP() returns 400 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.
Documentation & related
- Full API documentation
- Humanos website
- Sister SDKs: Python, C#
Support
- Email: [email protected]
License
MIT License — see LICENSE file for details.
