You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Replace the CMP-specific Bearer callback assumption with a generic tenant-scoped, redacted, versioned HMAC callback outbox and make late/unmatched provider events converge safely.
Every Relay transport transition needed by a client projection must create durable callback work after the state commit. Callback loss is recovered through bounded retry and the authoritative message-status API. Valid provider events that cannot yet be correlated are deduplicated, retained redacted for seven days, retried for correlation, and made operator-visible if unresolved.
Baseline gaps
At the audited commit:
callbacks are represented by CmpCallback and configured through CMP-specific URL/token fields;
callback delivery uses a static Bearer token rather than timestamped HMAC signing;
payloads include full normalized email, contact identifiers, and merged message metadata;
the event contract is not a generic versioned tenant transport contract;
callback rows/delivery are tied to CMP naming and behavior;
provider-event correlation does not provide the required seven-day unmatched-event recovery/operator visibility contract; and
no website-safe callback/OpenAPI consumer fixture exists.
Scope
Generic callback endpoint configuration and secret boundary
Introduce tenant/client-scoped callback configuration independent of CMP product naming.
Use a separate callback signing secret from every API credential.
Obtain the retrievable signing secret only through an approved server-side secret boundary/reference; never return it after initial authorized provisioning and never place it in URLs, ordinary database metadata, logs, errors, audit rows, docs examples, screenshots, or test artifacts.
Support explicit callback enable/disable, endpoint URL, contract version, secret rotation with a bounded verification overlap, and immediate revocation/disablement.
Validate the callback destination against an explicit client configuration boundary. Do not permit arbitrary per-message callback URLs or redirects to unapproved origins.
Real secret provisioning and external endpoint configuration remain human/infrastructure work outside this issue; deterministic tests use non-secret fixtures.
Versioned redacted transport event
Define a canonical JSON event with stable serialization and explicit contract_version. It contains only:
raw provider payload, provider diagnostic, or unrestricted metadata; or
unrelated tenant/contact/campaign identifiers.
Persist the exact canonical body/hash used for retry so every attempt signs and sends the same logical event.
HMAC/timestamp delivery
Sign <timestamp>.<raw-body> with HMAC-SHA-256 using the client callback secret.
Send X-Relay-Timestamp, X-Relay-Signature, content type, event/contract identifiers, and no Bearer credential.
Store callback work in the same database transaction as, or through a durable transactionally coupled outbox created by, the authoritative state transition. A committed transition cannot be silently missing callback work.
Dispatch only after commit.
Retry timeout, connection, and retryable HTTP failures with bounded exponential backoff and jitter policy that remains deterministic under tests.
Treat duplicate successful acknowledgement as success and never create a second callback row for one client/event/contract.
Record attempts, response status class, next attempt, terminal failure, and safe error code without response bodies or protected headers.
Callback failure never rolls back or regresses authoritative Relay transport state.
Provider-event correlation and recovery
Deduplicate provider events by tenant/provider/event ID before applying state.
Validate and normalize provider ingress first; retain no unnecessary raw payload in the unmatched record.
When a valid event cannot be correlated to a Relay message:
store a redacted unmatched-event record with stable identity, safe event class, provider correlation hash/reference, occurrence/receipt time, retry schedule, and expiration;
retry correlation with bounded backoff for seven days;
apply the event exactly once if correlation later succeeds;
Provide safe metrics/status for callback backlog/oldest age/failures and unmatched-event count/oldest age/expiration.
Preserve a migration path for existing CMP callbacks only when explicitly bounded and tested; no new client may depend on the old Bearer/full-email contract.
Expected files
Implementation should remain within Relay-owned integration/event surfaces such as:
mailing/models.py and new forward-only migrations;
a generic callback service module under mailing/services/, replacing or narrowly adapting mailing/services/cmp_callbacks.py;
mailing/services/ses_webhooks.py and provider-event correlation helpers;
The versioned payload contains only stable IDs/status/time/correlation/template/safe reason fields and excludes recipient, bodies, context, credentials, raw provider payload, and unrestricted metadata.
HMAC covers the exact timestamp.raw-body bytes and deterministic fixtures verify valid, invalid, rotated, replay-window, and tampered signatures.
Delivery retries retryable failures with bounded backoff, records only safe attempt data, and never regresses transport state.
Callback row/event deduplication prevents duplicate work under repeated transition processing or dispatcher concurrency.
Provider event IDs deduplicate before transition application.
Valid unmatched events are stored redacted, retried for correlation for seven days, applied once when matched, and become operator-visible terminal exceptions when unresolved.
Authoritative status polling supports client reconciliation through callback loss or delay.
Metrics/operational status cover callback backlog/age/failure and unmatched-event backlog/age/expiration without PII.
Existing CMP callback compatibility, if retained, is explicitly bounded; website/new clients cannot receive the Bearer/full-email payload.
Unit, migration, concurrency, provider-ingress, dispatcher, redaction, and OpenAPI/contract tests pass without external network/AWS credentials.
Mandatory failure tests
Transition commit/rollback, crash before dispatcher wakeup, response loss after callback receiver commit, repeated dispatcher execution, and concurrent callback claims.
Correct, incorrect, missing, expired, future, replayed, rotated-overlap, and revoked HMAC signatures using the published canonical fixture.
Endpoint timeout, DNS/connection failure, redirect to unapproved origin, 2xx, retryable 429/5xx, permanent 4xx, retry exhaustion, and secret/endpoint disablement.
Payload redaction canaries for email, subject/body/context, headers/message parts, credentials, provider payload, and arbitrary metadata.
Duplicate provider event ID, provider event before message correlation, late correlation, duplicate correlation workers, out-of-order accepted/delivered/bounced/complained, and unresolved seven-day expiry.
Cross-tenant event/message/config access and callback routing.
Lost callbacks followed by status reconciliation proving authoritative convergence without re-sending email.
Validation
Use uv, local receiver fakes, deterministic clocks, and provider fixtures only:
git diff --check
uv run python manage.py makemigrations --check --dry-run
uv run pytest mailing/tests/test_cmp_callbacks.py mailing/tests/test_ses_webhooks.py mailing/tests/test_transactional_api.py
make lint
make test
No live callback endpoint, secret provisioning, Relay sandbox request, AWS credential, SES request, infrastructure change, deployment, or email send is permitted.
Browser and screenshots
This is a backend callback/provider-event/OpenAPI contract. No visible operator-page change is required, so browser testing and screenshots are not applicable unless UI scope is separately approved.
Dependencies
External ownership and consumer requirements: website #21.
No engagement/open/click analytics expansion, marketing/newsletter behavior, or tracking pixels.
No real callback secret provision/rotation, live endpoint registration, infrastructure mutation, deployment, production rollout, broad recipient access, or email send.
No raw provider-payload retention for convenience.
Process handoff
Follow docs/PROCESS.md in an isolated Relay issue worktree. The engineer implements and tests without committing; a separate tester verifies exact callback, redaction, retry, and reconciliation criteria; a product manager accepts; only then is the issue committed with Closes #N and moved through the repository lifecycle without a pull request.
External parent epic: DataTalksClub/website#21
Lifecycle prerequisite: #2
Template/version prerequisite: #1
Audited baseline:
c0fdfac632f057f1e6a467111613928c4899b13bOutcome
Replace the CMP-specific Bearer callback assumption with a generic tenant-scoped, redacted, versioned HMAC callback outbox and make late/unmatched provider events converge safely.
Every Relay transport transition needed by a client projection must create durable callback work after the state commit. Callback loss is recovered through bounded retry and the authoritative message-status API. Valid provider events that cannot yet be correlated are deduplicated, retained redacted for seven days, retried for correlation, and made operator-visible if unresolved.
Baseline gaps
At the audited commit:
CmpCallbackand configured through CMP-specific URL/token fields;Scope
Generic callback endpoint configuration and secret boundary
Versioned redacted transport event
Define a canonical JSON event with stable serialization and explicit
contract_version. It contains only:It must never contain:
Persist the exact canonical body/hash used for retry so every attempt signs and sends the same logical event.
HMAC/timestamp delivery
<timestamp>.<raw-body>with HMAC-SHA-256 using the client callback secret.X-Relay-Timestamp,X-Relay-Signature, content type, event/contract identifiers, and no Bearer credential.Provider-event correlation and recovery
Consumer and reconciliation contract
GET /api/transactional/messages/{message_id}from Guarantee idempotent transactional delivery and ambiguity-safe provider state #2 authoritative so a client can reconcile when callbacks are delayed/lost.Expected files
Implementation should remain within Relay-owned integration/event surfaces such as:
mailing/models.pyand new forward-only migrations;mailing/services/, replacing or narrowly adaptingmailing/services/cmp_callbacks.py;mailing/services/ses_webhooks.pyand provider-event correlation helpers;mailing/services/transactional.py/ lifecycle transition event hooks from Guarantee idempotent transactional delivery and ambiguity-safe provider state #2;mailing/views.py,mailing/services/api_docs.py,docs/api.md, and OpenAPI fixtures;mailing/tests/test_cmp_callbacks.pyonly for bounded legacy behavior;mailing/tests/test_ses_webhooks.pyand status/operations tests.Do not redesign website callback handling, campaigns, tracking analytics, inbound mail, Taskdeck, or infrastructure.
Acceptance criteria
timestamp.raw-bodybytes and deterministic fixtures verify valid, invalid, rotated, replay-window, and tampered signatures.Mandatory failure tests
2xx, retryable429/5xx, permanent4xx, retry exhaustion, and secret/endpoint disablement.Validation
Use
uv, local receiver fakes, deterministic clocks, and provider fixtures only:git diff --check uv run python manage.py makemigrations --check --dry-run uv run pytest mailing/tests/test_cmp_callbacks.py mailing/tests/test_ses_webhooks.py mailing/tests/test_transactional_api.py make lint make testNo live callback endpoint, secret provisioning, Relay sandbox request, AWS credential, SES request, infrastructure change, deployment, or email send is permitted.
Browser and screenshots
This is a backend callback/provider-event/OpenAPI contract. No visible operator-page change is required, so browser testing and screenshots are not applicable unless UI scope is separately approved.
Dependencies
Non-goals
EmailDeliveryprojection, scheduler, Studio/admin UI, or Datamailer migration.Process handoff
Follow
docs/PROCESS.mdin an isolated Relay issue worktree. The engineer implements and tests without committing; a separate tester verifies exact callback, redaction, retry, and reconciliation criteria; a product manager accepts; only then is the issue committed withCloses #Nand moved through the repository lifecycle without a pull request.