<!-- Canonical: https://startupmail.dev/docs/webhooks -->
<!-- Documentation index: https://startupmail.dev/llms.txt -->

# Webhooks

> Receive signed, retried HTTPS events when mail or domain state changes.

## Create an endpoint

Create a webhook in **Settings → Developers** or with the API. The URL must use HTTPS and must not resolve to a private or unsafe network destination.

```ts
const webhook = await mail.createWebhook({
  url: "https://example.com/startupmail/events",
  description: "Production message worker",
  eventTypes: ["message.received", "message.failed"],
});

console.log(webhook.signingSecret); // store now; shown once
```

## Request headers

Each delivery is a JSON `POST` with:

- `StartupMail-Event` — the event type.
- `StartupMail-Delivery` — a unique delivery ID.
- `StartupMail-Signature` — `t=<unix-seconds>,v1=<hex-hmac>`.
- `User-Agent` — `StartupMail-Webhooks/1.0`.

## Verify the signature

Compute HMAC-SHA256 over `<timestamp>.<raw-request-body>` using the endpoint secret, compare it in constant time, and reject timestamps outside your replay window. Do not parse and reserialize the JSON before verification.

```ts
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody: string, header: string, secret: string) {
  const [timestampPart, signaturePart] = header.split(",");
  const timestamp = timestampPart.replace("t=", "");
  const received = Buffer.from(signaturePart.replace("v1=", ""), "hex");
  const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
  return received.length === expected.length && timingSafeEqual(received, expected);
}
```

## Event payloads

`message.received` includes `messageId`, `mailboxId`, `threadId`, sender, subject, and `hasAttachments`. Delivery events identify the message and its status. `domain.verified` identifies the domain that completed verification.

Use event IDs as signals to retrieve the current resource rather than assuming the event body is a complete message object.

## Delivery and retries

Return any `2xx` status quickly. Non-success responses and network errors retry up to five delivery attempts with increasing delay. Your handler must be idempotent because a request can be delivered more than once.

> **Acknowledge before slow work**
>
> Verify, persist the delivery ID, enqueue your own job, and return `2xx`. Fetching threads, calling
> models, or processing attachments should happen after acknowledgement.
