# Documenso Webhooks

[Documenso](https://documenso.com) is an open-source document signing platform. Its webhooks
POST a JSON event to your URL whenever a document is created, sent, opened, signed, completed,
rejected or cancelled. **Documenso does not sign the payload.** Despite the setup page saying
the secret "signs" it, the secret is sent as plain text in the `X-Documenso-Secret` header, so
authentication is a shared-token check, not an HMAC.

## When to Use This Skill

* How do I receive Documenso webhooks?
* How do I verify the `X-Documenso-Secret` header?
* Does Documenso sign webhook payloads with HMAC? (No: it sends the secret itself)
* Why does my Documenso webhook arrive with an empty `X-Documenso-Secret` header?
* How do I handle `DOCUMENT_SIGNED` and `DOCUMENT_COMPLETED` events?

## Verification (core)

Documenso sends the secret you entered when creating the webhook, unchanged, in
`X-Documenso-Secret`. If the webhook has no secret the header is an **empty string**, so reject
empty values and refuse to run without a configured secret.

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyDocumensoSecret(received, expected = process.env.DOCUMENSO_WEBHOOK_SECRET) {
  if (!expected) return false;   // never accept when no secret is configured
  if (!received) return false;   // empty header = webhook has no secret
  return safeEqual(received, expected);
}

// req.headers['x-documenso-secret'] -> 401 if verifyDocumensoSecret(...) is false

```

Return `401` when this fails, `400` for a body that isn't a JSON object, and `200` once
handled (Documenso expects a 2xx within 10 seconds). There is no payload signature, so the raw
body isn't part of verification, but authenticate before parsing it. In Express use
`express.raw({ type: '*/*' })` so any content type reaches the handler. Use HTTPS, because the
secret travels in every request. Details in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/documenso-webhooks/references/verification.md).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/documenso-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/documenso-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/documenso-webhooks/examples/fastapi/).

## Payload and Event Types

Every request has the shape `{ "event", "payload", "createdAt", "webhookEndpoint" }`, where
`payload` is the document (or template) with its `recipients`.

| Event | Triggered When |
| --- | --- |
| `DOCUMENT_CREATED` | A new document is created |
| `DOCUMENT_SENT` | A document is sent to recipients |
| `DOCUMENT_OPENED` | A recipient opens the document for the first time |
| `DOCUMENT_SIGNED` | A recipient signs the document |
| `DOCUMENT_RECIPIENT_COMPLETED` | A recipient completes their required action |
| `DOCUMENT_COMPLETED` | All recipients have completed their actions |
| `DOCUMENT_REJECTED` | A recipient rejects the document |
| `DOCUMENT_CANCELLED` | A pending document is cancelled, or an owner deletes it |
| `DOCUMENT_REMINDER_SENT` | A reminder email is sent to a recipient |
| `RECIPIENT_EXPIRED` | A recipient's signing deadline passes |
| `TEMPLATE_CREATED` / `TEMPLATE_UPDATED` / `TEMPLATE_DELETED` / `TEMPLATE_USED` | Template lifecycle events |

## Environment Variables

```bash
DOCUMENSO_WEBHOOK_SECRET=replace-with-a-long-random-secret   # the webhook's "Secret" field

```

## Local Development

Documenso rejects webhook URLs that resolve to private or loopback addresses, so tunnel with
the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 documenso --path /webhooks/documenso

```

Use the printed URL as the webhook's **Webhook URL**.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/documenso-webhooks/references/overview.md) - How Documenso webhooks work, events, payload fields
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/documenso-webhooks/references/setup.md) - Create the webhook in team settings, set the secret, test and resend
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/documenso-webhooks/references/verification.md) - The X-Documenso-Secret check, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/documenso-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests