Skip to content
MiniMailer
Docs
Toggle sidebar
Quick links
Documentation
Knowledge Base
API Reference
Changelog
No results found

Webhooks

Webhooks are how MiniMailer tells your application what happened — every email lifecycle event, campaign transition, and domain state change can push to an HTTPS endpoint you control, as it happens. The event model is webhooks-first by design: polling the API works, but webhooks are the intended integration.

Deliveries follow the Standard Webhooks specification, so you can verify signatures with an off-the-shelf library in any language instead of hand-rolling crypto.

Create a webhook

A webhook belongs to a domain and subscribes to one or more event types:

curl https://api.minimailer.app/webhooks \
  -X POST \
  -H "Authorization: Bearer $MINIMAILER_TOKEN" \
  -H "Content-Type: application/vnd.api+json" \
  -H "Accept: application/vnd.api+json" \
  -d '{
    "data": {
      "type": "webhooks",
      "attributes": {
        "name": "Production events",
        "url": "https://app.example.com/hooks/minimailer",
        "events": ["email.delivered", "email.bounced", "email.complained", "inbound_email.received"]
      },
      "relationships": {
        "domain": {
          "data": { "type": "domains", "id": "{domain}" }
        }
      }
    }
  }'

Subscribe to exactly what you handle, or to everything with ["*"]. Event types:

Group Events
Email lifecycle email.sent · email.delivered · email.delayed · email.bounced · email.complained · email.opened · email.clicked · email.rejected · email.unsubscribed
Inbound inbound_email.received
Campaigns campaign.sending · campaign.sent · campaign.paused · campaign.failed
Domain domain.verified · domain.verification_failed · domain.suspended · domain.reputation_warning · domain.reputation_recovered
Account usage.limit_approaching · usage.limit_reached · subscription.past_due · subscription.downgrade_scheduled · subscription.revoked
Agent reputation agent.pool.promoted · agent.pool.demoted · agent.pool.recovered · agent.reputation.damaged

The creation response includes the signing secret (prefixed whsec_) once. Store it next to the endpoint that will verify deliveries; afterwards the API exposes only a secret_hint (the last four characters) so you can tell keys apart.

The delivery

Each delivery is an HTTP POST with three Standard Webhooks headers and a JSON body:

webhook-id: msg_01k02w9k3f7m8n5p6q7r8s9t0v
webhook-timestamp: 1752566400
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pojjQmB6Jla4tA=
Content-Type: application/json
{
    "type": "email.delivered",
    "timestamp": "2026-07-15T08:30:00+00:00",
    "data": {
        "...": "event-specific fields"
    }
}

webhook-id is unique per logical delivery and stable across retries — it is your idempotency key. Process each webhook-id once, no matter how many times it arrives.

Verify the signature

Use a Standard Webhooks library — most languages have one; pass it the secret, the headers, and the raw request body. Under the hood verification is:

  1. Concatenate {webhook-id}.{webhook-timestamp}.{raw body}.
  2. Compute HMAC-SHA256 over that string, keyed with the base64-decoded secret (the part after whsec_).
  3. Base64-encode the result and compare against the header value after v1, — constant-time.
  4. Reject stale timestamps (the libraries default to a five-minute tolerance) to block replays.

The signature header can carry multiple space-separated signatures; accept the delivery if any one verifies. That is what makes secret rotation seamless.

Respond 2xx quickly — acknowledge, queue, process async. Anything slower than 15 seconds counts as a failed attempt.

Retries

A failed delivery (non-2xx, timeout, connection error) is retried up to 10 attempts total with increasing delays, each with ±10% jitter:

Attempt 1 2 3 4 5 6 7 8 9 10
Delay after failure 5s 5m 30m 2h 5h 10h 14h 20h 24h

Three responses get special treatment:

  • 429 — the Retry-After header, when present, overrides the schedule.
  • 410 Gone — the webhook is disabled immediately; you are telling us the endpoint no longer exists.
  • 10 consecutive failed deliveries — the webhook is disabled automatically and you are notified, so a dead endpoint doesn't retry forever.

Rotate the secret

Rotate by updating the webhook with rotate_secret: true. The response carries the new secret (again, shown once); the old secret keeps signing for 24 hours alongside the new one — deliveries in that window carry two signatures, so deploy the new secret to your endpoint at your own pace, then the old one expires on its own.

Inspect and replay deliveries

Every delivery attempt is recorded — status, response code, response body (truncated), attempt count:

  • GET /webhooks/{webhook}/deliveries — delivery history for a webhook.
  • GET /deliveries/{outboundWebhook} — one delivery in detail.
  • POST /deliveries/{outboundWebhook}/replay — redeliver. A replay is a new delivery with a fresh webhook-id, so your idempotency handling treats it as distinct — exactly what you want when the original was consumed by a buggy handler.

The dashboard's webhook activity log shows the same history with one-click replay.