@zyphr-dev/node-sdk
v0.1.59
Published
Official Zyphr SDK for Node.js, React, and React Native
Maintainers
Readme
@zyphr-dev/node-sdk
Official Zyphr SDK for Node.js, React, and React Native. Type-safe access to multi-channel notifications, subscriber management, webhooks, and authentication.
Installation
npm install @zyphr-dev/node-sdk
# or
yarn add @zyphr-dev/node-sdkRequirements: Node.js 18+. Zero runtime dependencies.
Quick Start
import { Zyphr } from '@zyphr-dev/node-sdk';
const zyphr = new Zyphr({
apiKey: process.env.ZYPHR_API_KEY!,
});
// Send an email
await zyphr.emails.sendEmail({
to: [{ email: '[email protected]', name: 'Jane' }],
subject: 'Welcome!',
html: '<h1>Hello, {{name}}!</h1>',
templateData: { name: 'Jane' },
});
// Send a push notification
await zyphr.push.sendPush({
subscriberId: 'sub_123',
payload: { title: 'New message', body: 'You have a new notification' },
});
// Send an SMS
await zyphr.sms.sendSms({
to: '+15551234567',
message: 'Your verification code is 123456',
});
// Send an in-app notification
await zyphr.inbox.sendInApp({
subscriberId: 'sub_123',
title: 'Welcome aboard',
body: 'Thanks for signing up!',
});Configuration
const zyphr = new Zyphr({
apiKey: 'zy_live_xxxx', // Required — your API key
baseUrl: 'https://custom.api.url', // Optional — defaults to https://api.zyphr.dev/v1
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | — | Required. API key (zy_live_* or zy_test_*) |
| baseUrl | string | https://api.zyphr.dev/v1 | API base URL override |
API Reference
Emails — zyphr.emails
Send transactional emails with template support, tracking, and scheduling.
// Send a single email
await zyphr.emails.sendEmail({
to: [{ email: '[email protected]', name: 'Jane' }],
from: { email: '[email protected]', name: 'My App' },
subject: 'Order Confirmed',
html: '<h1>Order #{{orderId}} confirmed</h1>',
templateData: { orderId: '12345' },
tag: 'order-confirmation',
trackingEnabled: true,
});
// Send batch emails (up to 100)
await zyphr.emails.sendBatchEmail({
messages: [
{ to: [{ email: '[email protected]' }], subject: 'Hello A', html: '...' },
{ to: [{ email: '[email protected]' }], subject: 'Hello B', html: '...' },
],
});
// Get email details and tracking
const email = await zyphr.emails.getEmail('email_id');
const events = await zyphr.emails.getEmailEvents('email_id');
const tracking = await zyphr.emails.getEmailTracking('email_id');
// List emails with filtering
const { data } = await zyphr.emails.listEmails('delivered', 'welcome', 20, 0);Send options: to, from, replyTo, cc, bcc, subject, html, text, templateId, templateData, tag, metadata, headers, trackingEnabled, sendAt, delay.
SMS — zyphr.sms
Send SMS messages with scheduling and provider configuration.
// Send SMS
await zyphr.sms.sendSms({
to: '+15551234567',
message: 'Your code is 123456',
});
// Batch send (up to 100)
await zyphr.sms.sendBatchSms({
messages: [
{ to: '+15551234567', message: 'Hello from Zyphr' },
{ to: '+15559876543', message: 'Hello from Zyphr' },
],
});
// Manage SMS provider configuration
const config = await zyphr.sms.getSmsConfig();
await zyphr.sms.upsertSmsConfig({
provider: 'twilio',
accountSid: 'AC...',
authToken: '...',
fromNumber: '+15550001234',
});
await zyphr.sms.verifySmsConfig();
// List and inspect
const list = await zyphr.sms.listSms(1, 20, 'delivered');
const detail = await zyphr.sms.getSms('sms_id');Push Notifications — zyphr.push
Send push notifications to individual devices, subscribers, or topic subscribers.
// Send to a subscriber (all their devices)
await zyphr.push.sendPush({
subscriberId: 'sub_123',
payload: {
title: 'New Order',
body: 'Order #12345 has been placed',
icon: 'https://cdn.example.com/icon.png',
clickAction: 'https://app.example.com/orders/12345',
customData: { orderId: '12345' },
},
priority: 'high',
ttl: 3600,
});
// Send to a topic
await zyphr.push.sendPushToTopic('product-updates', {
payload: { title: 'New Feature', body: 'Check out our latest update' },
});
// Topic management
await zyphr.push.subscribePushTopic('alerts', { deviceIds: ['device_1'] });
await zyphr.push.unsubscribePushTopic('alerts', { deviceIds: ['device_1'] });
// Stats and listing
const stats = await zyphr.push.getPushStats();
const pushes = await zyphr.push.listPush('sub_123');In-App Inbox — zyphr.inbox
Manage in-app notifications for the @zyphr-dev/inbox-react component library.
// Send in-app notification
await zyphr.inbox.sendInApp({
subscriberId: 'sub_123',
title: 'New comment',
body: 'Someone replied to your post',
category: 'comments',
cta: { label: 'View Comment', action: '/posts/456#comments' },
icon: 'https://cdn.example.com/comment.svg',
});
// Batch send (up to 100)
await zyphr.inbox.sendBatchInApp({
notifications: [
{ subscriberId: 'sub_123', title: 'Hello', body: '...' },
{ subscriberId: 'sub_456', title: 'Hello', body: '...' },
],
});
// Read operations
const unread = await zyphr.inbox.getUnreadCount('sub_123');
const notifications = await zyphr.inbox.listInbox('sub_123');
// Actions
await zyphr.inbox.markInboxRead('notification_id');
await zyphr.inbox.markAllInboxRead({ subscriberId: 'sub_123' });
await zyphr.inbox.archiveInboxNotification('notification_id');
await zyphr.inbox.deleteInboxNotification('notification_id');Subscribers — zyphr.subscribers
Manage subscribers, their preferences, and consent records.
// Create/upsert subscriber
const subscriber = await zyphr.subscribers.createSubscriber({
externalId: 'user_123',
email: '[email protected]',
name: 'Jane Doe',
timezone: 'America/New_York',
metadata: { plan: 'pro', company: 'Acme' },
});
// Get by ID or external ID
const sub = await zyphr.subscribers.getSubscriber('sub_id');
const subByExt = await zyphr.subscribers.getSubscriberByExternalId('user_123');
// Update
await zyphr.subscribers.updateSubscriber('sub_id', {
name: 'Jane Smith',
metadata: { plan: 'enterprise' },
});
// List with filtering
const subs = await zyphr.subscribers.listSubscribers('subscribed', undefined, 50, 0);
// Preferences
const prefs = await zyphr.subscribers.getSubscriberPreferences('sub_id');
await zyphr.subscribers.setSubscriberPreferences('sub_id', {
preferences: [
{ categoryId: 'marketing', channel: 'email', enabled: false },
],
});
// Unsubscribe/resubscribe
await zyphr.subscribers.unsubscribeSubscriber('sub_id');
await zyphr.subscribers.resubscribeSubscriber('sub_id');
// Consent management
await zyphr.subscribers.recordSubscriberConsent('sub_id', {
type: 'opt_in', source: 'signup_form',
});
const consent = await zyphr.subscribers.getSubscriberConsent('sub_id');
const history = await zyphr.subscribers.getSubscriberConsentHistory('sub_id');
// Categories
const category = await zyphr.subscribers.createCategory({
name: 'Product Updates', slug: 'product-updates',
});Templates — zyphr.templates
Create and manage reusable email templates with variable substitution.
// Create template
const template = await zyphr.templates.createTemplate({
name: 'welcome-email',
subject: 'Welcome, {{name}}!',
html: '<h1>Hello {{name}}</h1><p>Welcome to {{company}}.</p>',
variables: ['name', 'company'],
});
// Render template (preview without sending)
const rendered = await zyphr.templates.renderTemplate('template_id', {
data: { name: 'Jane', company: 'Acme' },
});
// CRUD operations
const t = await zyphr.templates.getTemplate('template_id');
await zyphr.templates.updateTemplate('template_id', { subject: 'New subject' });
await zyphr.templates.deleteTemplate('template_id');
const all = await zyphr.templates.listTemplates(20, 0);Webhooks — zyphr.webhooks
Configure webhook endpoints, monitor deliveries, and manage circuit breakers.
// Create webhook
const webhook = await zyphr.webhooks.createWebhook({
url: 'https://api.example.com/webhooks',
events: ['email.delivered', 'email.bounced', 'subscriber.created'],
active: true,
});
// Delivery management
const deliveries = await zyphr.webhooks.listWebhookDeliveries(
'webhook_id', 'failed', undefined, undefined, undefined, undefined, 20, 0
);
await zyphr.webhooks.retryWebhookDelivery('webhook_id', 'delivery_id');
await zyphr.webhooks.bulkRetryWebhookDeliveries('webhook_id', {
status: 'failed', startDate: new Date('2026-01-01'),
});
// Replay events
await zyphr.webhooks.replayWebhookEvents('webhook_id', {
startDate: new Date('2026-01-01'), endDate: new Date('2026-01-02'),
});
// Testing
await zyphr.webhooks.sendWebhookTestEvent('webhook_id', {
eventType: 'email.delivered',
});
// Metrics and health
const metrics = await zyphr.webhooks.getWebhookMetrics('webhook_id');
const circuit = await zyphr.webhooks.getWebhookCircuitState('webhook_id');
await zyphr.webhooks.closeWebhookCircuit('webhook_id');
// Secret rotation
await zyphr.webhooks.rotateWebhookSecret('webhook_id');
// Metadata
const eventTypes = await zyphr.webhooks.listWebhookEventTypes();
const versions = await zyphr.webhooks.listWebhookVersions();
const ips = await zyphr.webhooks.getWebhookIps();Topics — zyphr.topics
Group subscribers by interest for targeted notifications.
// Create topic
await zyphr.topics.createTopic({
key: 'product-updates',
name: 'Product Updates',
description: 'New features and releases',
});
// Manage subscribers
await zyphr.topics.addTopicSubscribers('product-updates', {
subscriberIds: ['sub_123', 'sub_456'],
});
await zyphr.topics.removeTopicSubscribers('product-updates', {
subscriberIds: ['sub_789'],
});
// CRUD
const topic = await zyphr.topics.getTopic('product-updates');
await zyphr.topics.updateTopic('product-updates', { name: 'Product News' });
await zyphr.topics.deleteTopic('old-topic');
const topics = await zyphr.topics.listTopics(50, 0);Devices — zyphr.devices
Register and manage push notification devices.
// Register device
const device = await zyphr.devices.registerDevice({
userId: 'user_123',
platform: 'ios',
token: 'apns_token_xxx',
metadata: { deviceName: 'iPhone 16 Pro' },
});
// List devices
const devices = await zyphr.devices.listDevices('user_123', 'ios', 20, 0);
// Stats
const stats = await zyphr.devices.getDeviceStats();
// Cleanup
await zyphr.devices.deleteDevice('device_id');
await zyphr.devices.deleteUserDevices('user_123');Authentication — zyphr.auth.*
Full Auth-as-a-Service API for end-user authentication: registration, login, MFA, magic links, OAuth, and more.
Important: Auth endpoints use Application-scoped credentials (
X-Application-Key+X-Application-Secret), which are distinct from the Project-scoped notification API key (zy_live_*/zy_test_*, sent asAuthorization: Bearer). Create an application in the Zyphr dashboard first, then pass the credentials when constructing the client:
const zyphr = new Zyphr({
apiKey: process.env.ZYPHR_API_KEY!, // Project-scoped: zy_live_* / zy_test_*
applicationKey: process.env.ZYPHR_APP_PUBLIC_KEY!, // Application-scoped public: za_live_pub_* / za_test_pub_*
applicationSecret: process.env.ZYPHR_APP_SECRET_KEY!, // Application-scoped secret: za_live_sec_* / za_test_sec_*
});Legacy
za_pub_*/za_sec_*application-key prefixes are deprecated but still accepted.
Common mistake:
POST /v1/auth/registercreates a platform account (for the Zyphr dashboard). To register end users in your application, usePOST /v1/auth/users/registerviazyphr.auth.registration.registerEndUser().
// Register an end user
const result = await zyphr.auth.registration.registerEndUser({
email: '[email protected]',
password: 'SecureP@ss123',
name: 'New User',
});
// result.data?.user — the new user
// result.data?.tokens — { accessToken, refreshToken, expiresIn }
// Login
const session = await zyphr.auth.login.loginEndUser({
email: '[email protected]',
password: 'SecureP@ss123',
});
// User profile (requires end-user access token)
// Use zyphr.asEndUser(token) to authenticate as the end user
const login = await zyphr.auth.login.loginEndUser({
email: '[email protected]',
password: 'SecureP@ss123',
});
const token = login.data?.tokens?.accessToken;
if (!token) throw new Error('No access token returned');
const profile = await zyphr.auth.profile.getEndUser(zyphr.asEndUser(token));
await zyphr.auth.profile.updateEndUser(
{ name: 'Updated Name' },
zyphr.asEndUser(token),
);
// Email verification (acts on the authenticated end user)
await zyphr.auth.emailVerification.sendEmailVerification(
{ redirectUrl: 'https://myapp.com/verified' },
zyphr.asEndUser(token),
);
// Password reset
await zyphr.auth.passwordReset.forgotPassword({ email: '[email protected]' });
// Magic links
await zyphr.auth.magicLinks.sendMagicLink({
email: '[email protected]',
redirectUrl: 'https://myapp.com/auth/callback',
});
// MFA
const mfaStatus = await zyphr.auth.mfa.getMfaStatus('user_123');
await zyphr.auth.mfa.startMfaEnrollment({ userId: 'user_123' });Available auth modules: login, registration, sessions, emailVerification, passwordReset, magicLinks, mfa, oauth, phone, webauthn, profile.
See the Auth-as-a-Service guide for the full setup walkthrough.
WaaS — zyphr.waas.*
Webhooks-as-a-Service: multi-tenant webhook delivery infrastructure for your customers.
// Create a WaaS application
const created = await zyphr.waas.applications.createWaaSApplication({
name: 'My SaaS',
slug: 'my-saas',
description: 'Webhook delivery for My SaaS customers',
});
const appId = created.data?.id;
if (!appId) throw new Error('App id missing from response');
// Define event types
await zyphr.waas.eventTypes.createWaaSEventType(appId, {
eventType: 'order.created',
name: 'Order Created',
description: 'Fired when a new order is placed',
examplePayload: { orderId: '123', total: 99.99 },
});
// Publish events — fans out to all matching tenant endpoints
await zyphr.waas.events.publishWaaSEvent(appId, {
eventType: 'order.created',
tenantId: 'cust_abc123',
data: { orderId: '456', total: 149.99 },
});
// Generate a tenant-scoped portal token for embedded UI
const portalToken = await zyphr.waas.portal.generateWaaSPortalToken(appId, {
tenantId: 'cust_abc123',
});
// Pass portalToken.data?.token to @zyphr-dev/webhook-portalAvailable WaaS modules: applications, eventTypes, endpoints, events, deliveries, portal.
Error Handling
The SDK provides typed error classes for different failure scenarios:
import {
Zyphr,
ZyphrError,
ZyphrValidationError,
ZyphrAuthenticationError,
ZyphrRateLimitError,
ZyphrNotFoundError,
ZyphrPayloadTooLargeError,
} from '@zyphr-dev/node-sdk';
try {
await zyphr.subscribers.getSubscriber('non_existent_id');
} catch (error) {
if (error instanceof ZyphrNotFoundError) {
console.log('Not found:', error.message); // 404
} else if (error instanceof ZyphrValidationError) {
console.log('Invalid input:', error.details); // 400/422
} else if (error instanceof ZyphrAuthenticationError) {
console.log('Check your API key'); // 401/403
} else if (error instanceof ZyphrPayloadTooLargeError) {
// 413 — see https://zyphr.dev/resources/api-limits
console.log(`Body too large: sent ${error.receivedBytes}, max ${error.limitBytes}`);
} else if (error instanceof ZyphrRateLimitError) {
console.log('Retry after:', error.retryAfter); // 429
} else if (error instanceof ZyphrError) {
console.log('API error:', error.status, error.code);
}
}Error Properties
All error classes extend ZyphrError, which extends Error:
| Property | Type | Description |
|----------|------|-------------|
| status | number | HTTP status code |
| code | string \| undefined | Machine-readable error code |
| requestId | string \| undefined | Request ID for support tickets |
| details | Record<string, unknown> \| undefined | Validation errors and context |
| retryAfter | number \| undefined | Seconds to wait (ZyphrRateLimitError only) |
| limitBytes | number \| undefined | Server-enforced body cap (ZyphrPayloadTooLargeError only) |
| receivedBytes | number \| undefined | Size of the rejected body (ZyphrPayloadTooLargeError only) |
Branch on error.code, not error.message
error.code carries the API's stable, machine-readable code — branch on it
instead of matching message text (which is display copy and may change).
The code is the exact value from the API response body (e.g. account_frozen,
account_throttled, otp_invalid, email_not_found), not a generic
per-status placeholder:
Behavior change (≥ 0.1.30):
error.codenow reflects the API's actual machine-readable code for every status. Previously the SDK overwrote it with a generic per-status placeholder (authentication_error,rate_limit_exceeded,validation_error, …). If you were matching those generic strings, switch toinstanceoffor the error class and useerror.codefor the specific condition.error.statusand the error classes are unchanged.
try {
await zyphr.emails.sendEmail({ /* ... */ });
} catch (error) {
if (error instanceof ZyphrError) {
switch (error.code) {
case 'account_frozen': // 403 — sending paused for reputation; see the dashboard
case 'account_throttled': // 429 — sending throttled; back off and retry
// ...handle the account's sending state
break;
case 'otp_invalid': // 400 — wrong/expired OTP
case 'email_not_found': // 404 — no account for this email (registration fallback)
// ...
break;
}
}
}The full catalog of codes per endpoint is at
zyphr.dev/docs/resources/error-codes.
Reading error.code also flows through the Zyphr MCP server, so agents get the
same actionable code.
Bundle
Dual-format build — works with both ESM and CommonJS:
- ESM:
dist/index.js - CommonJS:
dist/index.cjs - Types:
dist/index.d.ts
Zero runtime dependencies.
Related Packages
| Package | Description |
|---------|-------------|
| @zyphr-dev/inbox-react | Drop-in React notification inbox components |
| @zyphr-dev/webhook-portal | Embeddable webhook management portal |
License
MIT
