Introduction
Notify is a notification infrastructure API that lets you send email, webhook, and in-app notifications with a single API call. You define event types, register subscribers, and trigger notifications — we handle fan-out, retries, and delivery tracking.
Delivered via Resend with high inbox placement
Webhook
HTTP POST to any endpoint with HMAC signature
In-app
Real-time via Socket.IO, buffered for offline users
Quickstart
Get up and running in under 5 minutes.
1. Create a project
Sign up at notify.dev, create a project, and copy your API key from the dashboard. Store it securely — it won't be shown again.
2. Install the SDK
npm install @notify/sdk
Or use the REST API directly — no SDK required.
3. Register a subscriber
SDK
import { Notify } from "@notify/sdk";
const notify = new Notify({
apiKey: process.env.NOTIFY_API_KEY!,
});
await notify.createSubscriber({
externalId: "usr_123", // your user's ID
email: "user@example.com",
webhookUrl: "https://your-server.com/webhooks",
});REST
curl -X POST https://api.notify.dev/v1/subscribers \
-H "Authorization: Bearer nk_live_..." \
-H "Content-Type: application/json" \
-d '{
"externalId": "usr_123",
"email": "user@example.com",
"webhookUrl": "https://your-server.com/webhooks"
}'4. Trigger a notification
SDK
const { notificationId } = await notify.trigger({
eventSlug: "order.placed",
subscriberId: "usr_123",
payload: {
orderId: "ord_999",
amount: 299,
},
idempotencyKey: "ord_999_placed",
});
// returns 202 immediately
// delivery happens in backgroundREST
curl -X POST https://api.notify.dev/v1/notify \
-H "Authorization: Bearer nk_live_..." \
-H "Content-Type: application/json" \
-d '{
"eventSlug": "order.placed",
"subscriberId": "usr_123",
"payload": { "orderId": "ord_999", "amount": 299 },
"idempotencyKey": "ord_999_placed"
}'Authentication
All API requests require an API key passed in the Authorization header.
Authorization: Bearer nk_live_xxxx_secretpart
Key format
API keys follow the format nk_live_<id>_<secret>. The prefix is stored in plaintext for lookup — the full key is hashed and never stored raw. If you lose a key, revoke it from the dashboard and create a new one.
Projects
Projects are the top-level unit of organization. Each project gets its own API keys, event types, and subscribers. Use separate projects for different apps or environments (production/staging).
Event Types
Event types define what notifications your app can send. Each event type has a slug and a list of channels it supports.
Create an event type
await notify.createEventType({
slug: "order.placed", // dot-separated, lowercase
description: "Triggered when a customer places an order",
channels: ["EMAIL", "WEBHOOK", "IN_APP"],
});Slugs must match the pattern lowercase.dotted — e.g. user.signup, payment.failed.
Subscribers
Subscribers are your end users. Register them with their externalId (your internal user ID), email, and optional webhook URL. Calling createSubscriber twice with the same externalId updates the subscriber — it's idempotent.
// register or update a subscriber
await notify.createSubscriber({
externalId: "usr_123",
email: "user@example.com",
webhookUrl: "https://their-server.com/webhooks",
});
// delete a subscriber
await notify.deleteSubscriber("usr_123");Preferences
Subscribers can opt in or out of specific channels per event type. If no preferences are set, the subscriber receives all channels the event type supports.
await notify.updatePreferences("usr_123", {
preferences: [
{
eventSlug: "order.placed",
channel: "EMAIL",
enabled: true,
},
{
eventSlug: "order.placed",
channel: "WEBHOOK",
enabled: false, // opted out of webhook for this event
},
],
});Trigger Notification
/v1/notifyRequest body
eventSlugrequiredstringThe slug of the event type to trigger
subscriberIdrequiredstringThe externalId of the subscriber to notify
payloadrequiredobjectArbitrary JSON data included in the notification
idempotencyKeystringOptional unique key to prevent duplicate deliveries on retry
Response — 202 Accepted
{
"notificationId": "ntf_cmsroi1fi00025...",
"duplicate": false
}Subscribers API
/v1/subscribers/v1/subscribers/:externalId/v1/subscribers/:externalId/v1/subscribers/:externalId/preferences/v1/subscribers/:externalId/preferencesEvent Types API
/v1/events/v1/events/v1/events/:slugDelivery Logs API
/v1/logs/v1/logs/:logIdQuery parameters
statusstringFilter by status: DELIVERED, FAILED, PENDING, RETRYING
channelstringFilter by channel: EMAIL, WEBHOOK, IN_APP
cursorstringCursor for pagination — from nextCursor in previous response
limitnumberNumber of results per page — default 20, max 100
Webhook Verification
Every webhook request is signed with HMAC-SHA256 using your project's webhook secret. Always verify the signature before processing the payload.
Verify with SDK
import { Notify } from "@notify/sdk";
app.post("/webhooks", (req, res) => {
const isValid = Notify.verifyWebhook(
JSON.stringify(req.body),
req.headers["x-notify-signature"] as string,
process.env.NOTIFY_WEBHOOK_SECRET!,
);
if (!isValid) {
return res.status(401).json({ error: "Invalid signature" });
}
const { event, payload, timestamp } = req.body;
// handle the event...
res.json({ ok: true }); // must return 2xx
});Verify manually
import crypto from "crypto";
const signature = req.headers["x-notify-signature"]; // "sha256=abc..."
const body = JSON.stringify(req.body);
const expected = crypto
.createHmac("sha256", process.env.NOTIFY_WEBHOOK_SECRET!)
.update(body)
.digest("hex");
const trusted = `sha256=${expected}`;
// timing-safe comparison
const isValid = crypto.timingSafeEqual(
Buffer.from(trusted),
Buffer.from(signature),
);Request headers
X-Notify-Signaturesha256=<hmac>HMAC-SHA256 signature of the request body
X-Notify-TimestampISO 8601Time the webhook was sent
X-Notify-EventstringThe event slug that triggered this webhook
X-Notify-ProjectstringYour project ID
SDK Usage
The official SDK wraps the REST API with full TypeScript support.
npm install @notify/sdk
import { Notify, NotifySDKError } from "@notify/sdk";
const notify = new Notify({
apiKey: process.env.NOTIFY_API_KEY!,
baseUrl: "https://api.notify.dev", // optional
});
// error handling
try {
await notify.trigger({
eventSlug: "order.placed",
subscriberId: "usr_123",
payload: { orderId: "ord_999" },
});
} catch (err) {
if (err instanceof NotifySDKError) {
console.error(err.status, err.message);
// 404 — event type not found
// 401 — invalid API key
// 429 — rate limit exceeded
}
}Idempotency
Pass an idempotencyKey to prevent duplicate notifications when retrying failed requests. If we receive the same key twice within 24 hours, we return the original response without triggering a second delivery.
await notify.trigger({
eventSlug: "payment.processed",
subscriberId: "usr_123",
payload: { amount: 999 },
idempotencyKey: `payment_${paymentId}_processed`,
// safe to retry — will never double-deliver
});Use a meaningful key — combine the resource ID with the event slug so it's naturally unique per occurrence.
Channels
EMAILrequires: subscriber.emailSent via Resend to the subscriber's email address. Requires email on the subscriber.
WEBHOOKrequires: subscriber.webhookUrlHTTP POST to the subscriber's webhookUrl with HMAC signature. Retried up to 5 times with exponential backoff on 5xx errors.
IN_APPrequires: socket connectionEmitted via Socket.IO in real time. Buffered in Redis for offline subscribers — delivered on next connect.