{
  "schemaVersion": "1.0",
  "title": "Twenty webhook security, replay, and events",
  "description": "This reference defines the reviewed Twenty inbound webhook boundary. It covers route activation, exact raw-body HMAC, clock and workspace checks, durable nonce replay fencing, deterministic event mapping, local acceptance, and central AIP p",
  "canonical": "https://getaip.org/docs/connectors/twenty/reference/webhook-security-replay-and-events",
  "route": "/docs/connectors/twenty/reference/webhook-security-replay-and-events",
  "source": "docs/connectors/twenty/reference/webhook-security-replay-and-events.md",
  "protocol": "Agent Interoperability Protocol",
  "protocolVersion": "1.0",
  "section": "Connectors",
  "documentType": "Connector",
  "language": "en",
  "downloads": {
    "md": "/docs/download/connectors/twenty/reference/webhook-security-replay-and-events.md",
    "txt": "/docs/download/connectors/twenty/reference/webhook-security-replay-and-events.txt",
    "json": "/docs/download/connectors/twenty/reference/webhook-security-replay-and-events.json",
    "pdf": "/docs/download/connectors/twenty/reference/webhook-security-replay-and-events.pdf"
  },
  "content": {
    "format": "text/markdown",
    "markdown": "---\ntitle: Twenty webhook security, replay, and events\ndescription: >-\n  Verify exact raw-body HMAC, bounded replay, workspace-scoped event mapping,\n  durable acceptance, and central delivery\nkind: reference\naudience: security-reviewer\nappliesTo: \"1.x\"\nwritingStandard: \"aip-docs/1.0\"\nlastReviewedRevision: \"97be86e9efedf07ecf1783b03800f683f107fb04\"\nconnector: twenty\n---\n\n# Twenty webhook security, replay, and events\n\nThis reference defines the reviewed Twenty inbound webhook boundary. It covers\nroute activation, exact raw-body HMAC, clock and workspace checks, durable nonce\nreplay fencing, deterministic event mapping, local acceptance, and central AIP\npublication.\n\nUse the metadata guide to create or rotate provider registration. This page\nstarts when an external HTTP delivery reaches the standalone connector host.\n\n## Status and route\n\n| Item | Reviewed value |\n|---|---|\n| AIP source revision | `97be86e9efedf07ecf1783b03800f683f107fb04` |\n| Method | `POST` |\n| Internal host route | `/webhooks/twenty` |\n| Timestamp header | `x-twenty-webhook-timestamp` |\n| Signature header | `x-twenty-webhook-signature` |\n| Nonce header | `x-twenty-webhook-nonce` |\n| Body limit | 4 MiB |\n| Signature | HMAC-SHA256 over `timestamp || \":\" || raw_body` |\n| Accepted clock skew | 300,000 ms in either direction |\n| Replay scope | Logical connector instance |\n| Live replay capacity | 100,000 nonces |\n| Local event channel | `twenty-webhooks` |\n\nThe route exists only when `AIP_TWENTY_WEBHOOK_SECRET_FILE` is configured.\nEnabling it also requires `AIP_CONNECTOR_HOST_EVENT_ENDPOINT`; otherwise host\nstartup fails.\n\nAn external reverse proxy may expose a different HTTPS origin and prefix. It\nmust preserve all three headers and the request body byte for byte when\nforwarding to the internal route.\n\n## What the signature authenticates\n\nThe verification equation is:\n\n```text\nexpected = HMAC-SHA256(\n  configured_webhook_secret,\n  timestamp_header_bytes || \":\" || exact_raw_body_bytes\n)\n\nhex_decode(signature_header) == expected\n```\n\nThe connector decodes exactly 64 hexadecimal signature characters and uses the\nHMAC library's verification method. Uppercase and lowercase hex are accepted.\n\nThe nonce is not an HMAC input. It is validated and fenced separately. The\nHTTP route and header names are also outside the signature input.\n\nPossession of the secret authenticates the timestamp and exact body. It does\nnot prove that Twenty generated a business fact, that the configured provider\nregistration belongs to the expected operator, or that the body is true.\n\n## Ingress pipeline\n\nThe standalone host processes one delivery in this order:\n\n1. apply the 4 MiB HTTP body limit;\n2. require non-empty timestamp, signature, and nonce headers;\n3. require a non-empty body no larger than 4 MiB;\n4. validate 32 hexadecimal nonce characters;\n5. validate 64 hexadecimal signature characters;\n6. parse a 1-to-32-digit Unix-millisecond timestamp;\n7. require absolute host-clock skew at most 300,000 ms;\n8. verify HMAC over timestamp, colon, and exact raw body;\n9. durably admit the nonce in the instance replay scope;\n10. parse the signed body as a JSON object;\n11. validate event name, configured workspace, webhook UUID, and event date;\n12. derive a deterministic AIP event;\n13. append it to the local event log;\n14. durably enqueue it on channel `twenty-webhooks`;\n15. attempt immediate central delivery;\n16. return HTTP `200` after durable local acceptance.\n\nCentral acknowledgement is optional at step 15. When it is unavailable, the\nbackground publisher owns a durably queued batch.\n\n## Validate the headers and clock\n\n| Value | Constraint |\n|---|---|\n| Timestamp | Decimal Unix milliseconds, 1 through 32 digits |\n| Signature | Exactly 64 ASCII hexadecimal characters |\n| Nonce | Exactly 32 ASCII hexadecimal characters |\n| Clock delta | Absolute difference from host time no greater than 300,000 ms |\n\nThe timestamp string itself is signed. Reformatting the same instant changes\nthe HMAC input.\n\nKeep host clocks synchronized. The connector does not contact Twenty or a time\nauthority to resolve clock disagreement.\n\n## Preserve exact raw-body bytes\n\nThe connector computes HMAC before parsing JSON. Whitespace, key order,\nescaping, number formatting, or a trailing newline changes the signed bytes.\n\nA proxy or middleware must not parse and reserialize the body. It may enforce a\nsmaller ingress limit if it preserves accepted bodies and headers exactly.\n\nDo not log the secret, signature, full body, or mapped event data. The body can\ncontain confidential and personal workspace data.\n\n## Validate the signed payload\n\nAfter HMAC and replay admission, the body must be a JSON object containing:\n\n```json\n{\n  \"eventName\": \"company.updated\",\n  \"workspaceId\": \"0190c42f-2d5a-7000-8000-000000000080\",\n  \"webhookId\": \"0190c42f-2d5a-7000-8000-000000000081\",\n  \"eventDate\": \"2030-01-03T10:00:00Z\"\n}\n```\n\nEach named field must be a non-empty string. Additional provider fields remain\ninside the event data.\n\n`eventName` accepts 1 through 256 ASCII alphanumeric, `.`, `_`, or `-`\ncharacters. It is not restricted to a source-owned event-name enum.\n\n`workspaceId` must equal the host's configured workspace string exactly.\n`webhookId` must be an RFC 4122 UUID version 1 through 5. `eventDate` must parse\nas RFC 3339 and becomes the AIP event's `occurred_at`.\n\nA valid signature from another workspace is rejected. The HMAC secret does not\nreplace workspace binding.\n\n## Replay state\n\nThe standalone host uses a profile-state replay store with:\n\n| Property | Value |\n|---|---|\n| Namespace | `aip.connector.twenty.webhook_replay.v1` |\n| Scope | `instance:<connector_instance_id>` |\n| Update | Profile-state compare-and-set |\n| CAS attempts | 32 |\n| Retained timestamps | Values at or after `now - 300000 ms` |\n| Capacity | 100,000 live nonces |\n\nEvery replica of one logical instance must share the same durable runtime\ndatabase and instance ID. Different instance IDs have independent replay\nscopes.\n\nBefore inserting a nonce, the store removes entries older than the cutoff. It\nrejects an existing nonce and rejects a new nonce when the live map already\ncontains 100,000 entries.\n\nCAS conflicts yield and retry. Continued contention after 32 attempts is a\nreplay-store failure, not an authentication success.\n\nThe connector also contains an in-memory implementation for tests and\nsingle-process development. The standalone host constructs the profile-state\nimplementation.\n\n## Understand the admission-order boundary\n\nThe replay store admits a verified nonce before JSON parsing, payload-field\nvalidation, local event-log append, or outbox enqueue.\n\nAn authenticated malformed payload therefore consumes its nonce. A failure in\nthe later local event-log or outbox step also occurs after nonce admission.\n\nRepeating the exact delivery with the same nonce then receives a public\nauthentication failure. Provider retry is not the recovery mechanism for a\nlate local failure at this revision.\n\nPreserve host storage evidence and inspect event log, replay state, and outbox\nbefore changing state. Escalate according to deployment policy when a verified\ndelivery was fenced but not durably published.\n\n## Map the deterministic event\n\nThe connector calculates:\n\n```text\ndigest = SHA-256(webhook_id || \":\" || nonce || \":\" || signature_header)\nevent_id = \"evt_\" || hex(first_16_digest_bytes)\nevent_kind = \"twenty.\" || eventName\n```\n\nThe event records:\n\n| Field | Value |\n|---|---|\n| `id` | `evt_` plus 32 lowercase hexadecimal characters |\n| `kind` | `twenty.<validated_eventName>` |\n| `occurred_at` | Parsed signed `eventDate` |\n| `data` | Complete parsed provider object |\n\nThe event ID does not include raw-body bytes directly. Signature changes alter\nthe ID, while the replay fence separately owns nonce uniqueness.\n\nBefore central publication, the common publisher projects a provider-originated\nactor into `data.upstream_actor`. Authenticated central ingress owns the trusted\nprotocol-level event actor and binds it to connector-host signing identity.\n\n## Local event log and central outbox\n\nThe host appends the deterministic event to its local AIP event log. Duplicate\nevent IDs reuse the log's accepted event outcome rather than creating a new\nevent identity.\n\nIt then durably enqueues one-event batch on channel `twenty-webhooks`. The\npublisher derives a stable batch key from normalized channel and event data.\n\nThe acknowledgement body reports one of two central outcomes:\n\n| `central_delivery` | Meaning at HTTP response time |\n|---|---|\n| `centrally_acknowledged` | Central AIP acknowledged the batch |\n| `durably_queued` | Local durable outbox owns delivery; background publication continues |\n\nA crash after central acknowledgement but before local outbox deletion may\nresend the same event ID. The central event log is expected to deduplicate that\nidentity.\n\nHTTP `200` proves durable local acceptance or central acknowledgement. It does\nnot always prove that central delivery completed before the response.\n\n## Public HTTP responses\n\n| Condition | Status | Body code or fields |\n|---|---:|---|\n| Missing or invalid required header encoding | `400` | `webhook.missing_header` |\n| Invalid body, timestamp, nonce, signature, HMAC, duplicate, JSON, payload field, workspace, or event mapping | `401` | `webhook.authentication_failed` |\n| Replay-store, event-log, or durable-outbox failure | `503` | `webhook.storage_unavailable` |\n| Accepted and centrally acknowledged | `200` | `accepted_events: 1`, `central_delivery: centrally_acknowledged` |\n| Accepted and durably queued | `200` | `accepted_events: 1`, `central_delivery: durably_queued` |\n\nThe common HTTP body layer can reject an oversized request before the handler.\nDo not depend on one JSON error code for that proxy or framework-level failure.\n\nThe public `401` intentionally collapses validation details. Diagnose through\nbounded internal telemetry without logging secret or payload content.\n\n## Operate and recover the boundary\n\n| Symptom | First decision |\n|---|---|\n| Repeated `400` | Verify exact header names and non-empty HTTP values |\n| Repeated `401` | Check clock, raw-byte preservation, configured workspace, secret revision, and nonce reuse |\n| `503` before acceptance | Preserve replay, event-log, profile-state, PostgreSQL, and outbox evidence |\n| Success reports `durably_queued` | Monitor the durable publisher and central acknowledgement; do not ask provider to resend |\n| Replay capacity reached | Check clock, traffic volume, stale-state pruning, and abuse before changing instance identity |\n| CAS contention persists | Check replica count, shared scope, database latency, and competing state ownership |\n| Central event is absent | Search by deterministic event ID and outbox batch before changing registration |\n| Event belongs to another workspace | Reject it and reconcile provider registration with host binding |\n\nDo not clear replay state, rotate an instance ID, replace the event ID, or ask\nfor same-nonce replay merely to make a delivery pass. Those actions can weaken\ndeduplication or lose the original failure evidence.\n\n## Security and evidence boundaries\n\nRetain:\n\n- host artifact, instance, replica, tenant, workspace, and secret revision;\n- redacted provider registration identity;\n- receipt time and bounded header validation outcomes;\n- deterministic event ID and kind;\n- replay-store scope and admission outcome;\n- local event-log append identity;\n- outbox batch identity and central outcome;\n- recovery actions and final central observation.\n\nDo not retain secret bytes, full raw payload, or unredacted personal data in\nordinary logs. Store any necessary payload evidence under explicit tenant,\naccess, and retention controls.\n\nSource review establishes this implemented pipeline. It does not establish\nthat a particular deployment, provider registration, artifact, or live event\nhas passed qualification.\n\n## Related documentation\n\n- [Twenty connector configuration](configuration.md)\n- [Authentication and workspace isolation](../getting-started/authentication-and-workspace-isolation.md)\n- [Manage views, layouts, and webhook metadata](../guides/manage-views-layouts-and-webhook-metadata.md)\n- [Connector fleet architecture](../../../architecture/connector-fleet.md)\n- [Errors and retry decisions](../../../reference/errors.md)\n",
    "text": "Twenty webhook security, replay, and events\n\nThis reference defines the reviewed Twenty inbound webhook boundary. It covers\nroute activation, exact raw-body HMAC, clock and workspace checks, durable nonce\nreplay fencing, deterministic event mapping, local acceptance, and central AIP\npublication.\n\nUse the metadata guide to create or rotate provider registration. This page\nstarts when an external HTTP delivery reaches the standalone connector host.\n\nStatus and route\n\n| Item | Reviewed value |\n\n| AIP source revision | 97be86e9efedf07ecf1783b03800f683f107fb04 |\n| Method | POST |\n| Internal host route | /webhooks/twenty |\n| Timestamp header | x-twenty-webhook-timestamp |\n| Signature header | x-twenty-webhook-signature |\n| Nonce header | x-twenty-webhook-nonce |\n| Body limit | 4 MiB |\n| Signature | HMAC-SHA256 over timestamp || \":\" || rawbody |\n| Accepted clock skew | 300,000 ms in either direction |\n| Replay scope | Logical connector instance |\n| Live replay capacity | 100,000 nonces |\n| Local event channel | twenty-webhooks |\n\nThe route exists only when AIPTWENTYWEBHOOKSECRETFILE is configured.\nEnabling it also requires AIPCONNECTORHOSTEVENTENDPOINT; otherwise host\nstartup fails.\n\nAn external reverse proxy may expose a different HTTPS origin and prefix. It\nmust preserve all three headers and the request body byte for byte when\nforwarding to the internal route.\n\nWhat the signature authenticates\n\nThe verification equation is:\n\nexpected = HMAC-SHA256(\n  configuredwebhooksecret,\n  timestampheaderbytes || \":\" || exactrawbodybytes\n)\n\nhexdecode(signatureheader) == expected\n\nThe connector decodes exactly 64 hexadecimal signature characters and uses the\nHMAC library's verification method. Uppercase and lowercase hex are accepted.\n\nThe nonce is not an HMAC input. It is validated and fenced separately. The\nHTTP route and header names are also outside the signature input.\n\nPossession of the secret authenticates the timestamp and exact body. It does\nnot prove that Twenty generated a business fact, that the configured provider\nregistration belongs to the expected operator, or that the body is true.\n\nIngress pipeline\n\nThe standalone host processes one delivery in this order:\n1. apply the 4 MiB HTTP body limit;\n2. require non-empty timestamp, signature, and nonce headers;\n3. require a non-empty body no larger than 4 MiB;\n4. validate 32 hexadecimal nonce characters;\n5. validate 64 hexadecimal signature characters;\n6. parse a 1-to-32-digit Unix-millisecond timestamp;\n7. require absolute host-clock skew at most 300,000 ms;\n8. verify HMAC over timestamp, colon, and exact raw body;\n9. durably admit the nonce in the instance replay scope;\n10. parse the signed body as a JSON object;\n11. validate event name, configured workspace, webhook UUID, and event date;\n12. derive a deterministic AIP event;\n13. append it to the local event log;\n14. durably enqueue it on channel twenty-webhooks;\n15. attempt immediate central delivery;\n16. return HTTP 200 after durable local acceptance.\n\nCentral acknowledgement is optional at step 15. When it is unavailable, the\nbackground publisher owns a durably queued batch.\n\nValidate the headers and clock\n\n| Value | Constraint |\n\n| Timestamp | Decimal Unix milliseconds, 1 through 32 digits |\n| Signature | Exactly 64 ASCII hexadecimal characters |\n| Nonce | Exactly 32 ASCII hexadecimal characters |\n| Clock delta | Absolute difference from host time no greater than 300,000 ms |\n\nThe timestamp string itself is signed. Reformatting the same instant changes\nthe HMAC input.\n\nKeep host clocks synchronized. The connector does not contact Twenty or a time\nauthority to resolve clock disagreement.\n\nPreserve exact raw-body bytes\n\nThe connector computes HMAC before parsing JSON. Whitespace, key order,\nescaping, number formatting, or a trailing newline changes the signed bytes.\n\nA proxy or middleware must not parse and reserialize the body. It may enforce a\nsmaller ingress limit if it preserves accepted bodies and headers exactly.\n\nDo not log the secret, signature, full body, or mapped event data. The body can\ncontain confidential and personal workspace data.\n\nValidate the signed payload\n\nAfter HMAC and replay admission, the body must be a JSON object containing:\n\n{\n  \"eventName\": \"company.updated\",\n  \"workspaceId\": \"0190c42f-2d5a-7000-8000-000000000080\",\n  \"webhookId\": \"0190c42f-2d5a-7000-8000-000000000081\",\n  \"eventDate\": \"2030-01-03T10:00:00Z\"\n}\n\nEach named field must be a non-empty string. Additional provider fields remain\ninside the event data.\n\neventName accepts 1 through 256 ASCII alphanumeric, ., , or -\ncharacters. It is not restricted to a source-owned event-name enum.\n\nworkspaceId must equal the host's configured workspace string exactly.\nwebhookId must be an RFC 4122 UUID version 1 through 5. eventDate must parse\nas RFC 3339 and becomes the AIP event's occurredat.\n\nA valid signature from another workspace is rejected. The HMAC secret does not\nreplace workspace binding.\n\nReplay state\n\nThe standalone host uses a profile-state replay store with:\n\n| Property | Value |\n\n| Namespace | aip.connector.twenty.webhookreplay.v1 |\n| Scope | instance: |\n| Update | Profile-state compare-and-set |\n| CAS attempts | 32 |\n| Retained timestamps | Values at or after now - 300000 ms |\n| Capacity | 100,000 live nonces |\n\nEvery replica of one logical instance must share the same durable runtime\ndatabase and instance ID. Different instance IDs have independent replay\nscopes.\n\nBefore inserting a nonce, the store removes entries older than the cutoff. It\nrejects an existing nonce and rejects a new nonce when the live map already\ncontains 100,000 entries.\n\nCAS conflicts yield and retry. Continued contention after 32 attempts is a\nreplay-store failure, not an authentication success.\n\nThe connector also contains an in-memory implementation for tests and\nsingle-process development. The standalone host constructs the profile-state\nimplementation.\n\nUnderstand the admission-order boundary\n\nThe replay store admits a verified nonce before JSON parsing, payload-field\nvalidation, local event-log append, or outbox enqueue.\n\nAn authenticated malformed payload therefore consumes its nonce. A failure in\nthe later local event-log or outbox step also occurs after nonce admission.\n\nRepeating the exact delivery with the same nonce then receives a public\nauthentication failure. Provider retry is not the recovery mechanism for a\nlate local failure at this revision.\n\nPreserve host storage evidence and inspect event log, replay state, and outbox\nbefore changing state. Escalate according to deployment policy when a verified\ndelivery was fenced but not durably published.\n\nMap the deterministic event\n\nThe connector calculates:\n\ndigest = SHA-256(webhookid || \":\" || nonce || \":\" || signatureheader)\neventid = \"evt\" || hex(first16digestbytes)\neventkind = \"twenty.\" || eventName\n\nThe event records:\n\n| Field | Value |\n\n| id | evt plus 32 lowercase hexadecimal characters |\n| kind | twenty. |\n| occurredat | Parsed signed eventDate |\n| data | Complete parsed provider object |\n\nThe event ID does not include raw-body bytes directly. Signature changes alter\nthe ID, while the replay fence separately owns nonce uniqueness.\n\nBefore central publication, the common publisher projects a provider-originated\nactor into data.upstreamactor. Authenticated central ingress owns the trusted\nprotocol-level event actor and binds it to connector-host signing identity.\n\nLocal event log and central outbox\n\nThe host appends the deterministic event to its local AIP event log. Duplicate\nevent IDs reuse the log's accepted event outcome rather than creating a new\nevent identity.\n\nIt then durably enqueues one-event batch on channel twenty-webhooks. The\npublisher derives a stable batch key from normalized channel and event data.\n\nThe acknowledgement body reports one of two central outcomes:\n\n| centraldelivery | Meaning at HTTP response time |\n\n| centrallyacknowledged | Central AIP acknowledged the batch |\n| durablyqueued | Local durable outbox owns delivery; background publication continues |\n\nA crash after central acknowledgement but before local outbox deletion may\nresend the same event ID. The central event log is expected to deduplicate that\nidentity.\n\nHTTP 200 proves durable local acceptance or central acknowledgement. It does\nnot always prove that central delivery completed before the response.\n\nPublic HTTP responses\n\n| Condition | Status | Body code or fields |\n\n| Missing or invalid required header encoding | 400 | webhook.missingheader |\n| Invalid body, timestamp, nonce, signature, HMAC, duplicate, JSON, payload field, workspace, or event mapping | 401 | webhook.authenticationfailed |\n| Replay-store, event-log, or durable-outbox failure | 503 | webhook.storageunavailable |\n| Accepted and centrally acknowledged | 200 | acceptedevents: 1, centraldelivery: centrallyacknowledged |\n| Accepted and durably queued | 200 | acceptedevents: 1, centraldelivery: durablyqueued |\n\nThe common HTTP body layer can reject an oversized request before the handler.\nDo not depend on one JSON error code for that proxy or framework-level failure.\n\nThe public 401 intentionally collapses validation details. Diagnose through\nbounded internal telemetry without logging secret or payload content.\n\nOperate and recover the boundary\n\n| Symptom | First decision |\n\n| Repeated 400 | Verify exact header names and non-empty HTTP values |\n| Repeated 401 | Check clock, raw-byte preservation, configured workspace, secret revision, and nonce reuse |\n| 503 before acceptance | Preserve replay, event-log, profile-state, PostgreSQL, and outbox evidence |\n| Success reports durablyqueued | Monitor the durable publisher and central acknowledgement; do not ask provider to resend |\n| Replay capacity reached | Check clock, traffic volume, stale-state pruning, and abuse before changing instance identity |\n| CAS contention persists | Check replica count, shared scope, database latency, and competing state ownership |\n| Central event is absent | Search by deterministic event ID and outbox batch before changing registration |\n| Event belongs to another workspace | Reject it and reconcile provider registration with host binding |\n\nDo not clear replay state, rotate an instance ID, replace the event ID, or ask\nfor same-nonce replay merely to make a delivery pass. Those actions can weaken\ndeduplication or lose the original failure evidence.\n\nSecurity and evidence boundaries\n\nRetain:\n• host artifact, instance, replica, tenant, workspace, and secret revision;\n• redacted provider registration identity;\n• receipt time and bounded header validation outcomes;\n• deterministic event ID and kind;\n• replay-store scope and admission outcome;\n• local event-log append identity;\n• outbox batch identity and central outcome;\n• recovery actions and final central observation.\n\nDo not retain secret bytes, full raw payload, or unredacted personal data in\nordinary logs. Store any necessary payload evidence under explicit tenant,\naccess, and retention controls.\n\nSource review establishes this implemented pipeline. It does not establish\nthat a particular deployment, provider registration, artifact, or live event\nhas passed qualification.\n\nRelated documentation\n• Twenty connector configuration (configuration.md)\n• Authentication and workspace isolation (../getting-started/authentication-and-workspace-isolation.md)\n• Manage views, layouts, and webhook metadata (../guides/manage-views-layouts-and-webhook-metadata.md)\n• Connector fleet architecture (../../../architecture/connector-fleet.md)\n• Errors and retry decisions (../../../reference/errors.md)\n"
  },
  "integrity": {
    "algorithm": "sha256",
    "sourceDigest": "490bac20a32d102983a464e355a7d086d4e27b7d05316ac9778433895433e8b2"
  }
}
