Single-ingress webhook orchestrator.
Plugged at adopter-mounted paths via Mailglass.Webhook.Router.
Owns the full request lifecycle:
- Extract
raw_bodyfromconn.private[:raw_body](populated byMailglass.Webhook.CachingBodyReaderin the adopter'sPlug.Parsers:body_reader) - Dispatch to
Mailglass.Webhook.Providerimpl per route opts (provider: :postmark | :sendgrid | :mailgun) Provider.verify!/3— raises%SignatureError{}on failureMailglass.Tenancy.resolve_webhook_tenant/1— runs after verifyMailglass.Tenancy.with_tenant/2BLOCK form — clean tenant cleanup on raise (Pitfall 7)Provider.normalize/2— pure, returns[%Event{}]Mailglass.Webhook.Ingest.ingest_multi/3— single Ecto.Multi insideRepo.transact/1- Post-commit:
Mailglass.Outbound.Projector.broadcast_delivery_updated/3per matched delivery (events_with_deliveries3-tuples) send_resp(conn, 200, "")
Response code matrix
| Outcome | Status | Notes |
|---|---|---|
| Success | 200 | Normal happy path |
| Duplicate replay (UNIQUE collision) | 200 | Idempotent — provider sees no error |
| %SignatureError{} (any of 7+ atoms) | 401 | Logger.warning with provider + atom |
| %TenancyError{:webhook_tenant_unresolved} | 422 | Distinct from signature failure |
| %ConfigError{:webhook_caching_body_reader_missing} | 500 | Adopter wiring gap |
| %ConfigError{:webhook_verification_key_missing} | 500 | Missing provider secret |
| Ingest {:error, reason} | 500 | Logger.error with reason atom only |
Telemetry
Emits [:mailglass, :webhook, :ingest, :start | :stop | :exception]
around the entire call/2 body via
Mailglass.Webhook.Telemetry.ingest_span/2. Stop metadata
is intentionally whitelisted:
%{provider, tenant_id, status, event_count, duplicate, failure_reason}
— never IP, headers, or payload bytes.
Also emits [:mailglass, :webhook, :signature, :verify, :start | :stop | :exception] around Provider.verify!/3 via
Mailglass.Webhook.Telemetry.verify_span/2.
Failure log discipline
Logger.warning on signature failure includes provider + atom
reason only. Never the source IP, headers, or payload excerpts.
Adopters wanting IP-based abuse triage attach their own telemetry
handler on [:mailglass, :webhook, :signature, :verify, :stop]
with status: :failed and pull conn.remote_ip from their own
plug lineage.
Forward-declared contracts
Mailglass.Webhook.Ingest.ingest_multi/3 is referenced directly; the
@compile {:no_warn_undefined, ...} attribute below suppresses
compile warnings until the module is available.