---
title: Webhooks | SendHQ Docs
description: Receive signed email events (sent, delivered, delayed, bounced, complained, failed, received) at your own HTTPS endpoint, with retries.
canonical: https://sendhq.cc/docs/webhooks
last-updated: 2026-09-07
updated: 2026-09-07
---
# 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.

```json
{
  "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`.

```json
{
  "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](https://www.standardwebhooks.com/) 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.

```javascript
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.
