Track recipient delivery with a signed webhook
Authenticate raw bytes, reject stale signatures, and persist independent state for each recipient.
Local fixtures, not a delivery claim.Local fixtures on Node.js 22.23.2 and contract review; authenticated sandbox and delivery not executed.
The public site does not accept credentials or send email. Authenticated use happens only in your own backend.
Before you start
- Node.js 22.23.2 with experimental node:sqlite.
- For the optional receiver: the signing secret of an authorized outgoing webhook endpoint, stored only on your backend.
- The receiver creates .1loc-data with mode 0700 and SQLite files with mode 0600. Custom data directories must be private.
- An HTTPS reverse proxy, request/rate limits and an authorized endpoint registration are needed before exposing the local receiver. No registration is performed by this starter.
Run the local example
- Run npm run mock to verify a signed synthetic notification and its repeat, without starting a server or sending email.
- Run npm test to exercise byte-preserving verification, expiry, malformed signatures, restart deduplication and per-recipient state.
- To adapt a local receiver, set OVEYON_WEBHOOK_SECRET on the backend and run npm run receiver. It listens only on 127.0.0.1:3001/webhook.
- Acknowledge only after the small database transaction commits; queue heavier work separately and keep response time within the provider 10-second limit.
Expected result: The first fixture updates mock-recipient-0001 to delivered. Repeating it returns duplicate: true. A different recipient can independently be bounced.
The complete files
These exact files are included in the starter. Test fixtures use synthetic addresses and mock identifiers.
README.md
# Track recipient delivery with a signed webhook
Maintained by Oveyon. MIT licensed. Version 1.0.0.
Canonical: https://1loc.dev/recipes/delivery-webhook/
Authenticate raw bytes, reject stale signatures, and persist independent state for each recipient.
## Run locally
Node.js 22.23.2. No install is needed; there are no external dependencies.
```sh
npm run mock
npm test
```
These commands do not send email or make API requests. SQLite, when used, is experimental in Node 22.
## Prerequisites
- Node.js 22.23.2 with experimental node:sqlite.
- For the optional receiver: the signing secret of an authorized outgoing webhook endpoint, stored only on your backend.
- The receiver creates .1loc-data with mode 0700 and SQLite files with mode 0600. Custom data directories must be private.
- An HTTPS reverse proxy, request/rate limits and an authorized endpoint registration are needed before exposing the local receiver. No registration is performed by this starter.
## Procedure
1. Run npm run mock to verify a signed synthetic notification and its repeat, without starting a server or sending email.
2. Run npm test to exercise byte-preserving verification, expiry, malformed signatures, restart deduplication and per-recipient state.
3. To adapt a local receiver, set OVEYON_WEBHOOK_SECRET on the backend and run npm run receiver. It listens only on 127.0.0.1:3001/webhook.
4. Acknowledge only after the small database transaction commits; queue heavier work separately and keep response time within the provider 10-second limit.
## Expected result
The first fixture updates mock-recipient-0001 to delivered. Repeating it returns duplicate: true. A different recipient can independently be bounced.
## Permissions and effects
Synthetic mode uses an in-memory database only. The local receiver writes a SQLite database and verifies an existing endpoint secret; it does not call Oveyon or create an endpoint.
Optional backend command(s): npm run receiver.
Keep credentials in .env on your own backend or in a secrets manager. Never commit .env. The public site does not accept credentials.
## Failures
- 401: the raw body, signing secret, HMAC or 300-second timestamp window is invalid.
- 400: the authenticated payload cannot be parsed or does not match the supported outgoing event shape.
- 503: local state could not be committed. Let the provider retry; do not claim the event was stored.
## Limits and verification
- The public OpenAPI outgoing-event schema is permissive. messageId, optional deliveryId, recipient, status and timestamp were reviewed against the current producer; no real signed provider request was observed.
- Outgoing notifications do not expose the inbound eventId guarantee. This example deduplicates the same event/recipient/timestamp tuple and applies status idempotently; separately emitted equivalent notifications may remain separate observations.
- Recipient identity always uses messageId plus the exact recipient address. Optional deliveryId does not change that identity or bypass ordering/deduplication.
- Only delivered/bounced and their fallback variants are stored. Engagement, complaint, blocked, inbound and approval events are ignored by this focused handler.
- An older notification cannot overwrite a newer observation. Equal-time conflicting states are not ordered; reconcile them with the message API.
- Recipient-server acceptance does not establish inbox placement. A fresh signed retry may contain an old event timestamp; anti-replay uses the signed header timestamp.
Local fixture tests executed on Node.js 22.23.2 on 2026-10-28. Contract snapshot reviewed; authenticated API/sandbox and real delivery were not executed.
## Sources
- https://api.oveyon.com/
- https://api.oveyon.com/v1/openapi.json
package.json
{
"name": "1loc-delivery-webhook",
"version": "1.0.0",
"private": true,
"type": "module",
"license": "MIT",
"engines": {
"node": ">=22.23.2 <23"
},
"scripts": {
"mock": "OVEYON_MODE=mock node --env-file-if-exists=.env --experimental-strip-types delivery-webhook/main.ts",
"test": "node --experimental-strip-types --test delivery-webhook/recipe.test.ts",
"receiver": "node --env-file-if-exists=.env --experimental-strip-types delivery-webhook/server.ts"
}
}
package-lock.json
{
"name": "1loc-delivery-webhook",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "1loc-delivery-webhook",
"version": "1.0.0",
"license": "MIT",
"engines": {
"node": ">=22.23.2 <23"
}
}
}
}
.env.example
# Backend only; never put the real value in a public site.
OVEYON_WEBHOOK_SECRET=replace_with_your_endpoint_secret
# DELIVERY_DB=.1loc-data/delivery-state.sqlite
# Existing custom data directories must have mode 0700.
.gitignore
.env
.1loc-data/
*.sqlite
*.sqlite-shm
*.sqlite-wal
node_modules/
LICENSE
MIT License
Copyright (c) 2026 Oveyon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
delivery-webhook/main.ts
import { pathToFileURL } from 'node:url';
import { openDeliveryStore, receive } from './receiver.ts';
import { syntheticEvent, syntheticSecret, fixtureHeaders } from './fixture.ts';
export function run() {
const db = openDeliveryStore(':memory:');
try {
const raw = Buffer.from(JSON.stringify(syntheticEvent));
const now = Math.floor(Date.now() / 1000);
const headers = fixtureHeaders(raw, now);
const first = receive(db, raw, headers, syntheticSecret, now);
const repeated = receive(db, raw, headers, syntheticSecret, now);
console.log(JSON.stringify({ mode: 'synthetic fixture; no email is sent', first, repeated, meaning: 'Recipient server acceptance does not prove inbox placement.' }, null, 2));
} finally { db.close(); }
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) run();
delivery-webhook/receiver.ts
import { createHmac, timingSafeEqual } from 'node:crypto';
import type { DatabaseSync } from 'node:sqlite';
import { openPrivateDatabase } from '../shared/private-store.ts';
type Headers = { timestamp: string; signature: string; event?: string };
type Delivery = { event: string; messageId: string; deliveryId?: string; recipient: string; status: string; timestamp: string };
const supported = new Map([['delivered', 'delivered'], ['fallback_delivered', 'delivered'], ['bounced', 'bounced'], ['fallback_bounced', 'bounced']]);
export function verifySignature(raw: Buffer, headers: Headers, secret: string, nowSeconds: number) {
if (raw.byteLength > 65_536 || secret.length < 16 || !/^\d{10,12}$/.test(headers.timestamp) || !/^sha256=[a-f0-9]{64}$/.test(headers.signature)) return false;
const timestamp = Number(headers.timestamp);
if (!Number.isSafeInteger(timestamp) || !Number.isFinite(nowSeconds) || Math.abs(nowSeconds - timestamp) > 300) return false;
const expected = createHmac('sha256', secret).update(`${headers.timestamp}.`).update(raw).digest();
const given = Buffer.from(headers.signature.slice(7), 'hex');
return given.length === expected.length && timingSafeEqual(expected, given);
}
export function openDeliveryStore(path: string) {
const db = openPrivateDatabase(path);
db.exec('PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; CREATE TABLE IF NOT EXISTS seen_events (event_key TEXT PRIMARY KEY); CREATE TABLE IF NOT EXISTS recipient_state (message_id TEXT NOT NULL, recipient_key TEXT NOT NULL, status TEXT NOT NULL, event_at TEXT NOT NULL, PRIMARY KEY(message_id,recipient_key));');
return db;
}
function validString(value: unknown, max: number): value is string { return typeof value === 'string' && value.length > 0 && value.length <= max; }
export function receive(db: DatabaseSync, raw: Buffer, headers: Headers, secret: string, nowSeconds = Date.now() / 1000) {
if (!verifySignature(raw, headers, secret, nowSeconds)) throw new Error('invalid_signature_or_timestamp');
const event: Delivery = JSON.parse(raw.toString('utf8'));
if (!event || !validString(event.event, 80) || (headers.event && headers.event !== event.event)) throw new TypeError('Invalid event name.');
if (!supported.has(event.event)) return { ignored: true, reason: 'This example stores only delivery/bounce outcomes.' };
if (!validString(event.messageId, 100) || !validString(event.recipient, 320) || !validString(event.timestamp, 40) || !Number.isFinite(Date.parse(event.timestamp)) || (event.deliveryId !== undefined && !validString(event.deliveryId, 100))) throw new TypeError('Unsupported outgoing delivery payload.');
const outcome = supported.get(event.event)!;
if (event.status !== outcome) throw new TypeError('Event and recipient status disagree.');
// Outgoing notifications do not promise inbound's eventId. This key identifies
// the same frozen outgoing notification, not every future equivalent event.
// messageId + recipient is stable even when optional deliveryId is absent on
// one notification. Preserve the exact address; do not rewrite its local part.
const recipientKey = event.recipient;
const eventAt = new Date(event.timestamp).toISOString();
const eventKey = JSON.stringify([event.messageId, recipientKey, event.event, eventAt]);
db.exec('BEGIN IMMEDIATE');
try {
const inserted = db.prepare('INSERT OR IGNORE INTO seen_events(event_key) VALUES(?)').run(eventKey);
if (inserted.changes === 0) { db.exec('COMMIT'); return { duplicate: true }; }
// An older notification can be retried after a newer one. Do not overwrite
// the current per-recipient state with an older observation.
db.prepare('INSERT INTO recipient_state(message_id,recipient_key,status,event_at) VALUES(?,?,?,?) ON CONFLICT(message_id,recipient_key) DO UPDATE SET status=excluded.status,event_at=excluded.event_at WHERE excluded.event_at>recipient_state.event_at').run(event.messageId, recipientKey, outcome, eventAt);
db.exec('COMMIT');
return { duplicate: false, recipientKey, status: outcome };
} catch (error) { db.exec('ROLLBACK'); throw error; }
}
delivery-webhook/fixture.ts
import { createHmac } from 'node:crypto';
export const syntheticSecret = 'synthetic-fixture-only-not-a-production-secret';
export const syntheticEvent = { event: 'delivered', messageId: 'mock-message-0001', deliveryId: 'mock-recipient-0001', recipient: '[email protected]', status: 'delivered', detail: {}, timestamp: '2026-10-28T12:00:00.000Z' };
export function fixtureHeaders(raw: Buffer, timestamp: number, secret = syntheticSecret) {
return { timestamp: String(timestamp), signature: `sha256=${createHmac('sha256', secret).update(`${timestamp}.`).update(raw).digest('hex')}`, event: JSON.parse(raw.toString()).event };
}
delivery-webhook/server.ts
import { createServer } from 'node:http';
import { openDeliveryStore, receive } from './receiver.ts';
const secret = process.env.OVEYON_WEBHOOK_SECRET;
if (!secret || secret.length < 16 || secret.includes('replace_')) throw new Error('Provide the actual endpoint secret on your backend, never in a browser.');
const port = Number(process.env.WEBHOOK_PORT ?? '3001');
if (!Number.isInteger(port) || port < 0 || port > 65_535) throw new Error('WEBHOOK_PORT must be an integer between 0 and 65535.');
const db = openDeliveryStore(process.env.DELIVERY_DB ?? '.1loc-data/delivery-state.sqlite');
const server = createServer(async (req, res) => {
if (req.method !== 'POST' || req.url !== '/webhook') { res.writeHead(404).end(); return; }
if (!req.headers['content-type']?.startsWith('application/json')) { res.writeHead(415).end(); req.resume(); return; }
const chunks: Buffer[] = [];
let size = 0;
try {
for await (const chunk of req) {
size += chunk.length;
if (size > 65_536) { res.writeHead(413).end(); req.destroy(); return; }
chunks.push(chunk);
}
const result = receive(db, Buffer.concat(chunks), { timestamp: String(req.headers['x-oveyon-timestamp'] ?? ''), signature: String(req.headers['x-oveyon-signature'] ?? ''), event: String(req.headers['x-oveyon-event'] ?? '') }, secret);
res.writeHead(200, { 'Content-Type': 'application/json' }).end(JSON.stringify(result));
} catch (error) {
// Reject invalid input without logging bodies, addresses, signatures or keys.
res.writeHead(error instanceof Error && error.message === 'invalid_signature_or_timestamp' ? 401 : error instanceof SyntaxError || error instanceof TypeError ? 400 : 503).end();
}
});
server.requestTimeout = 15_000;
server.headersTimeout = 10_000;
server.listen(port, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') throw new Error('Expected a loopback TCP listener.');
console.log(`Local receiver: http://127.0.0.1:${address.port}/webhook. Add your own HTTPS reverse proxy and rate limits before registration.`);
});
function close() { server.close(() => { db.close(); process.exit(0); }); }
process.on('SIGINT', close); process.on('SIGTERM', close);
delivery-webhook/recipe.test.ts
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, statSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { verifySignature, openDeliveryStore, receive } from './receiver.ts';
import { syntheticEvent, syntheticSecret, fixtureHeaders } from './fixture.ts';
const now = 1793196000;
test('signature uses exact raw bytes and signed timestamp, not reserialized JSON', () => {
const raw = Buffer.from(JSON.stringify(syntheticEvent, null, 2)); const headers = fixtureHeaders(raw, now);
assert.equal(verifySignature(raw, headers, syntheticSecret, now), true);
assert.equal(verifySignature(Buffer.from(JSON.stringify(syntheticEvent)), headers, syntheticSecret, now), false);
assert.equal(verifySignature(raw, headers, syntheticSecret, now + 301), false);
assert.equal(verifySignature(raw, { ...headers, timestamp: 'NaN' }, syntheticSecret, now), false);
assert.equal(verifySignature(raw, { ...headers, signature: 'sha256=bad' }, syntheticSecret, now), false);
assert.equal(verifySignature(Buffer.alloc(65_537), headers, syntheticSecret, now), false);
});
test('outgoing notifications persist deduplication and keep each recipient separate', () => {
const directory = mkdtempSync(join(tmpdir(), '1loc-webhook-')); const path = join(directory, 'state.sqlite');
try {
let db = openDeliveryStore(path); const raw = Buffer.from(JSON.stringify(syntheticEvent)); const headers = fixtureHeaders(raw, now);
assert.equal(receive(db, raw, headers, syntheticSecret, now).duplicate, false); db.close();
db = openDeliveryStore(path);
try {
assert.equal(receive(db, raw, headers, syntheticSecret, now).duplicate, true);
const other = Buffer.from(JSON.stringify({ ...syntheticEvent, recipient: '[email protected]', deliveryId: 'mock-recipient-0002', event: 'bounced', status: 'bounced' }));
receive(db, other, fixtureHeaders(other, now), syntheticSecret, now);
assert.equal(db.prepare('SELECT COUNT(*) AS n FROM recipient_state').get()!.n, 2);
assert.equal(db.prepare('SELECT status FROM recipient_state WHERE recipient_key=?').get('[email protected]')!.status, 'delivered');
} finally { db.close(); }
} finally { rmSync(directory, { recursive: true, force: true }); }
});
test('optional deliveryId cannot fragment state or bypass ordering/deduplication', () => {
const db = openDeliveryStore(':memory:');
try {
const newest = { ...syntheticEvent, timestamp: '2026-10-28T13:00:00Z' };
const { deliveryId, ...withoutDeliveryId } = newest;
const first = Buffer.from(JSON.stringify(newest));
receive(db, first, fixtureHeaders(first, now), syntheticSecret, now);
const repeated = Buffer.from(JSON.stringify(withoutDeliveryId));
assert.equal(receive(db, repeated, fixtureHeaders(repeated, now), syntheticSecret, now).duplicate, true);
const old = Buffer.from(JSON.stringify({ ...withoutDeliveryId, event: 'bounced', status: 'bounced', timestamp: '2026-10-28T12:00:00Z' }));
receive(db, old, fixtureHeaders(old, now), syntheticSecret, now);
assert.equal(db.prepare('SELECT COUNT(*) AS n FROM recipient_state').get()!.n, 1);
assert.equal(db.prepare('SELECT status FROM recipient_state').get()!.status, 'delivered');
} finally { db.close(); }
});
test('delivery state creates a private directory and SQLite files under umask 022', () => {
const directory = mkdtempSync(join(tmpdir(), '1loc-private-delivery-'));
const data = join(directory, 'private-data'); const path = join(data, 'state.sqlite');
const previousMask = process.umask(0o022);
try {
const db = openDeliveryStore(path);
try {
const raw = Buffer.from(JSON.stringify(syntheticEvent)); receive(db, raw, fixtureHeaders(raw, now), syntheticSecret, now);
assert.equal(statSync(data).mode & 0o777, 0o700);
for (const file of [path, `${path}-wal`, `${path}-shm`]) assert.equal(statSync(file).mode & 0o777, 0o600);
} finally { db.close(); }
} finally { process.umask(previousMask); rmSync(directory, { recursive: true, force: true }); }
});
test('old notifications cannot overwrite newer recipient state; invalid payload rolls back', () => {
const db = openDeliveryStore(':memory:');
try {
for (const event of [{ ...syntheticEvent, event: 'bounced', status: 'bounced', timestamp: '2026-10-28T13:00:00Z' }, syntheticEvent]) {
const raw = Buffer.from(JSON.stringify(event)); receive(db, raw, fixtureHeaders(raw, now), syntheticSecret, now);
}
assert.equal(db.prepare('SELECT status FROM recipient_state').get()!.status, 'bounced');
const invalid = Buffer.from(JSON.stringify({ ...syntheticEvent, status: 'queued' }));
assert.throws(() => receive(db, invalid, fixtureHeaders(invalid, now), syntheticSecret, now), /disagree/);
} finally { db.close(); }
});
shared/private-store.ts
import { DatabaseSync } from 'node:sqlite';
import { chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, openSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
export function openPrivateDatabase(path: string) {
// Keep this restrictive for the lifetime of the process: SQLite can recreate
// its WAL/SHM files after the opening call has returned.
process.umask(0o077);
if (path === ':memory:') return new DatabaseSync(path);
const absolute = resolve(path);
const directory = dirname(absolute);
mkdirSync(directory, { recursive: true, mode: 0o700 });
const folder = lstatSync(directory);
if (!folder.isDirectory() || folder.isSymbolicLink() || (folder.mode & 0o077) !== 0) {
throw new Error('Use a dedicated data directory with mode 0700; shared directory permissions were not changed.');
}
for (const file of [absolute, `${absolute}-wal`, `${absolute}-shm`]) {
if (!existsSync(file)) continue;
if (!lstatSync(file).isFile() || lstatSync(file).isSymbolicLink()) throw new Error('Database state must be regular files, not symlinks.');
// Harden existing state in place without recreating or truncating it.
chmodSync(file, 0o600);
}
const descriptor = openSync(absolute, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600);
closeSync(descriptor);
chmodSync(absolute, 0o600);
return new DatabaseSync(absolute);
}
Failures to handle
- 401: the raw body, signing secret, HMAC or 300-second timestamp window is invalid.
- 400: the authenticated payload cannot be parsed or does not match the supported outgoing event shape.
- 503: local state could not be committed. Let the provider retry; do not claim the event was stored.
Limits & operational decisions
- The public OpenAPI outgoing-event schema is permissive. messageId, optional deliveryId, recipient, status and timestamp were reviewed against the current producer; no real signed provider request was observed.
- Outgoing notifications do not expose the inbound eventId guarantee. This example deduplicates the same event/recipient/timestamp tuple and applies status idempotently; separately emitted equivalent notifications may remain separate observations.
- Recipient identity always uses messageId plus the exact recipient address. Optional deliveryId does not change that identity or bypass ordering/deduplication.
- Only delivered/bounced and their fallback variants are stored. Engagement, complaint, blocked, inbound and approval events are ignored by this focused handler.
- An older notification cannot overwrite a newer observation. Equal-time conflicting states are not ordered; reconcile them with the message API.
- Recipient-server acceptance does not establish inbox placement. A fresh signed retry may contain an old event timestamp; anti-replay uses the signed header timestamp.
Copy for your agent
The context package contains this recipe’s complete files, prerequisites, execution effects, tests, limitations and sources. It is task information, with no hidden instructions.
Sources & verification
Maintained by Oveyon, the provider used in these examples. This relationship is commercial and explicit. Code is MIT licensed; no independent endorsement is implied.
Next step
Run the mock tests first. For an authenticated integration, create a key with the permissions stated in the recipe and verify the current contract in the official reference. An API acceptance response is not proof of inbox placement.