Webhooks

Webhook ingester

A self-hosted, open-source service that receives, verifies, and stores every MailBlastr webhook event in your own database — with signature verification, idempotent storage, and connectors for Postgres, MySQL, MongoDB, and data warehouses.

The MailBlastr Webhook Ingester is a small, self-hosted application that receives, verifies, and stores all of your webhook events in your own database. Deploy it to your own infrastructure and gain full control over your email event data.

You can always build your own handler, but the ingester is a production-ready drop-in that handles the parts that are easy to get wrong: signature verification, deduplication, and durable storage.

For why you should keep your own copy of webhook data, see the data storage guide.

Why use the ingester?

Compared with hand-rolling a handler, the ingester ships with:

  • Signature verification of the mb-signature / svix-* headers, so only authentic MailBlastr deliveries are stored.
  • Idempotent storage keyed on the svix-id header, so duplicate deliveries (from retries) never create duplicate rows.
  • Multiple database connectors including PostgreSQL, MySQL, MongoDB, and common data warehouses.
  • One-command deployment to any container host — it is a single stateless HTTP service.

Supported databases

Each connector is exposed as its own path. Register a webhook endpoint at https://your-ingester.example.com/{connector} and the ingester writes to the matching backend.

DatabaseEndpointBest for
Supabase/supabaseQuick setup with managed Postgres
Neon/neonServerless Postgres with branching
PostgreSQL/postgresqlSelf-hosted or managed Postgres
MySQL/mysqlSelf-hosted or managed MySQL
PlanetScale/planetscaleServerless MySQL
MongoDB/mongodbDocument database (Atlas or self-hosted)
Snowflake/snowflakeData warehousing and analytics
BigQuery/bigqueryGoogle Cloud analytics
ClickHouse/clickhouseHigh-performance analytics

Quick start

  1. 1
    Clone and install

    Pull the ingester source, then install dependencies with your package manager.

  2. 2
    Configure environment variables

    Copy the example env file and set MB_WEBHOOK_SECRET (your webhook signing secret from the dashboard) plus the connection string for your chosen database.

  3. 3
    Set up your database

    Apply the provided schema for your backend (the schema files live in the schemas/ directory). This creates the event tables described below.

  4. 4
    Deploy and register

    Deploy the service, then register the endpoint URL https://your-ingester.example.com/{connector} in the dashboard and select every event type you want to store.

Configure
# 1. install
git clone https://github.com/your-org/mailblastr-webhooks-ingester.git
cd mailblastr-webhooks-ingester
pnpm install

# 2. environment
cp .env.example .env.local
.env.local
# Required: your webhook signing secret from the MailBlastr dashboard
MB_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx

# Database credentials (example for PostgreSQL)
POSTGRESQL_URL=postgresql://user:password@host:5432/database
Get your signing secret from the dashboard when you create the webhook. The secret is shown once and is used by the ingester to verify every delivery.

Database schema

The ingester creates three tables, one per resource family of events:

TableDescription
mb_wh_emailsAll email events (e.g. email.sent, email.delivered, email.bounced, email.opened, email.clicked).
mb_wh_contactsContact lifecycle events (contact.created, contact.updated, contact.deleted).
mb_wh_domainsDomain lifecycle events (domain.created, domain.updated, domain.deleted).

Every row includes a small set of common columns plus the event-specific payload:

  • svix_id — the unique webhook delivery id, used as the idempotency key.
  • event_type — the dotted event name, e.g. email.delivered.
  • event_created_at — when the event occurred (from the payload created_at).
  • webhook_received_at — when your ingester received the delivery.
  • Event-specific fields — recipient, subject, bounce detail, clicked URL, and so on.

Idempotency

The ingester deduplicates automatically. Every delivery carries a unique svix-id header, and the ingester upserts on that id. If MailBlastr retries a delivery after a transient failure, the duplicate is safely ignored without creating a second row.

Example query

Once events are flowing, you can run analytics directly against the tables. For example, daily counts of each email event type:

Email events by day (PostgreSQL)
SELECT
  DATE(event_created_at) AS day,
  event_type,
  COUNT(*) AS count
FROM mb_wh_emails
GROUP BY DATE(event_created_at), event_type
ORDER BY day DESC, event_type;

Data retention

By default the ingester keeps events indefinitely. To enforce a retention window, schedule a periodic delete. Most databases also support partitioning by date so you can drop old partitions cheaply.

Delete events older than 90 days
DELETE FROM mb_wh_emails
WHERE event_created_at < NOW() - INTERVAL '90 days';

Troubleshooting

  • Signature verification fails — confirm MB_WEBHOOK_SECRET matches the signing secret in the dashboard, that you verify against the raw request body, and that the secret has not been rotated.
  • Database connection errors — verify your credentials, confirm the schema has been applied, and check that the database is reachable from where the ingester runs (firewall / network rules).
  • Webhooks not received — confirm the endpoint URL is publicly reachable, check the endpoint status in the dashboard, and ensure your service responds 2xx so the delivery is not retried indefinitely.