> ## Documentation Index
> Fetch the complete documentation index at: https://docs.moritosh.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications about message and contact events

Webhooks let Pulsewave push events to your server as they happen, instead of you polling [Events](/api-reference/events/list) or [Messages](/api-reference/messages/retrieve).

## Setting up an endpoint

Create a [webhook endpoint](/api-reference/webhook-endpoints/create) pointing at a URL on your server, and choose which events to receive:

```bash theme={null}
curl -X POST https://api.pulsewave.dev/v1/webhook-endpoints \
  -H "Authorization: Bearer pw_live_8f2k9q3m1n7r5t6y4u2i0o8p" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/pulsewave",
    "enabled_events": ["message.delivered", "message.bounced", "contact.unsubscribed"]
  }'
```

The response includes a `secret` starting with `whsec_`. Save it — you'll need it to verify incoming payloads, and it's only shown once.

## Event types

| Event                                                                        | Fired when                               |
| ---------------------------------------------------------------------------- | ---------------------------------------- |
| [`message.sent`](/api-reference/webhook-events/message-sent)                 | A message is handed off to the carrier   |
| [`message.delivered`](/api-reference/webhook-events/message-delivered)       | The receiving server confirms delivery   |
| [`message.bounced`](/api-reference/webhook-events/message-bounced)           | A message permanently fails to deliver   |
| [`message.opened`](/api-reference/webhook-events/message-opened)             | An email recipient opens the message     |
| [`message.clicked`](/api-reference/webhook-events/message-clicked)           | A recipient clicks a tracked link        |
| [`message.complained`](/api-reference/webhook-events/message-complained)     | A recipient marks the message as spam    |
| [`contact.unsubscribed`](/api-reference/webhook-events/contact-unsubscribed) | A contact unsubscribes                   |
| [`domain.verified`](/api-reference/webhook-events/domain-verified)           | A sending domain passes DNS verification |

## Payload shape

Every event has the same envelope:

```json theme={null}
{
  "id": "evt_1a2b3c",
  "object": "event",
  "type": "message.delivered",
  "created_at": "2024-06-01T14:32:00Z",
  "data": {
    "id": "msg_3p2k9q",
    "object": "message",
    "channel": "email",
    "status": "delivered",
    "to": "ada@example.com"
  }
}
```

## Verifying signatures

Every request includes a `Pulsewave-Signature` header. Verify it before trusting the payload, so an attacker can't spoof events by POSTing to your endpoint directly.

```javascript theme={null}
import crypto from 'node:crypto';

function verifySignature(rawBody, signatureHeader, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}
```

<Warning>
  Compute the signature over the raw request body, before your framework parses it as JSON. Re-serializing the parsed body can change whitespace and break the comparison.
</Warning>

## Retries

If your endpoint doesn't return a `2xx` within 10 seconds, Pulsewave retries with exponential backoff for up to 24 hours: after 1 minute, 5 minutes, 30 minutes, 2 hours, then every 6 hours. Return `200` as soon as you've durably queued the event — do the slow work asynchronously.

## Disabling an endpoint

If an endpoint fails repeatedly, Pulsewave automatically sets its `status` to `disabled` after 24 hours of consecutive failures and stops sending it events. Re-enable it with [Update a webhook endpoint](/api-reference/webhook-endpoints/update) once it's fixed.
