Skip to main content
Resend Docs

Search documentation

Type to search this documentation.

On this pageOverview

Webhook Ingester

A self-hosted solution to store all your Resend webhook events in your own database.

The Resend Webhook Ingester is an open-source Next.js application that receives, verifies, and stores all your webhook events in your own database. Deploy it to your infrastructure and gain full control over your email event data.

While you can build your own webhook handler, the Webhook Ingester provides a production-ready solution with:

  • Signature verification using Svix to ensure webhook authenticity
  • Idempotent storage that safely handles duplicate webhook deliveries
  • Multiple database support including PostgreSQL, MySQL, MongoDB, and data warehouses
  • One-click deployment to Vercel, Railway, or Render

Get started in minutes with one-click deployment:

Deploy with Vercel Deploy on Railway Deploy to Render

Or use Docker:

Bash
docker pull ghcr.io/resend/resend-webhooks-ingester
Database Endpoint Best For
Supabase /supabase Quick setup with managed Postgres
Neon /neon Serverless Postgres with branching
PostgreSQL /postgresql Self-hosted or managed Postgres
MySQL /mysql Self-hosted or managed MySQL
PlanetScale /planetscale Serverless MySQL
MongoDB /mongodb Document database (Atlas, self-hosted)
Snowflake /snowflake Data warehousing and analytics
BigQuery /bigquery Google Cloud analytics
ClickHouse /clickhouse High-performance analytics
  1. Clone and install

    Bash
    git clone https://github.com/resend/resend-webhooks-ingester.git
    cd resend-webhooks-ingester
    pnpm install
  2. Configure environment variables

    Copy the example environment file and add your credentials:

    Bash
    cp .env.example .env.local

    At minimum, you need:

    .env.local
    # Required: Your Resend webhook signing secret
    RESEND_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxx
    
    # Database credentials (example for PostgreSQL)
    POSTGRESQL_URL=postgresql://user:password@host:5432/database
  3. Set up your database

    Set up your database and run the provided schema for your database. The ingester supports PostgreSQL, MySQL, MongoDB, and several data warehouses. Schema files can be found in the schemas/ directory.

    Bash
    pnpm db:setup --postgresql
    # or use a different flag for a different database
  4. Deploy and register webhook

    Deploy to your preferred platform, then register your webhook endpoint in the Resend Dashboard and select all the events you'd like to store.

    Your endpoint URL will be: https://example.com/{connector}

    For example: https://your-app.vercel.app/postgresql

The ingester creates three tables to store webhook events:

Table Description
resend_wh_emails All email events (sent, delivered, bounced, opened, clicked)
resend_wh_contacts Contact events (created, updated, deleted)
resend_wh_domains Domain events (created, updated, deleted)

Each table includes:

  • svix_id - Unique webhook event ID for idempotency
  • event_type - The type of event (e.g., email.delivered)
  • event_created_at - When the event occurred
  • webhook_received_at - When the webhook was received
  • Event-specific fields (e.g., email details, bounce info, click data)

The ingester handles duplicate webhooks automatically. Each webhook includes a unique svix-id header, and the ingester uses this to ensure events are stored only once.

If Resend retries a webhook delivery (due to a temporary failure), the duplicate will be safely ignored without creating duplicate records in your database.

Variable Description
RESEND_WEBHOOK_SECRET Your webhook signing secret from Resend
Supabase
env
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Neon
env
NEON_DATABASE_URL=postgresql://user:password@ep-xyz.us-east-1.aws.neon.tech/database?sslmode=require
PostgreSQL
env
POSTGRESQL_URL=postgresql://user:password@host:5432/database
MySQL
env
MYSQL_URL=mysql://user:password@host:3306/database
PlanetScale
env
PLANETSCALE_URL=mysql://username:password@host/database?ssl={"rejectUnauthorized":true}
MongoDB
env
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/
MONGODB_DATABASE=resend_webhooks
Snowflake
env
SNOWFLAKE_ACCOUNT=your-account-identifier
SNOWFLAKE_USERNAME=your-username
SNOWFLAKE_PASSWORD=your-password
SNOWFLAKE_DATABASE=your-database
SNOWFLAKE_SCHEMA=your-schema
SNOWFLAKE_WAREHOUSE=your-warehouse
BigQuery
env
BIGQUERY_PROJECT_ID=your-project-id
BIGQUERY_DATASET_ID=your-dataset-id
BIGQUERY_CREDENTIALS={"type":"service_account","project_id":"..."}
ClickHouse
env
CLICKHOUSE_URL=https://your-instance.clickhouse.cloud:8443
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=your-password
CLICKHOUSE_DATABASE=default

Once your data is stored, you can run analytics queries. Here's an example to get email status counts by day:

PostgreSQL
SELECT
  DATE(event_created_at) AS day,
  event_type,
  COUNT(*) AS count
FROM resend_wh_emails
GROUP BY DATE(event_created_at), event_type
ORDER BY day DESC, event_type;
MongoDB
db.resend_wh_emails.aggregate([
  {
    $group: {
      _id: {
        day: {
          $dateToString: { format: '%Y-%m-%d', date: '$event_created_at' },
        },
        event_type: '$event_type',
      },
      count: { $sum: 1 },
    },
  },
  { $sort: { '_id.day': -1 } },
]);
ClickHouse
SELECT
  toDate(event_created_at) AS day,
  event_type,
  count() AS count
FROM resend_wh_emails
FINAL
GROUP BY day, event_type
ORDER BY day DESC, event_type;

By default, webhook events are stored indefinitely. To implement data retention policies, you can set up scheduled jobs to delete old events.

Example for PostgreSQL (delete events older than 90 days):

SQL
DELETE FROM resend_wh_emails
WHERE event_created_at < NOW() - INTERVAL '90 days';
Webhook signature verification failing
  • Ensure RESEND_WEBHOOK_SECRET matches the signing secret in your Resend Dashboard - Make sure you're using the raw request body for verification - Check that the secret hasn't been rotated in Resend
Database connection errors
  • Verify your database credentials are correct - Check that the schema has been applied to your database - Ensure your database is accessible from your deployment (check firewall rules)
Webhooks not being received
  • Verify your endpoint URL is publicly accessible - Check the webhook status in your Resend Dashboard - Ensure your server responds with HTTP 200 for successful requests
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu