Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
inbox Submodule
webhooks
Public Resend webhook intake for delivery/engagement events and inbound (received) emails, verified by HMAC signature rather than the platform role/auth guard.
Source path: libraries/inbox/src/message (message.controller.ts: InboxWebhookController), resend-integration.service.ts
Module file: message.module.ts
Introduction
The Webhooks submodule is `InboxWebhookController`, mounted at `/inbox/webhook` and decorated `@Public()` — it is declared in the same file as `MessageController` and shares `message.module.ts`, but is registered as its own controller with its own (absent) auth requirement. `route.yaml` still lists role relations (`admin`, `admin-inbox`, `inbox-owner`) for all three of its routes, but that configuration has no effect at runtime: NestJS's auth guard checks for the `@Public()` metadata before evaluating any role relation, and short-circuits past the role check entirely for a controller marked `@Public()`. In practice these three routes are open to any caller — the role.yaml/route.yaml entries for them are vestigial and should not be read as the real access control. Actual authentication for this controller happens one layer down, inside `ResendIntegrationService.assertValidSignature`: every webhook body is checked against an HMAC-SHA256 signature computed from the `inbox.resend.webhook-secret` setting, supporting both Resend's native `x-resend-signature`/`x-resend-timestamp` headers and Svix-formatted `svix-id`/`svix-timestamp`/`svix-signature` headers (Resend webhooks are Svix-backed), with a timing-safe, hex-or-base64-tolerant comparison. A request with a missing or invalid signature is rejected with a 401 before any payload processing occurs; a missing `inbox.resend.webhook-secret` setting rejects every request outright.
`POST /inbox/webhook` is a unified endpoint that inspects the payload shape to decide how to route it: `isInboundWebhookPayload` treats the body as an inbound (received) email if its `type`/`event`/`name` field contains `email.received`, `inbound`, or `received`, or if the payload's `data` object has `from`/`to`/`subject` fields even without a matching type string; otherwise, if `body.type` is a string, it is routed as a delivery/engagement event; the final fallback (no recognizable type at all) also routes as inbound. `POST /inbox/webhook/events` and `POST /inbox/webhook/inbound` skip that detection and always route to their respective handler, which is the safer integration point when the Resend endpoint configuration can specify separate URLs per webhook category.
Both handlers do not process the payload inline — they call `IntegrationDeveloperApiService.publishEvent` to drop the raw body onto the platform's Integration outbox (`inbox.resend.webhook.event.received` / `inbox.resend.webhook.inbound.received`) and return `{ accepted: true, outboxEventId }` immediately. `ResendIntegrationService.onModuleInit` subscribes its own consumers to those same event names, so the actual side effects — updating `mail_event`/`inbox_message` status for delivery events, or creating a brand-new inbound message/thread — run asynchronously and are retried by the Integration subsystem independently of the original HTTP request/response cycle.
Inbound processing (`processInboundWebhook`) is the most involved path in the module: it resolves the target `inbox_account` by matching the "to" recipient email, deduplicates by `provider_message_id`, threads the new message using a three-strategy waterfall — first by `In-Reply-To`/`References` header tokens against a prior outbound message's `provider_message_id`, then by normalized subject plus the sender appearing as a recipient on a prior outbound message in the same account, then by normalized subject alone — and, when an API token is configured, fetches the full HTML/text body and attachments from the Resend API by `email_id` (falling back to whatever body fields are present in the webhook payload, or parsing a raw MIME blob with `mailparser`, if no token is set). Attachments are downloaded and re-uploaded through the platform File service into `inbox_attachment` on a best-effort basis (upload failures are logged, not thrown), and every account owner/member is notified via `NotificationService` once the message is persisted.
HTTP Endpoints
3 endpoints
Unified webhook intake. Inspects the payload to decide whether it is a delivery/engagement event or an inbound (received) email, then publishes it to the Integration outbox for async processing.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| (raw body) | Record<string, any> | yes | Resend webhook payload; shape depends on event vs. inbound |
Always routes as a delivery/engagement event webhook, regardless of payload shape.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| (raw body) | Record<string, any> | yes | Resend delivery/engagement event payload (sent, delivered, bounced, complained, opened, clicked, failed, delivery_delayed) |
Always routes as an inbound (received email) webhook, regardless of payload shape.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| (raw body) | Record<string, any> | yes | Resend inbound (received email) payload |