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.
Why use the Webhook Ingester
Section titled “Why use the Webhook Ingester”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
Deploy
Section titled “Deploy”Get started in minutes with one-click deployment:
Or use Docker:
docker pull ghcr.io/resend/resend-webhooks-ingesterSupported Databases
Section titled “Supported Databases”| 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 |
Quick Start
Section titled “Quick Start”Clone and install
Bash git clone https://github.com/resend/resend-webhooks-ingester.git cd resend-webhooks-ingester pnpm installConfigure environment variables
Copy the example environment file and add your credentials:
Bash cp .env.example .env.localAt 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/databaseSet 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 databaseDeploy 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
Database Schemas
Section titled “Database Schemas”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 idempotencyevent_type- The type of event (e.g.,email.delivered)event_created_at- When the event occurredwebhook_received_at- When the webhook was received- Event-specific fields (e.g., email details, bounce info, click data)
Idempotency
Section titled “Idempotency”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.
Configuration Reference
Section titled “Configuration Reference”Required Environment Variables
Section titled “Required Environment Variables”| Variable | Description |
|---|---|
RESEND_WEBHOOK_SECRET |
Your webhook signing secret from Resend |
Database-Specific Variables
Section titled “Database-Specific Variables”Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...Neon
NEON_DATABASE_URL=postgresql://user:password@ep-xyz.us-east-1.aws.neon.tech/database?sslmode=requirePostgreSQL
POSTGRESQL_URL=postgresql://user:password@host:5432/databaseMySQL
MYSQL_URL=mysql://user:password@host:3306/databasePlanetScale
PLANETSCALE_URL=mysql://username:password@host/database?ssl={"rejectUnauthorized":true}MongoDB
MONGODB_URI=mongodb+srv://username:password@cluster.mongodb.net/
MONGODB_DATABASE=resend_webhooksSnowflake
SNOWFLAKE_ACCOUNT=your-account-identifier
SNOWFLAKE_USERNAME=your-username
SNOWFLAKE_PASSWORD=your-password
SNOWFLAKE_DATABASE=your-database
SNOWFLAKE_SCHEMA=your-schema
SNOWFLAKE_WAREHOUSE=your-warehouseBigQuery
BIGQUERY_PROJECT_ID=your-project-id
BIGQUERY_DATASET_ID=your-dataset-id
BIGQUERY_CREDENTIALS={"type":"service_account","project_id":"..."}ClickHouse
CLICKHOUSE_URL=https://your-instance.clickhouse.cloud:8443
CLICKHOUSE_USERNAME=default
CLICKHOUSE_PASSWORD=your-password
CLICKHOUSE_DATABASE=defaultExample Queries
Section titled “Example Queries”Once your data is stored, you can run analytics queries. Here's an example to get email status counts by day:
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;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 } },
]);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;Data Retention
Section titled “Data Retention”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):
DELETE FROM resend_wh_emails
WHERE event_created_at < NOW() - INTERVAL '90 days';Troubleshooting
Section titled “Troubleshooting”Webhook signature verification failing
- Ensure
RESEND_WEBHOOK_SECRETmatches 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


