# Mail Authentication This document records the inbound and outbound mail-authentication contract for the API and worker runtimes. It is intentionally operational and redacted: use placeholder names, environment key names, and status codes only; do not paste API keys, webhook secrets, webhook bodies, message contents, tenant identifiers, or real attempt ids into this file or related diagnostics. ## Outbound Mailjet delivery Transactional email delivery uses Mailjet (Send API v3.1) through the mail provider boundary. Mailjet authenticates outbound requests with HTTP Basic Auth using a public/secret API key pair. Outbound delivery requires the key pair and verified sender settings documented in `docs/configuration.md`: - `MAILJET_API_KEY` is the Mailjet public API key (Basic-Auth username). - `MAILJET_SECRET_KEY` is the Mailjet private/secret API key (Basic-Auth password). - `MAILJET_FROM_EMAIL` is the default sender address from a verified Mailjet sender domain. - `MAILJET_FROM_NAME` is an optional sender display name. Treat `MAILJET_API_KEY`, `MAILJET_SECRET_KEY`, and `MAILJET_FROM_EMAIL` as an outbound delivery set. If any one is configured without the others, the runtime should report a redacted configuration error by key name rather than attempting delivery or logging raw values. ## Inbound Mailjet webhook authentication The Mailjet provider webhooks — the email-sequence event endpoint `POST /v1/email-sequences/mailjet/webhooks`, the inbound parse endpoint `POST /v1/support/inbound-email`, and the legacy retry endpoint `POST /v1/events/webhooks/attempts/:id` — are public webhook boundaries and must authenticate inbound Mailjet attempts before looking up a record or enqueueing work. Mailjet does **not** sign event/parse webhooks (there is no HMAC signature on the payload, unlike Svix-style schemes). The supported authentication is HTTP Basic Auth over HTTPS: configure the Mailjet event callback URL as `https://mailjet:SECRET@host/...` so Mailjet sends an `Authorization: Basic base64("mailjet:SECRET")` header. Configure the API runtime with: - `MAILJET_WEBHOOK_SECRET`: the shared secret used as the HTTP Basic-Auth password. The username may be anything (the verifier ignores it and checks the password in constant time). This secret is separate from the `MAILJET_API_KEY`/`MAILJET_SECRET_KEY` outbound pair. The outbound keys authorize outbound API calls to Mailjet; the webhook secret authenticates inbound webhook payloads. Do not reuse outbound values for the webhook secret, do not store any of them in source control, and do not expose any value in logs, health responses, OpenAPI examples, or support tickets. ### Expected credentials Mailjet webhook requests are verified against the Basic-Auth credentials in the `Authorization` header: - `Authorization: Basic ` The header is required. The password portion must match `MAILJET_WEBHOOK_SECRET` in a constant-time comparison. Missing, malformed, or mismatched credentials must be rejected with `401 invalid_webhook_signature` before any database lookup or queue enqueue. When `MAILJET_WEBHOOK_SECRET` itself is not configured, the runtime returns `500 webhook_signing_not_configured`. ### Transport requirement Because Basic Auth carries the secret in the request, the callback must be served over HTTPS so the credential is not exposed in transit. Avoid placing the secret in the URL query string (it would land in access and proxy logs); the credential travels in the `Authorization` header derived from the configured callback URL. ### Rotation notes Rotate `MAILJET_WEBHOOK_SECRET` using a deployment window that keeps Mailjet and the API runtime in sync: 1. Update the Mailjet event callback URL with the new Basic-Auth password for the target endpoint. 2. Update the API runtime secret store with the new `MAILJET_WEBHOOK_SECRET` value. 3. Redeploy or restart API instances so every instance reads the same current secret. 4. Send a Mailjet test event and confirm authenticated requests reach the existing endpoint behavior. 5. Remove any superseded secret from secret stores after all API instances have switched. If zero-downtime dual-secret rotation is later required, implement and document an explicit previous/current-secret verifier contract before changing operational steps. Until then, avoid staggered deployments where old and new API instances validate different webhook secrets for the same Mailjet endpoint. ## Troubleshooting `401 invalid_webhook_signature` Use only redacted facts when investigating webhook auth failures. Safe diagnostics include request correlation id, HTTP status, error code, environment key names, and the failure reason class (missing-credentials, malformed-credentials, credentials-mismatch). Unsafe diagnostics include the webhook secret, raw body fragments, the `Authorization` header value, attempt ids from customer traffic, API keys, or message content. Common causes: - `MAILJET_WEBHOOK_SECRET` is missing from the API runtime secret store (returns `500 webhook_signing_not_configured`). - The Mailjet callback URL is configured with a different Basic-Auth password than the API runtime secret. - A proxy, middleware, or test helper stripped or rewrote the `Authorization` header before it reached the API. - The callback URL is configured without credentials, so no `Authorization` header is sent. - The request was sent to a different webhook endpoint than the one whose secret was configured. Expected behavior during failures: the API returns a request-correlated error envelope with `401 invalid_webhook_signature` (or `500 webhook_signing_not_configured` when the secret is unset), does not return or log secret/credential/body fragments, and does not query records or enqueue work. Once a request presents valid Mailjet Basic-Auth credentials, the endpoint preserves the existing behavior and any downstream diagnostics.