> ## Documentation Index
> Fetch the complete documentation index at: https://docs.humanos.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Quick Start

> End-to-end walkthrough — define an action, collect approval, issue VPs, verify, and revoke.

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](/essentials/authentication) for how to grab the credentials from the dashboard and sign each request.

<div className="hm-tip">
  If you're integrating from Node.js or TypeScript, the [Humanos SDK](https://www.npmjs.com/package/humanos) wraps every call here, including auto-signing and webhook decryption. The two surfaces are equivalent — pick whichever fits your stack.
</div>

## Step 1 — Define an action

In the dashboard, create an [Action](/dashboard/actions). 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:

| 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** (`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.

```javascript theme={"dark"}
const actionId = "urn:via:action:<uuid>"; // from step 1

const res = await signedFetch("/request", {
  method: "POST",
  body: {
    contacts: ["user@example.com"],
    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](/essentials/iframe-integration) 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](/essentials/webhooks-intro) for the full signature verification + decryption pipeline. Once the payload is decoded, a `credential` event handler looks like this:

```javascript theme={"dark"}
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:

```json theme={"dark"}
{
  "eventType": "credential",
  "requestId": "<request id>",
  "internalId": "<your reference, if provided>",
  "issuerDid": "did:via:org-...",
  "user": {
    "contact": "user@example.com",
    "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.

<div className="hm-tip">
  **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.
</div>

## 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.

```javascript theme={"dark"}
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`).

```javascript theme={"dark"}
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:

| 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) |

## 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.

```javascript theme={"dark"}
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.

| Event                  | Trigger                          | Captured                                                                |
| ---------------------- | -------------------------------- | ----------------------------------------------------------------------- |
| Mandate issued         | User accepts a request           | Action ID, `userParams`, signed credential, timestamp                   |
| Mandate revoked        | Revoke endpoint 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               | VP successfully issued           | Mandate ID, VP, target verifier, receipt                                |
| VP issue denied        | VP issuance rejected             | Mandate ID, reason code (e.g., `credential_revoked`)                    |
| Verify accept          | Verify returns 200               | VP, `executionParams`, rule evaluations, signed receipt                 |
| Verify deny            | Verify returns 403               | VP, `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](/essentials/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:

```json theme={"dark"}
{
  "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](/essentials/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](https://ngrok.com) 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.
