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

# Webhooks

> 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"
  }
}
```

<ResponseField name="eventType" type="string" required>
  The event type: `credential`, `identity`, `otp.failed`, or `test`.
</ResponseField>

<ResponseField name="api_version" type="string" required>
  API version the payload is formatted for, given as `YYYY-MM-DD`.
</ResponseField>

<ResponseField name="requestId" type="string" required>
  Identifier of the request this event belongs to.
</ResponseField>

<ResponseField name="issuerDid" type="string" required>
  DID of your organization, the issuer.
</ResponseField>

<ResponseField name="internalId" type="string">
  Your internal identifier for the request, if you set one.
</ResponseField>

<ResponseField name="user" type="object" required>
  The subject the event concerns.

  <Expandable title="user">
    <ResponseField name="contact" type="string" required>
      The subject's contact, an email or phone number.
    </ResponseField>

    <ResponseField name="id" type="string" required>
      Subject identifier, a DID such as `did:web:…`.
    </ResponseField>

    <ResponseField name="internalId" type="string">
      Your internal identifier for the user, if you set one.
    </ResponseField>
  </Expandable>
</ResponseField>

Then, keyed by `eventType`, each event adds its own fields. Pick the event you are handling:

<div className="sdk-tabs">
  <Tabs>
    <Tab title="credential">
      <ResponseField name="credential" type="object" required>
        The credential that was decided.

        <Expandable title="credential">
          <ResponseField name="id" type="string" required>
            URN of the credential.
          </ResponseField>

          <ResponseField name="resourceType" type="string" required>
            `DOCUMENT`, `CONSENT`, `FORM`, `JSON`, or `POLICY`.
          </ResponseField>

          <ResponseField name="status" type="string" required>
            `DRAFT`, `ACTIVE`, `REJECTED`, `CANCELED`, `REVOKED`, or `EXPIRED`.
          </ResponseField>

          <ResponseField name="name" type="string">
            Human-readable name of the credential.
          </ResponseField>

          <ResponseField name="description" type="string">
            Free-text description of the credential.
          </ResponseField>

          <ResponseField name="internalId" type="string">
            Your own identifier for the credential.
          </ResponseField>

          <ResponseField name="tags" type="string[]">
            Free-form labels attached to the credential.
          </ResponseField>

          <ResponseField name="w3cCredential" type="object" required>
            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.
          </ResponseField>

          <ResponseField name="decisions" type="object[]" required>
            Per-grantor decision records for the credential.
          </ResponseField>
        </Expandable>
      </ResponseField>

      <ResponseField name="decision" type="object" required>
        The subject's decision on the request.

        <Expandable title="decision">
          <ResponseField name="action" type="string" required>
            Either `accept` or `reject`.
          </ResponseField>

          <ResponseField name="date" type="string" required>
            ISO 8601 timestamp of when the decision was made.
          </ResponseField>
        </Expandable>
      </ResponseField>
    </Tab>

    <Tab title="identity">
      <ResponseField name="decision" type="object" required>
        The identity-verification (KYC) outcome.

        <Expandable title="decision">
          <ResponseField name="success" type="boolean" required>
            Whether verification succeeded.
          </ResponseField>

          <ResponseField name="date" type="string" required>
            ISO 8601 timestamp of the decision.
          </ResponseField>

          <ResponseField name="message" type="string">
            Human-readable detail, such as a rejection reason.
          </ResponseField>
        </Expandable>
      </ResponseField>

      <ResponseField name="identity" type="object">
        Data extracted from the verified document. Present on success; omitted when verification did not complete.

        <Expandable title="identity">
          <ResponseField name="fullName" type="string" required>
            Full name as printed on the document.
          </ResponseField>

          <ResponseField name="birth" type="string" required>
            Date of birth, in ISO 8601.
          </ResponseField>

          <ResponseField name="docId" type="string" required>
            Document number.
          </ResponseField>

          <ResponseField name="fullDocId" type="string" required>
            Extended document number.
          </ResponseField>

          <ResponseField name="countryAlpha3" type="string" required>
            ISO 3166-1 alpha-3 country code.
          </ResponseField>

          <ResponseField name="gender" type="string | null">
            Gender as recorded on the document.
          </ResponseField>

          <ResponseField name="nationality" type="string">
            Nationality.
          </ResponseField>

          <ResponseField name="placeOfBirth" type="string">
            Place of birth.
          </ResponseField>

          <ResponseField name="placeOfIssue" type="string">
            Place the document was issued.
          </ResponseField>

          <ResponseField name="documentType" type="string">
            Document type, such as `passport` or `id_card`.
          </ResponseField>

          <ResponseField name="issueDate" type="string">
            Document issue date.
          </ResponseField>

          <ResponseField name="expiresAt" type="string">
            Document expiry date.
          </ResponseField>

          <ResponseField name="addresses" type="string[]">
            Addresses on record.
          </ResponseField>

          <ResponseField name="height" type="number">
            Height.
          </ResponseField>

          <ResponseField name="weight" type="number">
            Weight.
          </ResponseField>
        </Expandable>
      </ResponseField>
    </Tab>

    <Tab title="otp.failed">
      <ResponseField name="otp" type="object" required>
        Details of the failed OTP delivery.

        <Expandable title="otp">
          <ResponseField name="resendEndpoint" type="string" required>
            `PATCH` this URL exactly as delivered to issue a fresh code. Doing so invalidates the old one.
          </ResponseField>

          <ResponseField name="error" type="object" required>
            <Expandable title="error">
              <ResponseField name="code" type="string" required>
                Machine-readable error code.
              </ResponseField>

              <ResponseField name="message" type="string" required>
                Human-readable error detail.
              </ResponseField>
            </Expandable>
          </ResponseField>
        </Expandable>
      </ResponseField>
    </Tab>

    <Tab title="test">
      <ResponseField name="message" type="string" required>
        A static confirmation message, sent when you trigger a test webhook from the dashboard.
      </ResponseField>
    </Tab>
  </Tabs>
</div>

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