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

Single-ingress webhook orchestrator (CONTEXT D-10).

Plugged at adopter-mounted paths via `Mailglass.Webhook.Router`
(Plan 05). 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`)
  3. `Provider.verify!/3` — raises `%SignatureError{}` on failure
  4. `Mailglass.Tenancy.resolve_webhook_tenant/1` (D-12) — runs
     AFTER verify (D-13)
  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` (Plan 06, forward-declared)
  8. Post-commit: `Mailglass.Outbound.Projector.broadcast_delivery_updated/3`
     per matched delivery (Phase 3 D-04 — Plan 06 returns the
     `events_with_deliveries` 3-tuples for this loop)
  9. `send_resp(conn, 200, "")`

## Response code matrix (CONTEXT D-10 + D-14 + D-21)

| 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 (CONTEXT D-22)

Emits `[:mailglass, :webhook, :ingest, :start | :stop | :exception]`
around the entire call/2 body via
`Mailglass.Webhook.Telemetry.ingest_span/2` (Plan 08). Stop metadata
follows D-23 whitelist:
`%{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 (CONTEXT D-24)

`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 shipped by Plan 06
(Wave 3). This module references it directly; the `@compile
{:no_warn_undefined, ...}` attribute below suppresses the warning
during `mix compile --warnings-as-errors` until Plan 06 lands.

---

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