Operate

Webhooks

Receive signed email events (sent, delivered, delayed, bounced, complained, failed, received) at your own HTTPS endpoint, with retries.

Add an endpoint

Open Webhooks in the dashboard, or call POST /api/v1/webhooks with an HTTPS URL. Omit events to receive every event type, or list the ones you want. The response includes the endpoint's signing secret (whsec_…); keep it server-side. A workspace can have up to 10 endpoints.

POST /webhooks
{
  "url": "https://example.com/webhooks/sendhq",
  "events": [
    "email.delivered",
    "email.bounced",
    "email.complained"
  ],
  "description": "Production"
}

Event types

email.sent
The provider accepted an outbound email.
email.delivered
The recipient's mail server accepted it. data.recipients lists the addresses.
email.delivery_delayed
Delivery is being retried. data.delay has type, the receiving server's diagnostic (for example a Gmail 4.7.28 rate limit), and expiration.
email.bounced
data.recipient and data.bounce with type (Permanent or Transient), sub_type, diagnostic, and suppressed.
email.complained
data.recipient and data.complaint.feedback_type. The address is suppressed.
email.failed
The email was not sent. data.reason says why.
email.received
An inbound email was stored, with data.inbox_id.

Payload

Every event is a JSON object with type, created_at, and data. data.email_id is the SendHQ email ID returned when you sent it, alongside from, to, and subject.

email.bounced
{
  "type": "email.bounced",
  "created_at": "2026-10-29T12:00:00.000Z",
  "data": {
    "email_id": "em_…",
    "from": "news@example.com",
    "to": [
      "reader@example.net"
    ],
    "subject": "Issue 12",
    "recipient": "reader@example.net",
    "bounce": {
      "type": "Permanent",
      "sub_type": "General",
      "diagnostic": "550 5.1.1 user unknown",
      "suppressed": true
    }
  }
}

Verify the signature

Deliveries are signed with the Standard Webhooks scheme. Each request carries webhook-id, webhook-timestamp, and webhook-signature. The signature is v1, followed by the base64 HMAC-SHA256 of id.timestamp.body, keyed with the base64-decoded secret after whsec_. Compare in constant time, reject timestamps more than five minutes old, and verify the raw body before parsing it. Any Standard Webhooks library works.

Node.js
import { Webhook } from "standardwebhooks";

const event = new Webhook(process.env.SENDHQ_WEBHOOK_SECRET)
  .verify(rawBody, Object.fromEntries(request.headers));

Retries and deduplication

Answer with any 2xx status within 10 seconds. Anything else, a timeout, or a redirect is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, and 10 hours: eight attempts over about 27 hours. A retry keeps the same webhook-id, so record it and ignore a delivery you have already processed. Events can arrive out of order.

Inspect and replay

GET /api/v1/webhooks/:id/deliveries lists attempts with the response status and error; filter with status=failed. Retry one with POST …/deliveries/:deliveryId/retry, send a signed webhook.test event with POST /api/v1/webhooks/:id/test, and rotate a leaked secret with POST /api/v1/webhooks/:id/rotate-secret. Delivery logs follow the workspace message retention.