Agent skill

Twenty Webhooks Skill

Receive and verify Twenty CRM webhooks (cloud or self-hosted). Use when setting up Twenty webhook handlers, debugging X-Twenty-Webhook-Signature verification, or handling record events such as person.created, company.updated, opportunity.updated, note.created and person.deleted.

Install this skill

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


When to Use This Skill

  • How do I receive Twenty (open-source CRM) webhooks?
  • How do I verify the X-Twenty-Webhook-Signature header?
  • How do I handle person.created, company.updated or opportunity.updated events?
  • How do I receive webhooks for custom objects in Twenty?
  • Why is my Twenty webhook signature verification failing?

Set a secret on the Twenty webhook (Settings > Developers > Webhooks). Twenty signs deliveries only when the webhook has a secret.

Verification (core)

Every delivery is a POST with X-Twenty-Webhook-Signature, X-Twenty-Webhook-Timestamp and X-Twenty-Webhook-Nonce. The signature is the bare lowercase hex HMAC-SHA256 (no prefix), keyed with the webhook secret (its UTF-8 bytes, used as-is), over the timestamp header value, a literal colon, then the raw request body. The nonce is sent but not signed.

const crypto = require('crypto');

function verifyTwentySignature(rawBody, signature, timestamp, secret) {
  if (!signature || !timestamp || !secret) return false;
  if (!/^[0-9a-fA-F]{64}$/.test(signature)) return false; // validate before decoding

  const signed = Buffer.concat([Buffer.from(timestamp, 'latin1'), Buffer.from(':'), rawBody]); // bytes, never a re-encoded string
  const expected = crypto.createHmac('sha256', secret).update(signed).digest();
  const received = Buffer.from(signature.toLowerCase(), 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(expected, received);
}

Respond 401 for a missing, malformed or mismatched signature and any 2xx (the examples use 200) for a valid delivery. Read the raw body first (express.raw({ type: '*/*' }), await request.arrayBuffer(), await request.body()) and parse JSON only after the check. Twenty's docs example signs JSON.stringify(req.body); Twenty's sender signs the JSON string it posts, so signing the raw body is equivalent and avoids re-serialisation drift.

Timestamp window is off by default. X-Twenty-Webhook-Timestamp is Date.now() (Unix milliseconds). Hookdeck retries and replays resend the original headers and timestamp (and Hookdeck applies no tolerance itself), so a fixed window would reject legitimate retries. The examples offer an opt-in: set TWENTY_WEBHOOK_TOLERANCE_SECONDS=300 to enforce one (unset or 0 = off). Use idempotency instead (see below).

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Common Event Types

Twenty sends one event per record change, for every object including custom objects. Dispatch on the body's eventName (<object>.<action>); Twenty's docs page shows the same value in a field named event, so the examples read eventName and fall back to event.

EventTriggered When
person.createdA person record is created
person.deletedA person record is deleted
company.updatedA company record is updated
opportunity.updatedAn opportunity record is updated
note.createdA note is created

The same .created, .updated and .deleted actions fire for every other object (e.g. company.created, opportunity.deleted, or your custom objects). All events go to every webhook URL; there is no event filter, so ignore the ones you don't need.

Twenty's docs say a 2xx acknowledges a delivery and non-2xx is logged as a failure; the sender's HTTP timeout is 5 seconds and retries are not documented.

Environment Variables

# Secret set on the webhook in Twenty (Settings > Developers > Webhooks)
TWENTY_WEBHOOK_SECRET=your_twenty_webhook_secret
# Optional: reject deliveries older than N seconds (unset or 0 = off)
TWENTY_WEBHOOK_TOLERANCE_SECONDS=0

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 twenty --path /webhooks/twenty

Use the printed public HTTPS URL as the webhook URL in Twenty (Settings > Developers > Webhooks).

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 11, 2026

View on GitHub →