Signed outbound webhooks with HMAC over the raw body, a stable delivery identity, bounded retries into a dead-letter, clock-skew tolerant timestamps, and no secret material in callback URLs.
Package status: reference context ready for human review. The contract and test scenarios are complete, but no claim is made that an adopting implementation has passed them.
Decision
Outbound integration events are delivered as signed HTTP POST bodies. Each delivery has a durable identity distinct from the domain event id so retries remain the same delivery. HMAC covers the raw bytes that will be transmitted, bound to a key id and sender timestamp. Secrets never appear as query parameters. Retries follow a named schedule profile with labeled jitter, then enter dead-letter. Delivery is at-least-once; receivers deduplicate on delivery id.
Scope
- Worker attempts after an event is committed to an outbox.
- Signing headers, timestamp skew, key rotation identifiers, retry bound, dead-letter, and terminal states.
Outside this block
- Atomic outbox writes with the domain transaction (related outbox package).
- How a partner verifies inbound signatures (related inbound signature package).
Contract
- Every HTTP attempt carries a stable delivery id, incrementing attempt number, key id, timestamp, and HMAC over the exact body bytes.
- The callback URL contains no signing secret, token, or tenant credential; credentials live in headers or the subscription record.
- Retryable partner statuses follow the schedule profile; non-retryable client statuses terminate without further attempts.
- After the profile maximum attempt, state is dead-lettered and terminal-failed; operator replay allocates a new delivery id.
- A timestamp outside the replay window is not sent as a new identity for the same delivery; the worker signs a current timestamp on retry or marks the attempt expired.
- Duplicate worker wakes reuse the same delivery id and do not create a second in-flight family.
Implementation guidance
- Publish through the outbox so the worker can crash and resume. Canonicalize the body once before signing; never re-serialize between sign and send.
- Rotate keys by advertising a new key id while the previous key remains valid for in-flight retries.
Failure handling and safeguards
- Transport timeouts are retryable and consume an attempt. Redirects to a host not on the subscription allowlist are terminal-failed.
- Missing signing material fails the attempt as an operator defect without sending an unsigned body.
Verification and operations
- Assert signed bytes equal the on-wire body and that the URL query contains no secrets. Chaos the worker after send-before-ack and prove one delivery id with two attempts.
The executable-looking examples in this package are fixtures and acceptance contracts. Run
python tools/validate.py from the collection root to check package structure and metadata; then
implement and execute the scenarios in the target repository.
Adoption assumptions
- Names and numeric values in
example.yamlare an adoption profile, not universal defaults. - The adopting team must map actors, data classes, error vocabulary, and ownership to its system.