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.

Email

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

bash
npm install @notify/sdk

Or use the REST API directly — no SDK required.

3. Register a subscriber

SDK

typescript
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

bash
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

typescript
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 background

REST

bash
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.

bash
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

typescript
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.

typescript
// 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.

typescript
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

POST/v1/notify

Request body

eventSlugrequired
string

The slug of the event type to trigger

subscriberIdrequired
string

The externalId of the subscriber to notify

payloadrequired
object

Arbitrary JSON data included in the notification

idempotencyKey
string

Optional unique key to prevent duplicate deliveries on retry

Response — 202 Accepted

json
{
  "notificationId": "ntf_cmsroi1fi00025...",
  "duplicate": false
}

Subscribers API

POST/v1/subscribers
GET/v1/subscribers/:externalId
DELETE/v1/subscribers/:externalId
GET/v1/subscribers/:externalId/preferences
PATCH/v1/subscribers/:externalId/preferences

Event Types API

GET/v1/events
POST/v1/events
DELETE/v1/events/:slug

Delivery Logs API

GET/v1/logs
GET/v1/logs/:logId

Query parameters

status
string

Filter by status: DELIVERED, FAILED, PENDING, RETRYING

channel
string

Filter by channel: EMAIL, WEBHOOK, IN_APP

cursor
string

Cursor for pagination — from nextCursor in previous response

limit
number

Number 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

typescript
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

typescript
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 8601

Time the webhook was sent

X-Notify-Eventstring

The event slug that triggered this webhook

X-Notify-Projectstring

Your project ID

SDK Usage

The official SDK wraps the REST API with full TypeScript support.

bash
npm install @notify/sdk
typescript
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.

typescript
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.email

Sent via Resend to the subscriber's email address. Requires email on the subscriber.

WEBHOOKrequires: subscriber.webhookUrl

HTTP POST to the subscriber's webhookUrl with HMAC signature. Retried up to 5 times with exponential backoff on 5xx errors.

IN_APPrequires: socket connection

Emitted via Socket.IO in real time. Buffered in Redis for offline subscribers — delivered on next connect.