TypeScript SDK
Complete reference for the @notifykit/sdk TypeScript/Node.js client.
Installation
npm install @notifykit/sdk
# yarn add @notifykit/sdk
# pnpm add @notifykit/sdk
Requirements: Node.js 18+, TypeScript 5.0+ (optional but recommended)
Initialization
import { NotifyKitClient } from "@notifykit/sdk";
const client = new NotifyKitClient({
apiKey: process.env.NOTIFYKIT_API_KEY!,
baseUrl: "https://api.notifykit.dev", // Optional — defaults to https://api.notifykit.dev
});
Configuration Options
| Option | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Your NotifyKit API key (nh_...) |
baseUrl | string | No | API base URL. Defaults to https://api.notifykit.dev |
Methods
sendEmail(options)
Queue an email for delivery. Returns immediately with a job ID — the email is sent asynchronously.
Parameters:
type EmailProvider = "SENDGRID" | "RESEND" | "POSTMARK";
interface SendEmailOptions {
to: string;
subject: string;
body: string; // Email body (HTML supported)
from?: string; // Sender address. Use your verified domain (e.g. support@yourdomain.com). Paid plans only.
priority?: 1 | 5 | 10; // Job priority: 1=high, 5=normal (default), 10=low
idempotencyKey?: string;
provider?: EmailProvider; // Force this email through a specific provider. Paid plans only.
fallback?: EmailProvider; // Second provider tried only if `provider` fails. Requires `provider`.
}
Returns: Promise<JobResponse>
interface JobResponse {
jobId: string;
status: string; // "pending" on creation
type: string; // "email"
createdAt: string; // ISO 8601 timestamp
}
Example:
const job = await client.sendEmail({
to: "user@example.com",
subject: "Welcome!",
body: "<h1>Hello World</h1>",
priority: 1, // High priority
idempotencyKey: "welcome-user-123",
});
console.log(job.jobId); // "job_abc123"
Emails are sent via NotifyKit's shared infrastructure with automatic failover across supported providers. The sender address is always noreply@notifykit.dev. Custom from addresses are not supported.
Emails are sent via your own provider keys. You must connect at least one provider API key in API Keys before sending emails. Without one, email requests will be rejected.
Configure multiple providers to enable automatic failover — NotifyKit delivers through them in the priority order you set in the dashboard. You can also force a specific provider for a single message using provider and fallback.
Custom from addresses (using your verified domain) are supported on paid plans.
Paid plans can force a single email through a specific provider by passing provider (and optionally fallback) on sendEmail(). Available since SDK 1.1.0. See Per-Message Provider Routing.
await client.sendEmail({
to: "user@example.com",
subject: "Receipt",
body: "<h1>Thanks</h1>",
provider: "SENDGRID",
fallback: "RESEND", // optional; requires provider
});
sendWebhook(options)
Queue a webhook delivery. Returns immediately with a job ID.
Parameters:
interface SendWebhookOptions {
url: string; // Webhook destination URL (must be HTTPS)
payload: any; // JSON payload to deliver (required, max 10kb)
method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; // HTTP method (default: "POST")
headers?: Record<string, string>;
priority?: 1 | 5 | 10;
idempotencyKey?: string;
}
Returns: Promise<JobResponse>
Example:
const job = await client.sendWebhook({
url: "https://api.example.com/webhook",
payload: {
event: "payment.completed",
amount: 49.99,
},
idempotencyKey: "payment-456-webhook",
});
console.log(job.jobId); // "job_xyz789"
getJob(jobId)
Get the full status and details of a specific job.
Parameters:
jobId(string) — The job ID returned fromsendEmailorsendWebhook
Returns: Promise<JobStatus>
interface JobStatus {
id: string;
type: string; // "email" or "webhook"
status: "pending" | "processing" | "completed" | "failed";
priority: number;
payload: object; // The original payload sent
attempts: number; // Number of delivery attempts made
maxAttempts: number; // Maximum attempts (3)
errorMessage?: string;
createdAt: string;
startedAt?: string;
completedAt?: string;
deliveryLogs: DeliveryLog[]; // Per-attempt delivery records
}
interface DeliveryLog {
id: string;
attempt: number;
status: string;
usedProvider: EmailProvider | null;
errorMessage: string | null;
createdAt: string;
}
Example:
const status = await client.getJob("job_abc123");
console.log(status.status); // "completed"
console.log(status.attempts); // 1
if (status.status === "failed") {
console.error("Delivery failed:", status.errorMessage);
}
for (const log of status.deliveryLogs) {
console.log(
`attempt ${log.attempt} via ${log.usedProvider ?? "unknown"}: ${log.status}`,
);
}
deliveryLogs was added in SDK 1.1.0Both email and webhook jobs populate deliveryLogs. For paid plan email jobs, each entry includes usedProvider — the provider that delivered (success) or the last one attempted (failure). Free plan and webhook job entries always have usedProvider: null.
listJobs(options?)
List your jobs with optional filters and pagination.
Parameters:
interface ListJobsOptions {
page?: number; // Page number (default: 1)
limit?: number; // Per page (default: 20, max: 100)
type?: "email" | "webhook"; // Filter by notification type
status?: "pending" | "processing" | "completed" | "failed"; // Filter by status
}
Returns: Promise<{ data: JobSummary[]; pagination: PaginationMeta }>
interface JobSummary {
id: string;
type: string;
status: "pending" | "processing" | "completed" | "failed";
priority: number;
attempts: number;
errorMessage?: string;
createdAt: string;
completedAt?: string;
}
interface PaginationMeta {
page: number;
limit: number;
total: number;
totalPages: number;
}
Example:
const { data, pagination } = await client.listJobs({
status: "failed",
type: "webhook",
limit: 10,
});
console.log(`${pagination.total} failed webhook jobs`);
data.forEach((job) => {
console.log(`${job.id}: ${job.errorMessage} (${job.attempts} attempts)`);
});
retryJob(jobId)
Re-queue a failed job for another delivery attempt.
Parameters:
jobId(string) — ID of the job withfailedstatus
Returns: Promise<RetryJobResponse>
interface RetryJobResponse {
jobId: string;
status: string; // "pending"
message: string;
}
Example:
const result = await client.retryJob("job_xyz789");
console.log(result.message); // "Job has been re-queued for processing"
console.log(result.status); // "pending"
Only jobs with failed status can be retried. pending, processing, and completed jobs cannot be retried.
verifyWebhookSignature(options)
Verify an incoming webhook delivery signed by NotifyKit. Call this at your receiving endpoint before processing the payload.
Parameters:
interface VerifyWebhookSignatureOptions {
payload: string; // Raw request body as a string — do NOT pass a parsed object
timestamp: string; // Value of the X-Webhook-Timestamp header
signature: string; // Value of the X-Webhook-Signature header (t=<ts>,v1=<hex>)
secret: string; // Plaintext webhook signing secret from your dashboard
tolerance?: number; // Max request age in seconds before rejection (default: 300)
}
Returns: boolean — true if the signature is valid and within the tolerance window, false otherwise. Never throws.
Example:
import { verifyWebhookSignature } from "@notifykit/sdk";
app.post("/webhooks/notifykit", (req, res) => {
const valid = verifyWebhookSignature({
payload: req.rawBody,
timestamp: req.headers["x-webhook-timestamp"],
signature: req.headers["x-webhook-signature"],
secret: process.env.NOTIFYKIT_WEBHOOK_SECRET!,
});
if (!valid) return res.status(401).send("Invalid signature");
// safe to process
res.sendStatus(200);
});
Pass the raw request body string — not the parsed JSON object. Re-serializing a parsed object can produce a different byte sequence and will cause verification to fail. Use express.raw({ type: "application/json" }) on your webhook route.
verifyWebhookSignature is a standalone export — it does not require an initialized NotifyKitClient. Available since @notifykit/sdk@1.3.0.
See Webhook Security for the full setup guide.
ping()
Test API connectivity.
Returns: Promise<string>
Example:
const pong = await client.ping();
console.log(pong); // "pong"
getApiInfo()
Get API version and metadata.
Returns: Promise<ApiInfo>
interface ApiInfo {
name: string;
version: string;
description: string;
documentation: string;
}
Example:
const info = await client.getApiInfo();
console.log(info.name); // "NotifyKit API"
console.log(info.version); // "1.0.0"
Error Handling
All API errors throw a NotifyKitError.
import { NotifyKitClient, NotifyKitError } from "@notifykit/sdk";
try {
await client.sendEmail({
to: "invalid-email",
subject: "Test",
body: "Hello",
});
} catch (error) {
if (error instanceof NotifyKitError) {
// Formatted message with status code
console.error(error.getFullMessage());
// Check specific codes
if (error.isStatus(400)) {
console.error("Bad request:", error.message);
} else if (error.isStatus(401)) {
console.error("Invalid API key");
} else if (error.isStatus(403)) {
// Could be: quota exceeded, account inactive
// On retryJob: also domain not verified or no provider configured
console.error("Forbidden:", error.message);
} else if (error.isStatus(409)) {
console.error("Duplicate idempotency key");
} else if (error.isStatus(429)) {
console.error("Rate limit exceeded");
}
// Raw details
console.error("Status code:", error.statusCode);
console.error("Validation errors:", error.errors); // Array, if any
}
}
NotifyKitError Properties
| Property | Type | Description |
|---|---|---|
message | string | Error message from the API |
statusCode | number | undefined | HTTP status code. undefined on network errors with no HTTP response |
response | any | Full raw API response |
errors | any[] | undefined | Validation error details, typically populated on 400 errors |
retryAfter | number | undefined | Seconds to wait before retrying. Only set on 429s that include the field |
NotifyKitError Methods
| Method | Description |
|---|---|
isStatus(code) | Returns true if statusCode === code |
getFullMessage() | Returns [statusCode] message\nValidation errors: ... |
TypeScript Types
All types are exported from the package:
import type {
NotifyKitConfig,
SendEmailOptions,
SendWebhookOptions,
JobResponse,
JobStatus,
JobSummary,
DeliveryLog,
EmailProvider,
RetryJobResponse,
PaginationMeta,
ApiInfo,
VerifyWebhookSignatureOptions,
} from "@notifykit/sdk";
Domain verification is dashboard-only (Domains). The SDK does not export domain types or methods.
Complete Example
import { NotifyKitClient, NotifyKitError } from "@notifykit/sdk";
const client = new NotifyKitClient({
apiKey: process.env.NOTIFYKIT_API_KEY!,
});
async function notifyUserAndSystem(
userId: string,
userEmail: string,
orderId: string,
) {
try {
// Send welcome email
const emailJob = await client.sendEmail({
to: userEmail,
subject: `Order #${orderId} Confirmed`,
body: `<h1>Thanks for your order!</h1><p>Order ID: ${orderId}</p>`,
priority: 1,
idempotencyKey: `order-${orderId}-email`,
});
console.log(`Email queued: ${emailJob.jobId}`);
// Notify internal system via webhook
const webhookJob = await client.sendWebhook({
url: "https://internal.your-app.com/order-events",
payload: { event: "order.confirmed", orderId, userId },
idempotencyKey: `order-${orderId}-webhook`,
});
console.log(`Webhook queued: ${webhookJob.jobId}`);
// Check email delivery after 10 seconds
setTimeout(async () => {
const status = await client.getJob(emailJob.jobId);
if (status.status === "failed") {
console.error(`Email delivery failed: ${status.errorMessage}`);
// Optionally retry
await client.retryJob(emailJob.jobId);
}
}, 10_000);
} catch (error) {
if (error instanceof NotifyKitError) {
if (error.isStatus(409)) {
console.log("Already notified — idempotency key matched");
} else if (error.isStatus(403)) {
console.error("Quota or permission error:", error.message);
} else {
console.error("Notification failed:", error.getFullMessage());
}
}
throw error;
}
}