Documentation
DEVELOPERS / EVENTS

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.

EXAMPLE
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-Signaturet=<unix-seconds>,v1=<hex-hmac>.
  • User-AgentStartupMail-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.

EXAMPLE
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.