Webhooks

Subscribe to real-time email event notifications. Posthawk sends signed HTTP POST requests to your endpoint when email events occur.

Webhook endpoints receive POST requests with a JSON payload for each event. Each delivery is signed with HMAC SHA-256 so you can verify authenticity.

Supported event types:

Email delivery events:
• send — email queued for sending via SES
• delivery — successfully delivered
• bounce — bounced (use `event_data.bounce.bounceType` to distinguish Permanent/Transient/Undetermined)
• complaint — recipient marked as spam
• reject — SES rejected at submission time
• delivery_delay — temporarily delayed but not yet a bounce
• open — recipient opened the email (tracking pixel hit)
• click — recipient clicked a link in the email

Newsletter events (cloud + self-hosted):
• newsletter.subscriber.created — pending subscriber created (DOI confirmation email sent)
• newsletter.subscriber.confirmed — subscriber clicked the confirm link, or signed up with DOI off
• newsletter.subscriber.unsubscribed — subscriber unsubscribed (manually, via auto-suppress, or via the public unsubscribe link)
• newsletter.issue.sent — issue finished fanning out to subscribers
• newsletter.issue.failed — issue send aborted with an error

Signature verification:

Each request includes an `X-Posthawk-Signature` header with the format `sha256={hex_digest}`. Compute HMAC SHA-256 of the raw request body using your webhook secret and compare.

Headers sent with each webhook delivery:
• Content-Type: application/json
• X-Posthawk-Signature: sha256={hmac_hex}
• X-Posthawk-Event: {event_type}
• User-Agent: Posthawk-Webhook/1.0

### Verification snippets

**Node.js:**

```js
const crypto = require('crypto');

function verifyWebhook(req, secret) {
  const body = JSON.stringify(req.body); // or use raw-body middleware
  const signature = req.headers['x-posthawk-signature'];
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
```

**Python:**

```python
import hmac, hashlib

def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(signature, expected)
```

**Go:**

```go
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func VerifyWebhook(body []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(expected))
}
```

Retry behavior:

• Failed deliveries (non-2xx response or timeout) are retried with exponential backoff
• Each delivery attempt is logged in `webhook_deliveries` and visible in the dashboard
• Permanent failures stop retrying after the configured max attempts
• Test endpoints with `POST /v1/webhooks/:id/test` to verify before going live

Webhook secrets are auto-generated when you create an endpoint and returned in the response. Store the secret securely — it is used to verify that requests are genuinely from Posthawk.
GET/v1/webhooksAPI Key

List all webhook endpoints for the authenticated workspace.

Response

json
[
  {
    "id": "uuid",
    "url": "https://yourapp.com/api/webhooks/posthawk",
    "secret": "whsec_...",
    "events": ["delivery", "bounce", "complaint"],
    "enabled": true,
    "description": "Production webhook",
    "created_at": "2026-01-15T10:00:00Z",
    "updated_at": "2026-01-15T10:00:00Z"
  }
]
POST/v1/webhooksAPI Key

Create a new webhook endpoint. A signing secret is auto-generated and returned.

ParameterTypeInDescription
urlrequiredstringbodyThe URL to receive webhook POST requests
eventsrequiredstring[]bodyArray of event types to subscribe to. Email events: send, delivery, bounce, complaint, reject, delivery_delay, open, click. Newsletter events: newsletter.subscriber.created, newsletter.subscriber.confirmed, newsletter.subscriber.unsubscribed, newsletter.issue.sent, newsletter.issue.failed
descriptionstringbodyHuman-readable label for this endpoint

Request

bash
curl -X POST https://api.posthawk.dev/v1/webhooks \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -d '{
    "url": "https://yourapp.com/api/webhooks/posthawk",
    "events": ["delivery", "bounce", "complaint"],
    "description": "Production webhook"
  }'

Response

json
{
  "id": "uuid",
  "url": "https://yourapp.com/api/webhooks/posthawk",
  "secret": "whsec_...",
  "events": ["delivery", "bounce", "complaint"],
  "enabled": true,
  "description": "Production webhook",
  "created_at": "2026-01-15T10:00:00Z",
  "updated_at": "2026-01-15T10:00:00Z"
}
PATCH/v1/webhooks/:idAPI Key

Update a webhook endpoint — change the URL, subscribed events, enabled state, or description.

ParameterTypeInDescription
idrequiredstringpathWebhook endpoint UUID
urlstringbodyNew webhook URL
eventsstring[]bodyNew event subscriptions
enabledbooleanbodyEnable or disable the endpoint
descriptionstringbodyUpdated description

Request

bash
curl -X PATCH https://api.posthawk.dev/v1/webhooks/uuid \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your_api_key" \
  -d '{
    "events": ["delivery", "bounce", "complaint", "open", "click"],
    "description": "Updated production webhook"
  }'

Response

json
{
  "id": "uuid",
  "url": "https://yourapp.com/api/webhooks/posthawk",
  "secret": "whsec_...",
  "events": ["delivery", "bounce", "complaint", "open", "click"],
  "enabled": true,
  "description": "Updated production webhook",
  "created_at": "2026-01-15T10:00:00Z",
  "updated_at": "2026-03-15T12:00:00Z"
}
POST/v1/webhooks/:id/testAPI Key

Send a test event to the webhook endpoint to verify it is reachable and correctly configured.

ParameterTypeInDescription
idrequiredstringpathWebhook endpoint UUID

Response

json
{
  "success": true,
  "status": 200,
  "body": "OK"
}
DELETE/v1/webhooks/:idAPI Key

Delete a webhook endpoint. It will immediately stop receiving events.

ParameterTypeInDescription
idrequiredstringpathWebhook endpoint UUID

Response

json
{
  "success": true
}