npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

sendry-webhooks-ingester

v0.1.0

Published

Self-hosted ingester that receives Sendry webhook deliveries, verifies signatures, and writes events to your own Postgres.

Readme

sendry-webhooks-ingester

Self-hosted HTTP server that receives Sendry webhook deliveries, verifies their HMAC signatures, and writes the events to your own Postgres database. Skip the handler code — point Sendry at this and query events with SQL.

  • Built on Hono + @hono/node-server.
  • Verifies signatures using the official sendry-sdk helper.
  • Idempotent inserts (ON CONFLICT DO NOTHING) — safe through retries.
  • Bootstraps its own table on startup. No migrations to run.
  • Apache-2.0 licensed.

Quick start

1. Install

npm i -g sendry-webhooks-ingester

Or run the Docker image (see Docker below).

2. Configure

Create a .env (see .env.example) or pass flags:

DATABASE_URL=postgres://user:pass@host:5432/sendry_events
SENDRY_WEBHOOK_SECRET=whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
PORT=4000

Find the webhook secret in the Sendry dashboard: Webhooks → your endpoint → Secret (always starts with whsec_).

3. Run

sendry-ingester
# or
sendry-ingester --port 4000 \
                --database-url postgres://... \
                --webhook-secret whsec_...

The first boot runs CREATE TABLE IF NOT EXISTS sendry_events (...) against your database. From then on it is a simple HTTP receiver.

4. Point Sendry at it

In the Sendry dashboard, open Webhooks → New endpoint and set the URL to:

https://your-ingester.example.com/webhooks/sendry

Subscribe to whichever events you care about (or * for everything). The ingester accepts any event Sendry sends — it doesn't care about the schema beyond a few well-known top-level fields.

5. Query Postgres

-- Last 100 bounces
SELECT id, email_id, occurred_at, payload->'data'->>'reason' AS reason
FROM sendry_events
WHERE event_type = 'email.bounced'
ORDER BY occurred_at DESC
LIMIT 100;

-- Click activity for one recipient over 7 days
SELECT occurred_at, payload->'data'->>'url' AS url
FROM sendry_events
WHERE event_type = 'email.clicked'
  AND email_id = 'email_abc123'
  AND occurred_at > now() - interval '7 days'
ORDER BY occurred_at;

Endpoints

| Method | Path | Description | | ------ | -------------------- | ------------------------------------------- | | POST | /webhooks/sendry | Receives a webhook delivery from Sendry. | | GET | /health | Liveness probe — returns {"status":"ok"}. |

POST /webhooks/sendry

Required headers (sent by Sendry):

  • X-Sendry-SignatureHMAC_SHA256(secret, "{timestamp}.{body}") as hex.
  • X-Sendry-Timestamp — Unix seconds when the request was signed.
  • Content-Type: application/json

Response codes:

| Code | Meaning | | ---- | ------------------------------------------------------ | | 200 | Event accepted (newly stored or duplicate of one we already had). | | 400 | Malformed body (invalid JSON, missing type). | | 401 | Missing/invalid signature or stale timestamp. | | 500 | Database error. |

Successful responses include { "received": true, "duplicate": false } so you can tell new events apart from idempotent retries in logs.

Table schema

The ingester writes to a single table, sendry_events:

| Column | Type | Notes | | ------------------- | ------------- | --------------------------------------------------------------------- | | id | text PK | The delivery ID. ON CONFLICT DO NOTHING makes retries idempotent. | | event_type | text | e.g. email.delivered, email.bounced, contact.created. | | email_id | text null | Promoted from payload.data.email_id/id when present. | | contact_id | text null | Promoted from payload.data.contact_id. | | domain_id | text null | Promoted from payload.data.domain_id. | | automation_run_id | text null | Promoted from payload.data.automation_run_id/run_id. | | occurred_at | timestamptz | From envelope created_at. | | payload | jsonb | The full webhook envelope, including type, created_at, data. | | received_at | timestamptz | Set by Postgres on insert. |

Indexes:

  • (event_type, occurred_at DESC) — typical "last N of event X" queries.
  • (email_id) — per-message timelines.

Run src/schema.sql by hand if you'd rather manage the DDL yourself (the ingester runs the same file at startup).

Docker

# From the packages/ directory of the Sendry monorepo:
docker compose -f sendry-webhooks-ingester/docker-compose.yml up

The compose file ships a Postgres 16 service on port 5433 and the ingester on port 4000. Set SENDRY_WEBHOOK_SECRET in your shell or a .env file.

For a production deploy, build the image directly:

docker build -f sendry-webhooks-ingester/Dockerfile -t sendry-ingester ./packages
docker run -p 4000:4000 \
  -e DATABASE_URL=postgres://... \
  -e SENDRY_WEBHOOK_SECRET=whsec_... \
  sendry-ingester

Programmatic use

import { serve } from "@hono/node-server";
import { createApp, createPool, ensureSchema } from "sendry-webhooks-ingester";

const pool = createPool({ databaseUrl: process.env.DATABASE_URL! });
await ensureSchema(pool);

const app = createApp({
  pool,
  webhookSecret: process.env.SENDRY_WEBHOOK_SECRET!,
});

serve({ fetch: app.fetch, port: 4000 });

You can also mount the app inside an existing Hono server, replace the pool with your own pg pool, or wrap it with auth middleware (e.g. mTLS) if you expose the ingester on the public internet.

Operational notes

  • Run behind TLS. Sendry will only deliver to HTTPS URLs in production.
  • Replay protection. Requests older than 5 minutes (per X-Sendry-Timestamp) are rejected. Tune via maxTimestampSkewSeconds when calling createApp().
  • At-least-once delivery. Sendry retries failed deliveries with exponential backoff. The unique primary key + ON CONFLICT DO NOTHING guarantees you only ever see one row per delivery.
  • Backpressure. Database errors return 500, which causes Sendry to retry. Don't disable retries unless you're prepared to lose events.

License

Apache-2.0