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

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

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

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

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

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

<ParamField body="users" type="object[]" required>
  The people who must approve. Up to 20 per request.

  <Expandable title="user">
    <ParamField body="contact" type="string" required>
      Email or phone number. Determines the delivery channel: email if it contains `@`, otherwise SMS.
    </ParamField>

    <ParamField body="secondaryContact" type="string">
      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`.
    </ParamField>

    <ParamField body="docId" type="string">
      A document ID you expect identity verification to return, used to match the person against your records.
    </ParamField>

    <ParamField body="language" type="string" default="ENG">
      OTP/notification language: `ENG`, `PRT`, `SPA`, or `FRA`. Defaults to the phone's country, else `ENG`.
    </ParamField>

    <ParamField body="signerQuality" type="string">
      Free-text label for the signer's role, max 60 chars, e.g. `Administrator`.
    </ParamField>
  </Expandable>
</ParamField>

<ParamField body="credentials" type="object[]">
  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`.
</ParamField>

<ParamField body="resourcesIds" type="string[]">
  Ids of existing dashboard resources to include as credentials.
</ParamField>

<ParamField body="groupIds" type="string[]">
  Ids of existing workflows, also called groups. Each workflow expands to all
  of its resources, so one request collects several approvals.
</ParamField>

<ParamField body="securityLevel" type="string" default="CONTACT">
  Whether identity verification is required. See [Identity
  verification](#identity-verification).
</ParamField>

<ParamField body="internalId" type="string">
  Your own reference for the whole request. Echoed back on the webhook so you
  can correlate. Each credential also takes its own `internalId`.
</ParamField>

<ParamField body="iframe" type="object">
  Embed the flow instead of sending a link. See [Embedding & trusted
  sessions](#embedding--trusted-sessions).
</ParamField>

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

<ParamField body="placements[]" type="object">
  <Expandable title="placement">
    <ParamField body="contact" type="string" required>
      Which subject this box belongs to; it must match a `users[].contact`. A
      document with placements supports a **single** contact.
    </ParamField>

    <ParamField body="type" type="string" required>
      `SIGNATURE`, `INITIALS`, `IDENTITY_FIELD`, `OPEN_TEXT`, `NUMERIC`, `DATE`,
      or `CHECKBOX`.
    </ParamField>

    <ParamField body="identityType" type="string | null">
      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`.
    </ParamField>

    <ParamField body="x, y, width, height" type="number" required>
      Position and size as a percentage of the page, from 0 to 100.
    </ParamField>

    <ParamField body="pageIndex" type="number" required>
      Zero-based page the box appears on.
    </ParamField>
  </Expandable>
</ParamField>

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

<div className="sdk-tabs">
  <Tabs>
    <Tab title="Standard">
      The code and the link both go to the subject's primary `contact`. They open
      the link, enter the code, and approve.
    </Tab>

    <Tab title="2FA">
      Set `users[].secondaryContact` to split delivery: the **code** goes to
      `contact`, the **link** goes to `secondaryContact`. The subject needs both
      to proceed.
    </Tab>

    <Tab title="Iframe">
      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).
    </Tab>
  </Tabs>
</div>

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.
