{
  "schemaVersion": "1.0",
  "title": "Cal.diy webhook security and replay",
  "description": "This reference defines the reviewed Cal.diy inbound webhook boundary. It covers route and header validation, raw-body HMAC, version and time checks, deterministic event mapping, bounded replay state, idempotent duplicate handling, durable l",
  "canonical": "https://getaip.org/docs/connectors/cal-diy/reference/webhook-security-and-replay",
  "route": "/docs/connectors/cal-diy/reference/webhook-security-and-replay",
  "source": "docs/connectors/cal-diy/reference/webhook-security-and-replay.md",
  "protocol": "Agent Interoperability Protocol",
  "protocolVersion": "1.0",
  "section": "Connectors",
  "documentType": "Connector",
  "language": "en",
  "downloads": {
    "md": "/docs/download/connectors/cal-diy/reference/webhook-security-and-replay.md",
    "txt": "/docs/download/connectors/cal-diy/reference/webhook-security-and-replay.txt",
    "json": "/docs/download/connectors/cal-diy/reference/webhook-security-and-replay.json",
    "pdf": "/docs/download/connectors/cal-diy/reference/webhook-security-and-replay.pdf"
  },
  "content": {
    "format": "text/markdown",
    "markdown": "---\ntitle: Cal.diy webhook security and replay\ndescription: Verify signed deliveries, map deterministic events, and acknowledge only durable local acceptance\nkind: reference\naudience: security-reviewer\nappliesTo: \"1.x\"\nwritingStandard: \"aip-docs/1.0\"\nlastReviewedRevision: \"97be86e9efedf07ecf1783b03800f683f107fb04\"\nconnector: cal-diy\n---\n\n# Cal.diy webhook security and replay\n\nThis reference defines the reviewed Cal.diy inbound webhook boundary. It covers\nroute and header validation, raw-body HMAC, version and time checks,\ndeterministic event mapping, bounded replay state, idempotent duplicate\nhandling, durable local acceptance, and central AIP delivery.\n\nUse the webhook capability pages to create or change subscriptions. 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| Connector source revision | `97be86e9efedf07ecf1783b03800f683f107fb04` |\n| Method | `POST` |\n| Internal host route | `/webhooks/cal-diy/{subscription_id}` |\n| Required signature header | `x-cal-signature-256` |\n| Required version header | `x-cal-webhook-version` |\n| Supported payload version | `2021-10-20` |\n| Body limit | 1 MiB |\n| Signature | HMAC-SHA256 over the exact raw body |\n| Replay retention | 86,400 seconds |\n| Replay capacity | 20,000 delivery digests per scope |\n\nThe route is registered only when at least one webhook secret mapping is\nconfigured. Enabling it also requires the connector-host event endpoint. When\nno secret mapping is configured, the reviewed host does not mount this route.\n\nAn external reverse proxy may expose a different HTTPS origin and prefix. It\nmust preserve the final `subscription_id` path segment, both required headers,\nand the request body byte for byte when forwarding to the internal route.\n\n## What the signature authenticates\n\nThe signature proves possession of the secret selected by the validated\n`subscription_id` route and authenticates the exact raw body bytes. The\nverification equation is:\n\n```text\nhex(HMAC-SHA256(selected_secret, raw_body_bytes)) == x-cal-signature-256\n```\n\nThe path selector and `x-cal-webhook-version` header are not HMAC inputs. The\nconnector validates them separately. Subscription creation binds the allowed\nHTTPS destination to the same opaque secret reference, but ingress must still\npreserve the trusted route and host configuration.\n\nThe signature is not proof that Cal.diy generated a particular business fact.\nIt proves that the delivery matches one configured secret and body. Provider\naccount ownership, subscription configuration, and secret custody remain\ndeployment responsibilities.\n\n## Ingress pipeline\n\nThe standalone host processes one delivery in this order:\n\n1. apply the 1 MiB HTTP body limit;\n2. require non-empty signature and version headers;\n3. require the request body to be valid UTF-8 without reserializing it;\n4. require version `2021-10-20`;\n5. validate the route selector and resolve its startup-loaded secret;\n6. verify the 64-character hexadecimal HMAC in constant time;\n7. derive a deterministic digest from the verified delivery;\n8. parse the signed payload, validate its timestamp, and map its trigger;\n9. observe the digest in the bounded shared replay store;\n10. append the deterministic event to the local AIP event log;\n11. durably enqueue the event for signed central publication;\n12. return HTTP `200` after local durable acceptance.\n\nCentral acknowledgement can happen during step 11, but it is not required for\nHTTP success. A durably queued batch belongs to the background publisher after\nthe response.\n\n## Route selector and secret lookup\n\n`subscription_id` is an opaque secret reference, not secret material. Its exact\ngrammar is:\n\n- 1 to 128 ASCII bytes;\n- first byte is alphanumeric;\n- remaining bytes are alphanumeric or `.`, `_`, `~`, or `-`;\n- no slash, whitespace, empty value, or duplicate configured reference.\n\nThe path value selects one secret from the deployment mapping loaded at host\nstartup. An unknown or malformed selector and an invalid signature share the\nsame public authentication failure, so the response does not reveal whether a\nparticular reference exists.\n\n## Raw-body HMAC verification\n\n`x-cal-signature-256` must contain exactly 64 hexadecimal characters. Uppercase\nand lowercase hex are both decodable. The connector computes HMAC-SHA256 over\nthe exact UTF-8 bytes received and compares the 32-byte values in constant time.\n\nWhitespace, object-key order, escaping, or a trailing newline changes the\nsigned bytes. A proxy, middleware, or application must not parse and reserialize\nthe JSON before the connector verifies it.\n\nDo not log the raw body, signature, resolved secret, or mapped event payload.\nWebhook bodies can contain attendee, booking, meeting, recording, calendar, or\nform data and must remain under restricted PII controls.\n\n## Payload forms\n\n### Standard payload\n\nWhen the top-level `payload` field is present, the signed JSON must deserialize\nto all three fields below:\n\n```json\n{\n  \"triggerEvent\": \"BOOKING_CREATED\",\n  \"createdAt\": \"2030-01-03T10:00:00Z\",\n  \"payload\": {\n    \"uid\": \"provider-booking-uid\"\n  }\n}\n```\n\n`triggerEvent` and `createdAt` must be strings. `createdAt` must parse as\nRFC 3339. `payload` may contain any provider JSON shape, including `null`; the\nconnector does not publish one exhaustive trigger-specific payload schema.\n\nManaged webhook creation rejects a custom `payloadTemplate`, so newly managed\nsubscriptions use the standard provider shape.\n\n### Flat compatibility payload\n\nIf the top-level `payload` field is absent, the connector accepts a compatibility\nform. It removes `triggerEvent` and optional `createdAt` and treats every\nremaining field as the event payload:\n\n```json\n{\n  \"triggerEvent\": \"BOOKING_CANCELLED\",\n  \"createdAt\": \"2030-01-03T10:05:00Z\",\n  \"uid\": \"provider-booking-uid\",\n  \"reason\": \"Attendee request\"\n}\n```\n\nIf this flat form omits `createdAt`, or supplies it with a non-string value, the\nconnector substitutes its trusted receipt time. The resulting event currently still records\n`timestampProvenance: \"signed_body\"`. Do not use that label alone as evidence\nthat the original flat body contained a timestamp. Record a redacted marker\nwhen the compatibility fallback is used and timestamp provenance matters.\n\n## Version and timestamp checks\n\nThe required `x-cal-webhook-version` value is exactly `2021-10-20`. The header\nis not covered by HMAC, but its accepted value is fixed in source and is also\nforced into managed webhook create and update requests.\n\nFor a parsed `createdAt`, define:\n\n```text\ndelta = trusted_received_at - signed_created_at\n```\n\nThe accepted inclusive interval is `-300 <= delta <= 86400` seconds. A delivery\nmay therefore be at most five minutes in the future or 24 hours old relative to\nthe host clock. Keep connector-host clocks synchronized; the connector does not\ncontact the provider to resolve clock disagreement.\n\n## Trigger-to-event mapping\n\nOnly these 19 provider triggers are accepted:\n\n| `triggerEvent` | AIP event kind |\n|---|---|\n| `BOOKING_CREATED` | `cal_diy.booking.created` |\n| `BOOKING_PAYMENT_INITIATED` | `cal_diy.booking.payment_initiated` |\n| `BOOKING_PAID` | `cal_diy.booking.paid` |\n| `BOOKING_RESCHEDULED` | `cal_diy.booking.rescheduled` |\n| `BOOKING_REQUESTED` | `cal_diy.booking.requested` |\n| `BOOKING_CANCELLED` | `cal_diy.booking.cancelled` |\n| `BOOKING_REJECTED` | `cal_diy.booking.rejected` |\n| `BOOKING_NO_SHOW_UPDATED` | `cal_diy.booking.no_show_updated` |\n| `FORM_SUBMITTED` | `cal_diy.form.submitted` |\n| `MEETING_ENDED` | `cal_diy.meeting.ended` |\n| `MEETING_STARTED` | `cal_diy.meeting.started` |\n| `RECORDING_READY` | `cal_diy.recording.ready` |\n| `RECORDING_TRANSCRIPTION_GENERATED` | `cal_diy.recording.transcription_generated` |\n| `OOO_CREATED` | `cal_diy.out_of_office.created` |\n| `AFTER_HOSTS_CAL_VIDEO_NO_SHOW` | `cal_diy.meeting.host_no_show` |\n| `AFTER_GUESTS_CAL_VIDEO_NO_SHOW` | `cal_diy.meeting.guest_no_show` |\n| `FORM_SUBMITTED_NO_EVENT` | `cal_diy.form.submitted_without_event` |\n| `DELEGATION_CREDENTIAL_ERROR` | `cal_diy.delegation.credential_error` |\n| `WRONG_ASSIGNMENT_REPORT` | `cal_diy.booking.wrong_assignment_reported` |\n\nAn unknown trigger is an invalid delivery. The connector does not derive a\nfallback event kind from arbitrary provider text.\n\n## Deterministic delivery and event identity\n\nAfter HMAC verification, the connector computes:\n\n```text\ndelivery_digest = SHA-256(\n  subscription_id || NUL ||\n  webhook_version || NUL ||\n  lowercase(signature_hex) || NUL ||\n  raw_body_bytes\n)\n\nevent_id = \"evt_cal_diy_\" || hex(delivery_digest)\n```\n\nLowercasing the hexadecimal signature makes equivalent uppercase and lowercase\nrepresentations produce the same event id. The digest is a replay and identity\nkey; it is not a second authentication mechanism.\n\nThe mapped event has:\n\n| Field | Value |\n|---|---|\n| `id` | `evt_cal_diy_<64 lowercase hex characters>` |\n| `kind` | Fixed value from the trigger table |\n| `occurred_at` | Parsed `createdAt` or flat-form receipt fallback |\n| `session_id`, `action_id`, `correlation_id` | Absent |\n| Local upstream actor | Service principal `service:cal_diy:webhook` |\n| `data.triggerEvent` | Original supported trigger |\n| `data.createdAt` | Parsed or substituted timestamp string |\n| `data.payload` | Provider payload JSON |\n| `data.webhookVersion` | `2021-10-20` |\n| `data.subscriptionId` | Validated route selector |\n| `data.deliveryDigest` | Lowercase delivery digest |\n| `data.timestampProvenance` | Current fixed value `signed_body` |\n\nBefore central publication, the connector-host publisher moves the local\nprovider actor into `data.upstream_actor`. The authenticated central ingress\nbinds the protocol-level event actor to the connector-host signing identity.\nThis preserves provider provenance without letting an external webhook assert a\ntrusted AIP actor.\n\n## Replay state and exact duplicates\n\nThe standalone host uses a cluster-safe profile-state replay store. Its scope is\n`instance:<cal_diy_account_id>`, so replicas for the same logical account must\nshare the same durable runtime database and account id.\n\n| Store implementation | Intended boundary |\n|---|---|\n| Profile-state CAS | Standalone host and shared multi-replica scope |\n| Atomic-replacement file | Durable single-process library use only |\n| In-memory map | Tests or ephemeral single-process use |\n\nThe store removes entries older than 86,400 seconds and retains at most 20,000\ndigests. At capacity it removes the oldest retained entry before inserting a\nnew one. Profile-state admission retries a contended compare-and-set at most 32\ntimes.\n\nThe replay store is not the final deduplication boundary. Its `false` result for\na seen digest is intentionally not an ingestion error. The connector returns\nthe same deterministic event, and the local AIP event log resolves the repeated\nevent id idempotently.\n\nConsequently, an exact provider retry that still passes the current version,\nsecret, body, and timestamp checks receives normal success after durable\nhandling. It is not rejected with HTTP `409`. The central event path can also\nsee the same event id after a retry or crash and must deduplicate it while the\nevent identity is retained.\n\nDifferent raw bytes produce a different digest even when they describe the same\nprovider object. This boundary deduplicates exact signed deliveries, not every\nsemantic duplicate the provider could emit.\n\n## Durable acknowledgement and crash windows\n\nThe host acknowledges after two local durability steps: event-log append and\nevent-outbox enqueue. The outbox attempts central delivery immediately, then a\nbackground worker owns any retained batch.\n\n| Interruption or duplicate point | Reviewed behavior |\n|---|---|\n| Crash before replay admission | Provider retry repeats full verification |\n| Replay admitted, event append fails | Host returns `503`; retry recreates the same event id |\n| Event appended, outbox enqueue fails | Host returns `503`; retry reuses the event and retries local enqueue |\n| Outbox durable, central endpoint unavailable | Host returns `200` with `durably_queued`; worker retries |\n| Central acknowledgement succeeds | Host returns `200` with `centrally_acknowledged` |\n| Crash after central acknowledgement but before outbox deletion | Exact event id may be sent again; central log deduplicates |\n| Exact provider delivery repeats within current validation windows | Same event id and deterministic outbox batch are used |\n\nThis design reduces loss and duplicate effects, but it is not an exactly-once\ntransport claim. Retention, storage health, identity preservation, and central\nevent-log behavior remain part of the delivery guarantee.\n\n## HTTP responses\n\n### Success\n\nThe reviewed mapper produces one event. A successful response is HTTP `200`:\n\n```json\n{\n  \"accepted_events\": 1,\n  \"central_delivery\": \"durably_queued\"\n}\n```\n\n`central_delivery` is either `centrally_acknowledged` or `durably_queued`.\nBoth mean the event was accepted durably by the connector host. Only the first\nmeans the central ingress acknowledged it before the HTTP response.\n\n### Failures\n\n| Condition | HTTP status | Error code |\n|---|---:|---|\n| Missing or empty required header | `400` | `webhook.missing_header` |\n| Body is not valid UTF-8 | `400` | `webhook.invalid_utf8` |\n| Unknown selector, malformed signature, or HMAC mismatch | `401` | `webhook.authentication_failed` |\n| Unsupported version, invalid JSON, time window, or trigger | `400` | `webhook.invalid_delivery` |\n| Replay-state, event-log, or pre-enqueue outbox storage failure | `503` | `webhook.storage_unavailable` |\n| Connector unexpectedly maps a non-event envelope | `500` | `webhook.invalid_mapping` |\n\nA body rejected by the HTTP body-limit layer may not reach the handler's JSON\nerror mapping. Do not depend on a connector error code for an oversized body.\n\nErrors intentionally do not echo signatures, secret references, raw payloads,\nor provider PII.\n\n## Operational review checklist\n\nVerify that:\n\n- the external HTTPS route maps one final path segment to `subscription_id`;\n- the proxy preserves the exact body and both required headers;\n- each configured selector maps to one non-empty owner-controlled secret file;\n- the managed subscriber URL and selector use the same reviewed binding;\n- every replica for one account shares PostgreSQL profile state and event state;\n- the connector-host event endpoint is configured before webhook ingress starts;\n- host clocks stay within the five-minute future-skew boundary;\n- raw bodies, signatures, secrets, and mapped payloads are absent from logs;\n- local event-log, replay-state, outbox depth, and central delivery failures are\n  monitored separately;\n- a recovery test covers valid, invalid-signature, stale, exact-duplicate, local\n  storage failure, and central-unavailable deliveries.\n\nPassing this checklist validates deployment handling. It does not prove that a\ncurrent Cal.diy account is sending every configured trigger.\n\n## Diagnose an ingress failure\n\n| Symptom | First evidence to inspect | Safe response |\n|---|---|---|\n| `webhook.missing_header` | Proxy forwarding and exact header names | Restore both headers; do not synthesize a signature |\n| `webhook.invalid_utf8` | Raw request encoding before any JSON parser | Reject the delivery and preserve redacted transport evidence |\n| `webhook.authentication_failed` | Route selector, secret revision, signature length, and raw-byte preservation | Compare configuration without logging secret material |\n| `webhook.invalid_delivery` | Version, JSON form, `createdAt`, host clock, and trigger enum | Correct the subscription or compatibility mismatch |\n| `webhook.storage_unavailable` | Replay CAS, local event log, and durable outbox | Restore local durability; allow the provider to retry `503` |\n| Success with `durably_queued` | Outbox depth, active host lease, central route, and network | Do not ask the provider to replay; let the durable worker deliver |\n| Repeated central event id | Local outbox recovery and central idempotent append | Treat it as expected retry evidence, not a new business event |\n| Same business object with different event ids | Raw-body and signature differences | Handle semantic duplication at the consumer if required |\n\n## Security and evidence boundaries\n\n- HMAC covers the raw body, not the route or version header.\n- The route selector chooses a deployment secret but is not itself a secret.\n- A bounded replay cache does not provide permanent semantic deduplication.\n- A deterministic event id does not prove exactly-once transport.\n- HTTP `200` can mean durable local queueing rather than central acknowledgement.\n- Event payloads remain restricted even after signature verification.\n- Source and deterministic tests do not prove current external delivery.\n\n## Related documentation\n\n- [User webhook capabilities](../capabilities/webhooks.md)\n- [Event-type webhook capabilities](../capabilities/event-types-and-private-links.md)\n- [Cal.diy configuration](configuration.md)\n- [Security model](../../../architecture/security-model.md)\n- [Cal.diy isolated-live evidence](../../../testing/cal-diy-isolated-live.md)\n",
    "text": "Cal.diy webhook security and replay\n\nThis reference defines the reviewed Cal.diy inbound webhook boundary. It covers\nroute and header validation, raw-body HMAC, version and time checks,\ndeterministic event mapping, bounded replay state, idempotent duplicate\nhandling, durable local acceptance, and central AIP delivery.\n\nUse the webhook capability pages to create or change subscriptions. This page\nstarts when an external HTTP delivery reaches the standalone connector host.\n\nStatus and route\n\n| Item | Reviewed value |\n\n| Connector source revision | 97be86e9efedf07ecf1783b03800f683f107fb04 |\n| Method | POST |\n| Internal host route | /webhooks/cal-diy/{subscriptionid} |\n| Required signature header | x-cal-signature-256 |\n| Required version header | x-cal-webhook-version |\n| Supported payload version | 2021-10-20 |\n| Body limit | 1 MiB |\n| Signature | HMAC-SHA256 over the exact raw body |\n| Replay retention | 86,400 seconds |\n| Replay capacity | 20,000 delivery digests per scope |\n\nThe route is registered only when at least one webhook secret mapping is\nconfigured. Enabling it also requires the connector-host event endpoint. When\nno secret mapping is configured, the reviewed host does not mount this route.\n\nAn external reverse proxy may expose a different HTTPS origin and prefix. It\nmust preserve the final subscriptionid path segment, both required headers,\nand the request body byte for byte when forwarding to the internal route.\n\nWhat the signature authenticates\n\nThe signature proves possession of the secret selected by the validated\nsubscriptionid route and authenticates the exact raw body bytes. The\nverification equation is:\n\nhex(HMAC-SHA256(selectedsecret, rawbodybytes)) == x-cal-signature-256\n\nThe path selector and x-cal-webhook-version header are not HMAC inputs. The\nconnector validates them separately. Subscription creation binds the allowed\nHTTPS destination to the same opaque secret reference, but ingress must still\npreserve the trusted route and host configuration.\n\nThe signature is not proof that Cal.diy generated a particular business fact.\nIt proves that the delivery matches one configured secret and body. Provider\naccount ownership, subscription configuration, and secret custody remain\ndeployment responsibilities.\n\nIngress pipeline\n\nThe standalone host processes one delivery in this order:\n1. apply the 1 MiB HTTP body limit;\n2. require non-empty signature and version headers;\n3. require the request body to be valid UTF-8 without reserializing it;\n4. require version 2021-10-20;\n5. validate the route selector and resolve its startup-loaded secret;\n6. verify the 64-character hexadecimal HMAC in constant time;\n7. derive a deterministic digest from the verified delivery;\n8. parse the signed payload, validate its timestamp, and map its trigger;\n9. observe the digest in the bounded shared replay store;\n10. append the deterministic event to the local AIP event log;\n11. durably enqueue the event for signed central publication;\n12. return HTTP 200 after local durable acceptance.\n\nCentral acknowledgement can happen during step 11, but it is not required for\nHTTP success. A durably queued batch belongs to the background publisher after\nthe response.\n\nRoute selector and secret lookup\n\nsubscriptionid is an opaque secret reference, not secret material. Its exact\ngrammar is:\n• 1 to 128 ASCII bytes;\n• first byte is alphanumeric;\n• remaining bytes are alphanumeric or ., , , or -;\n• no slash, whitespace, empty value, or duplicate configured reference.\n\nThe path value selects one secret from the deployment mapping loaded at host\nstartup. An unknown or malformed selector and an invalid signature share the\nsame public authentication failure, so the response does not reveal whether a\nparticular reference exists.\n\nRaw-body HMAC verification\n\nx-cal-signature-256 must contain exactly 64 hexadecimal characters. Uppercase\nand lowercase hex are both decodable. The connector computes HMAC-SHA256 over\nthe exact UTF-8 bytes received and compares the 32-byte values in constant time.\n\nWhitespace, object-key order, escaping, or a trailing newline changes the\nsigned bytes. A proxy, middleware, or application must not parse and reserialize\nthe JSON before the connector verifies it.\n\nDo not log the raw body, signature, resolved secret, or mapped event payload.\nWebhook bodies can contain attendee, booking, meeting, recording, calendar, or\nform data and must remain under restricted PII controls.\n\nPayload forms\n\nStandard payload\n\nWhen the top-level payload field is present, the signed JSON must deserialize\nto all three fields below:\n\n{\n  \"triggerEvent\": \"BOOKINGCREATED\",\n  \"createdAt\": \"2030-01-03T10:00:00Z\",\n  \"payload\": {\n    \"uid\": \"provider-booking-uid\"\n  }\n}\n\ntriggerEvent and createdAt must be strings. createdAt must parse as\nRFC 3339. payload may contain any provider JSON shape, including null; the\nconnector does not publish one exhaustive trigger-specific payload schema.\n\nManaged webhook creation rejects a custom payloadTemplate, so newly managed\nsubscriptions use the standard provider shape.\n\nFlat compatibility payload\n\nIf the top-level payload field is absent, the connector accepts a compatibility\nform. It removes triggerEvent and optional createdAt and treats every\nremaining field as the event payload:\n\n{\n  \"triggerEvent\": \"BOOKINGCANCELLED\",\n  \"createdAt\": \"2030-01-03T10:05:00Z\",\n  \"uid\": \"provider-booking-uid\",\n  \"reason\": \"Attendee request\"\n}\n\nIf this flat form omits createdAt, or supplies it with a non-string value, the\nconnector substitutes its trusted receipt time. The resulting event currently still records\ntimestampProvenance: \"signedbody\". Do not use that label alone as evidence\nthat the original flat body contained a timestamp. Record a redacted marker\nwhen the compatibility fallback is used and timestamp provenance matters.\n\nVersion and timestamp checks\n\nThe required x-cal-webhook-version value is exactly 2021-10-20. The header\nis not covered by HMAC, but its accepted value is fixed in source and is also\nforced into managed webhook create and update requests.\n\nFor a parsed createdAt, define:\n\ndelta = trustedreceivedat - signedcreatedat\n\nThe accepted inclusive interval is -300  |\n| kind | Fixed value from the trigger table |\n| occurredat | Parsed createdAt or flat-form receipt fallback |\n| sessionid, actionid, correlationid | Absent |\n| Local upstream actor | Service principal service:caldiy:webhook |\n| data.triggerEvent | Original supported trigger |\n| data.createdAt | Parsed or substituted timestamp string |\n| data.payload | Provider payload JSON |\n| data.webhookVersion | 2021-10-20 |\n| data.subscriptionId | Validated route selector |\n| data.deliveryDigest | Lowercase delivery digest |\n| data.timestampProvenance | Current fixed value signedbody |\n\nBefore central publication, the connector-host publisher moves the local\nprovider actor into data.upstreamactor. The authenticated central ingress\nbinds the protocol-level event actor to the connector-host signing identity.\nThis preserves provider provenance without letting an external webhook assert a\ntrusted AIP actor.\n\nReplay state and exact duplicates\n\nThe standalone host uses a cluster-safe profile-state replay store. Its scope is\ninstance:, so replicas for the same logical account must\nshare the same durable runtime database and account id.\n\n| Store implementation | Intended boundary |\n\n| Profile-state CAS | Standalone host and shared multi-replica scope |\n| Atomic-replacement file | Durable single-process library use only |\n| In-memory map | Tests or ephemeral single-process use |\n\nThe store removes entries older than 86,400 seconds and retains at most 20,000\ndigests. At capacity it removes the oldest retained entry before inserting a\nnew one. Profile-state admission retries a contended compare-and-set at most 32\ntimes.\n\nThe replay store is not the final deduplication boundary. Its false result for\na seen digest is intentionally not an ingestion error. The connector returns\nthe same deterministic event, and the local AIP event log resolves the repeated\nevent id idempotently.\n\nConsequently, an exact provider retry that still passes the current version,\nsecret, body, and timestamp checks receives normal success after durable\nhandling. It is not rejected with HTTP 409. The central event path can also\nsee the same event id after a retry or crash and must deduplicate it while the\nevent identity is retained.\n\nDifferent raw bytes produce a different digest even when they describe the same\nprovider object. This boundary deduplicates exact signed deliveries, not every\nsemantic duplicate the provider could emit.\n\nDurable acknowledgement and crash windows\n\nThe host acknowledges after two local durability steps: event-log append and\nevent-outbox enqueue. The outbox attempts central delivery immediately, then a\nbackground worker owns any retained batch.\n\n| Interruption or duplicate point | Reviewed behavior |\n\n| Crash before replay admission | Provider retry repeats full verification |\n| Replay admitted, event append fails | Host returns 503; retry recreates the same event id |\n| Event appended, outbox enqueue fails | Host returns 503; retry reuses the event and retries local enqueue |\n| Outbox durable, central endpoint unavailable | Host returns 200 with durablyqueued; worker retries |\n| Central acknowledgement succeeds | Host returns 200 with centrallyacknowledged |\n| Crash after central acknowledgement but before outbox deletion | Exact event id may be sent again; central log deduplicates |\n| Exact provider delivery repeats within current validation windows | Same event id and deterministic outbox batch are used |\n\nThis design reduces loss and duplicate effects, but it is not an exactly-once\ntransport claim. Retention, storage health, identity preservation, and central\nevent-log behavior remain part of the delivery guarantee.\n\nHTTP responses\n\nSuccess\n\nThe reviewed mapper produces one event. A successful response is HTTP 200:\n\n{\n  \"acceptedevents\": 1,\n  \"centraldelivery\": \"durablyqueued\"\n}\n\ncentraldelivery is either centrallyacknowledged or durablyqueued.\nBoth mean the event was accepted durably by the connector host. Only the first\nmeans the central ingress acknowledged it before the HTTP response.\n\nFailures\n\n| Condition | HTTP status | Error code |\n\n| Missing or empty required header | 400 | webhook.missingheader |\n| Body is not valid UTF-8 | 400 | webhook.invalidutf8 |\n| Unknown selector, malformed signature, or HMAC mismatch | 401 | webhook.authenticationfailed |\n| Unsupported version, invalid JSON, time window, or trigger | 400 | webhook.invaliddelivery |\n| Replay-state, event-log, or pre-enqueue outbox storage failure | 503 | webhook.storageunavailable |\n| Connector unexpectedly maps a non-event envelope | 500 | webhook.invalidmapping |\n\nA body rejected by the HTTP body-limit layer may not reach the handler's JSON\nerror mapping. Do not depend on a connector error code for an oversized body.\n\nErrors intentionally do not echo signatures, secret references, raw payloads,\nor provider PII.\n\nOperational review checklist\n\nVerify that:\n• the external HTTPS route maps one final path segment to subscriptionid;\n• the proxy preserves the exact body and both required headers;\n• each configured selector maps to one non-empty owner-controlled secret file;\n• the managed subscriber URL and selector use the same reviewed binding;\n• every replica for one account shares PostgreSQL profile state and event state;\n• the connector-host event endpoint is configured before webhook ingress starts;\n• host clocks stay within the five-minute future-skew boundary;\n• raw bodies, signatures, secrets, and mapped payloads are absent from logs;\n• local event-log, replay-state, outbox depth, and central delivery failures are\n  monitored separately;\n• a recovery test covers valid, invalid-signature, stale, exact-duplicate, local\n  storage failure, and central-unavailable deliveries.\n\nPassing this checklist validates deployment handling. It does not prove that a\ncurrent Cal.diy account is sending every configured trigger.\n\nDiagnose an ingress failure\n\n| Symptom | First evidence to inspect | Safe response |\n\n| webhook.missingheader | Proxy forwarding and exact header names | Restore both headers; do not synthesize a signature |\n| webhook.invalidutf8 | Raw request encoding before any JSON parser | Reject the delivery and preserve redacted transport evidence |\n| webhook.authenticationfailed | Route selector, secret revision, signature length, and raw-byte preservation | Compare configuration without logging secret material |\n| webhook.invaliddelivery | Version, JSON form, createdAt, host clock, and trigger enum | Correct the subscription or compatibility mismatch |\n| webhook.storageunavailable | Replay CAS, local event log, and durable outbox | Restore local durability; allow the provider to retry 503 |\n| Success with durablyqueued | Outbox depth, active host lease, central route, and network | Do not ask the provider to replay; let the durable worker deliver |\n| Repeated central event id | Local outbox recovery and central idempotent append | Treat it as expected retry evidence, not a new business event |\n| Same business object with different event ids | Raw-body and signature differences | Handle semantic duplication at the consumer if required |\n\nSecurity and evidence boundaries\n• HMAC covers the raw body, not the route or version header.\n• The route selector chooses a deployment secret but is not itself a secret.\n• A bounded replay cache does not provide permanent semantic deduplication.\n• A deterministic event id does not prove exactly-once transport.\n• HTTP 200 can mean durable local queueing rather than central acknowledgement.\n• Event payloads remain restricted even after signature verification.\n• Source and deterministic tests do not prove current external delivery.\n\nRelated documentation\n• User webhook capabilities (../capabilities/webhooks.md)\n• Event-type webhook capabilities (../capabilities/event-types-and-private-links.md)\n• Cal.diy configuration (configuration.md)\n• Security model (../../../architecture/security-model.md)\n• Cal.diy isolated-live evidence (../../../testing/cal-diy-isolated-live.md)\n"
  },
  "integrity": {
    "algorithm": "sha256",
    "sourceDigest": "40a4fb650bb950d96e83dcf22cc2ba4a4b50e80b764eca9a7ad606274bce92f6"
  }
}
