Webhooks

Receive real-time notifications when events happen in your newsletter.

Setting Up Webhooks

Via Dashboard

  1. Go to your newsletter → Settings → Webhooks
  2. Click "Add Webhook"
  3. Enter your endpoint URL
  4. Select events to subscribe to
  5. Save the webhook

Via API

POST /api/v1/webhook_subscriptions

{
  "url": "https://your-app.com/webhooks/dailydraft",
  "events": ["subscriber.created", "issue.sent"]
}

Available Events

Event Description
subscriber.created New subscriber added
subscriber.updated Subscriber details changed
subscriber.deleted Subscriber removed
issue.sent Issue finished sending
issue.published Issue published to the web
email.opened Subscriber opened email
email.clicked Subscriber clicked a link
email.bounced Email bounced (hard bounce)
email.unsubscribed Subscriber unsubscribed via email link

Webhook Payload

All webhooks follow this format:

{
  "event": "subscriber.created",
  "timestamp": "2025-01-10T12:00:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    // Event-specific data
  }
}

Event Payloads

subscriber.created

{
  "event": "subscriber.created",
  "timestamp": "2025-01-10T12:00:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "subscription_id": 456,
    "email": "[email protected]",
    "name": "New User",
    "status": "active",
    "subscribed_at": "2025-01-10T12:00:00Z",
    "tags": []
  }
}

subscriber.updated

{
  "event": "subscriber.updated",
  "timestamp": "2025-01-10T12:00:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "subscription_id": 456,
    "email": "[email protected]",
    "name": "Updated Name",
    "status": "active",
    "subscribed_at": "2025-01-09T10:00:00Z",
    "tags": ["vip"]
  }
}

subscriber.deleted

{
  "event": "subscriber.deleted",
  "timestamp": "2025-01-10T12:00:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "subscription_id": 456,
    "email": "[email protected]",
    "name": "Removed User",
    "status": "unsubscribed",
    "subscribed_at": "2025-01-01T10:00:00Z",
    "tags": []
  }
}

issue.sent

{
  "event": "issue.sent",
  "timestamp": "2025-01-10T12:00:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "issue_id": 42,
    "subject": "Weekly Newsletter #42",
    "path": "weekly-newsletter-42",
    "status": "sent",
    "sent_at": "2025-01-10T09:00:00Z",
    "published_at": null,
    "article_count": 5
  }
}

issue.published

{
  "event": "issue.published",
  "timestamp": "2025-01-10T12:00:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "issue_id": 42,
    "subject": "Weekly Newsletter #42",
    "path": "weekly-newsletter-42",
    "status": "published",
    "sent_at": "2025-01-10T09:00:00Z",
    "published_at": "2025-01-10T12:00:00Z",
    "article_count": 5
  }
}

email.opened

{
  "event": "email.opened",
  "timestamp": "2025-01-10T12:05:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "delivery_id": 789,
    "issue_id": 42,
    "email": "[email protected]",
    "opened_at": "2025-01-10T12:05:00Z",
    "clicked_at": null
  }
}

email.clicked

{
  "event": "email.clicked",
  "timestamp": "2025-01-10T12:10:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "delivery_id": 789,
    "issue_id": 42,
    "email": "[email protected]",
    "opened_at": "2025-01-10T12:05:00Z",
    "clicked_at": "2025-01-10T12:10:00Z",
    "clicked_url": "https://example.com/article"
  }
}

email.bounced

{
  "event": "email.bounced",
  "timestamp": "2025-01-10T12:00:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "delivery_id": 789,
    "issue_id": 42,
    "email": "[email protected]",
    "opened_at": null,
    "clicked_at": null
  }
}

email.unsubscribed

{
  "event": "email.unsubscribed",
  "timestamp": "2025-01-10T12:15:00Z",
  "newsletter": {
    "id": 123,
    "name": "Tech Weekly",
    "subdomain": "tech-weekly"
  },
  "data": {
    "delivery_id": 789,
    "issue_id": 42,
    "email": "[email protected]",
    "opened_at": "2025-01-10T12:05:00Z",
    "clicked_at": null
  }
}

Webhook Security

Signature Verification

Each webhook includes a signature header for verification:

X-Webhook-Signature: sha256=abc123...

Verify the signature:

require 'openssl'

def verify_webhook(payload, signature, secret)
  expected = "sha256=" + OpenSSL::HMAC.hexdigest(
    'sha256',
    secret,
    payload
  )

  Rack::Utils.secure_compare(expected, signature)
end

Additional Headers

Each webhook request also includes:

Header Description
X-Webhook-Event The event name (e.g. subscriber.created)
X-Webhook-Delivery-Id Unique delivery ID for deduplication
X-Webhook-Timestamp Unix timestamp of the delivery attempt

Retry Policy

Failed webhooks are retried up to 3 times with polynomial backoff. After all retries are exhausted, the delivery is marked as failed.

Endpoints that accumulate 10 consecutive failures are automatically disabled.

Success Response

Return a 2xx status code to acknowledge receipt:

HTTP/1.1 200 OK
{"received": true}

Managing Webhooks via API

List Webhooks

GET /api/v1/webhook_subscriptions

Create Webhook

POST /api/v1/webhook_subscriptions

{
  "url": "https://your-app.com/webhooks",
  "events": ["subscriber.created", "issue.sent"]
}
Limit: Each newsletter can have up to 16 webhook subscriptions.

Supported events: subscriber.created, subscriber.updated, subscriber.deleted, issue.sent, issue.published, email.opened, email.clicked, email.bounced, email.unsubscribed

Response (201 Created)

{
  "id": 42,
  "url": "https://your-app.com/webhooks",
  "events": ["subscriber.created", "issue.sent"],
  "enabled": true,
  "secret": "a1b2c3d4e5f6...",
  "created_at": "2025-01-10T12:00:00Z"
}
Store the secret now. The secret is only returned once at creation. Use it to verify webhook signatures. If you lose it, rotate it via the endpoint below.

Update Webhook

PATCH /api/v1/webhook_subscriptions/:id

{
  "url": "https://your-new-endpoint.com/webhooks",
  "events": ["subscriber.created", "issue.sent"]
}

Both fields are optional — omit any you don't want to change.

Response (200 OK)

{
  "id": 42,
  "url": "https://your-new-endpoint.com/webhooks",
  "events": ["subscriber.created", "issue.sent"],
  "enabled": true,
  "created_at": "2025-01-10T12:00:00Z"
}

Rotate Secret

Generate a new signing secret for a webhook. The old secret is immediately invalidated.

POST /api/v1/webhook_subscriptions/:id/rotate_secret

Response

{
  "id": 42,
  "url": "https://your-app.com/webhooks",
  "events": ["subscriber.created", "issue.sent"],
  "enabled": true,
  "secret": "new_secret_here...",
  "created_at": "2025-01-10T12:00:00Z"
}

Delete Webhook

DELETE /api/v1/webhook_subscriptions/:id

Returns 204 No Content on success.


Testing Webhooks

Use the dashboard to send test events:

  1. Go to Settings → Webhooks
  2. Click "Test" on any webhook
  3. Select an event type
  4. Click "Send Test"

Or use a tool like webhook.site to inspect payloads.

Still need help?

Can't find what you're looking for? Chat with our AI assistant or create a support ticket.

Sign in to get support