Agent skill

Documenso Webhooks Skill

Receive and authenticate Documenso webhooks. Use when setting up a Documenso webhook endpoint, verifying the X-Documenso-Secret header, debugging 401s or an empty secret header, or handling document events like DOCUMENT_SIGNED and DOCUMENT_COMPLETED.

Install this skill

npx skills add hookdeck/webhook-skills --skill documenso-webhooks


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

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.

For complete handlers with tests, see examples/express/, examples/nextjs/, 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.

EventTriggered When
DOCUMENT_CREATEDA new document is created
DOCUMENT_SENTA document is sent to recipients
DOCUMENT_OPENEDA recipient opens the document for the first time
DOCUMENT_SIGNEDA recipient signs the document
DOCUMENT_RECIPIENT_COMPLETEDA recipient completes their required action
DOCUMENT_COMPLETEDAll recipients have completed their actions
DOCUMENT_REJECTEDA recipient rejects the document
DOCUMENT_CANCELLEDA pending document is cancelled, or an owner deletes it
DOCUMENT_REMINDER_SENTA reminder email is sent to a recipient
RECIPIENT_EXPIREDA recipient's signing deadline passes
TEMPLATE_CREATED / TEMPLATE_UPDATED / TEMPLATE_DELETED / TEMPLATE_USEDTemplate lifecycle events

Environment Variables

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

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

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

Resources


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →