# `Mailglass.Webhook.Plug`
[🔗](https://github.com/szTheory/mailglass/blob/v2.5.0/lib/mailglass/webhook/plug.ex#L1)

Single-ingress webhook orchestrator.

Plugged at adopter-mounted paths via `Mailglass.Webhook.Router`.
Owns the full request lifecycle:

  1. Extract `raw_body` from `conn.private[:raw_body]` (populated by
     `Mailglass.Webhook.CachingBodyReader` in the adopter's
     `Plug.Parsers` `:body_reader`)
  2. Dispatch to `Mailglass.Webhook.Provider` impl per route opts
     (`provider: :postmark | :sendgrid | :mailgun`)
  3. `Provider.verify!/3` — raises `%SignatureError{}` on failure
  4. `Mailglass.Tenancy.resolve_webhook_tenant/1` — runs after verify
  5. `Mailglass.Tenancy.with_tenant/2` BLOCK form — clean tenant
     cleanup on raise (Pitfall 7)
  6. `Provider.normalize/2` — pure, returns `[%Event{}]`
  7. `Mailglass.Webhook.Ingest.ingest_multi/3` — single Ecto.Multi
     inside `Repo.transact/1`
  8. Post-commit: `Mailglass.Outbound.Projector.broadcast_delivery_updated/3`
     per matched delivery (`events_with_deliveries` 3-tuples)
  9. `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.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
