npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wk-xnotify/sdk

v1.3.5

Published

Official XNotify SDK — web push client + management API + React Native (FCM)

Readme

@wk-xnotify/sdk

npm version TypeScript License: MIT

Official JavaScript/TypeScript SDK for XNotify — an enterprise push notification platform.

This single package provides:

  • XNotifySDK — server-side management API (send messages, campaigns, contacts, analytics, …)
  • XNotifyWebPush — browser-side Web Push subscription client
  • XNotifyReactNative — native FCM device registration + listeners via @wk-xnotify/sdk/react-native
  • React hooks via the @wk-xnotify/sdk/react subpath

Installation

npm install @wk-xnotify/sdk
# or
yarn add @wk-xnotify/sdk
# or
pnpm add @wk-xnotify/sdk

React hooks are optional — react is a peer dependency and can be omitted in Node.js-only environments.

For React Native, install peers in the app (optional metadata in this package): react-native, @react-native-firebase/messaging.


Management SDK

Quickstart

import { XNotifySDK } from '@wk-xnotify/sdk';

const client = new XNotifySDK({
  apiKey: process.env.XNOTIFY_API_KEY!,
  environment: 'production', // 'development' | 'staging' | 'production'
});

Or using the factory function:

import { createXNotifyClient } from '@wk-xnotify/sdk';
const client = createXNotifyClient({ apiKey: 'xn_...' });

Send a Message

const result = await client.sendMessage({
  to: '[email protected]',
  channel: 'email',
  subject: 'Welcome!',
  content: 'Thanks for signing up.',
});
// result.data.messageId

Send Bulk Messages

const result = await client.sendBulkMessages([
  { to: '[email protected]', channel: 'email', content: 'Hi Alice!' },
  { to: '[email protected]',   channel: 'email', content: 'Hi Bob!' },
]);
// result.data.messageIds — array of created message IDs

Create & Launch a Campaign

const campaign = await client.createCampaign({
  name: 'Welcome Series',
  type: 'immediate',
  channels: ['email'],
  audience: { segments: ['new-users'] },
  content: { subject: 'Welcome to XNotify!', body: 'Hi {{firstName}}, welcome aboard.' },
});

await client.startCampaign(campaign.data!.campaignId);

Create a Segment (Criteria-Only)

const segment = await client.createSegment({
  name: 'Android active users',
  description: 'Users on Android who were recently active',
  criteria: {
    id: 'root',
    logic: 'AND',
    conditions: [
      { id: 'c1', attribute: 'platform', operator: 'equals', value: 'android' },
      { id: 'c2', attribute: 'last_active', operator: 'greater_than', value: '7' },
    ],
    groups: [],
  },
});

await client.createCampaign({
  name: 'Android promo',
  type: 'immediate',
  channels: ['push'],
  audience: { segments: [segment.data!.segmentId] },
  content: { body: 'Special offer for Android users' },
});

Target a Single User

Send a campaign to one user by matching a contact attribute (currently email). The server resolves the value to the matching contact and only their active device tokens receive the push.

await client.createCampaign({
  name: 'Account update',
  type: 'immediate',
  channels: ['push'],
  audience: {
    targetUser: { type: 'email', value: '[email protected]' },
  },
  content: { subject: 'Your account', body: 'A quick update for you.' },
});

If no contact with the given email exists in the organization, the API returns a 400 with Target user not found for email <value>.

Contact Management

// Create
const { data } = await client.createContact({
  email: '[email protected]',
  firstName: 'John',
  tags: ['vip'],
});

// Bulk upsert (up to 1000)
await client.bulkCreateContacts([
  { email: '[email protected]', firstName: 'Alice' },
  { email: '[email protected]', firstName: 'Bob' },
]);

// List with search
const contacts = await client.listContacts({ search: 'john', limit: 50 });

Bulk Profile Upload (Two Modes)

The SDK supports the customer bulk upload endpoint (POST /api/customer/contacts/upload) in two modes:

  1. Identifier-only upload (contact upsert + fallback identifier linking)
  2. Device-token-linked upload (explicit deviceToken to device_tokens.contact_id linkage)
// 1) Identifier-only bulk upload
await client.bulkUploadProfilesByIdentifiers([
  {
    type: 'profile',
    identity: 'user-123',
    profileData: {
      Name: 'Identifier User',
      Email: '[email protected]',
    },
  },
], { dryRun: false });

// 2) Device-token-linked bulk upload
await client.bulkUploadProfilesWithDeviceTokens([
  {
    type: 'profile',
    identity: 'user-456',
    deviceToken: 'fcm_or_apns_token_here',
    profileData: {
      Name: 'Token Linked User',
      Email: '[email protected]',
    },
  },
]);

Both methods return counters:

  • deviceTokenLinked
  • deviceTokenRelinked
  • deviceTokenNotFound

Use bulkUploadContacts({ d: [...] }, { dryRun: true }) for full low-level control.

Analytics

const analytics = await client.getAnalytics({
  period: '30d',
  granularity: 'day',
});
// analytics.data.sent, .delivered, .opened, .clicked, .bounced, .timeline

Web Push SDK

The XNotifyWebPush client runs in the browser only. It registers a service worker, manages VAPID-based push subscriptions, and communicates with the XNotify API.

Service Worker Setup

Before using XNotifyWebPush, copy sw.js from the XNotify platform to your web app's public root so it is served at https://your-app.com/sw.js.

If using Next.js, copy it to public/sw.js. For Create React App, copy it to public/sw.js. For Vite, copy it to public/sw.js.

# Example: copy from the platform repo
cp node_modules/xnotify-platform/public/sw.js public/sw.js

Quickstart

import { XNotifyWebPush } from '@wk-xnotify/sdk';

const push = new XNotifyWebPush({
  apiUrl: 'https://your-xnotify-instance.com',
  applicationId: 'your-app-id',
  apiKey: 'your-api-key',
  vapidPublicKey: 'your-vapid-public-key',
  userIdentifier: 'user-123', // optional — tie device to a user
  debug: false,
});

// Initialize (registers the service worker)
await push.initialize();

// Request browser permission and subscribe
const subscription = await push.requestPermission();
console.log('Subscribed!', subscription);

Topics

// Subscribe to a topic
await push.subscribeToTopic('breaking-news');

// Unsubscribe
await push.unsubscribeFromTopic('breaking-news');

User Identity

// Call after user logs in to associate the device with their account
await push.updateUserIdentifier('user-456');

Check Status

const status = await push.getSubscriptionStatus();
// { subscribed: true, permission: 'granted', subscription: PushSubscription }

Unsubscribe

await push.unsubscribe();

React Native SDK (@wk-xnotify/sdk/react-native)

Native mobile push uses FCM (and APNs on iOS) — not Web Push. This module does not replace Firebase setup in your app: keep @react-native-firebase/messaging, google-services.json / GoogleService-Info.plist, iOS capabilities, and Android notification channels as you have today.

It wires your FCM token to XNotify’s POST /api/customer/push-notifications/devices API and normalizes notification payloads for foreground, cold start, and tap flows.

Install

npm install @wk-xnotify/sdk @react-native-firebase/messaging

Quickstart (React Native Firebase)

import messaging from '@react-native-firebase/messaging';
import { Platform } from 'react-native';
import {
  XNotifyReactNative,
  createFirebaseMessagingAdapter,
} from '@wk-xnotify/sdk/react-native';

const xnotify = new XNotifyReactNative({
  apiUrl: 'https://your-xnotify-host.example.com',
  authToken: process.env.EXPO_PUBLIC_XNOTIFI_APP_API_KEY!, // org app API key or customer JWT
  applicationId: 'uuid-of-customer_applications-row',
  messaging: createFirebaseMessagingAdapter(messaging()),
  debug: __DEV__,
});

// After login (or on app start)
async function registerPush(userId: string) {
  const token = await xnotify.preparePush(); // iOS: register + permission + getToken
  const platform = Platform.OS === 'ios' ? 'ios' : 'android';
  const res = await xnotify.registerDevice({
    platform,
    userIdentifier: userId,
  });
  if (!res.success) console.error(res.error);
}

// Listeners
const unsubForeground = xnotify.onForegroundMessage((payload) => {
  console.log(payload.title, payload.body, payload.data);
});
const unsubOpen = xnotify.onNotificationOpenedApp((payload) => {
  /* deep link: payload.deepLinkUrl */
});
xnotify.getInitialNotification().then((payload) => {
  if (payload) { /* cold start */ }
});
const unsubRefresh = xnotify.onTokenRefresh((newToken) => {
  void xnotify.registerDevice({
    platform: Platform.OS === 'ios' ? 'ios' : 'android',
    deviceToken: newToken,
    userIdentifier: userId,
  });
});

First-run bootstrap flow (pending app)

New API keys can return a customer_application_id in pending state plus a one-time bootstrap_token.

  1. Initialize the SDK with apiUrl, authToken, and applicationId.
  2. Check app status:
const status = await xnotify.getApplicationStatus();
if (status.data?.status === 'pending') {
  // run bootstrap once
}
  1. Bootstrap provider credentials once:
await xnotify.bootstrapProviderCredentials({
  bootstrapToken: '<one-time-token-from-dashboard>',
  firebaseProjectId: 'my-firebase-project',
  firebaseProjectName: 'My Firebase Project',
  serviceAccount: serviceAccountJson,
});
  1. After status becomes active, call registerDevice(...).

registerDevice now returns an error when applicationId is not active (unless allowPendingRegistration: true is set for controlled testing).

Important auth note:

  • Use the generated api_key value (typically starts with xn_) as authToken in SDK calls.
  • Do not use api_secret as bearer token for SDK/device endpoints.

Security: bootstrap token is single-use and short-lived. Do not persist raw provider credentials in local storage or logs.

Multi-app credential model

@wk-xnotify/sdk/react-native is designed for many customer apps, each with its own push credentials.

  • Your app keeps its own Firebase/APNs setup and generates its own native push token.
  • XNotify stores that token against your applicationId.
  • XNotify backend sends using the credentials linked to that applicationId .
  • If your app token belongs to a different Firebase project than the server credentials, you will see sender mismatch errors.

Topic subscription

FCM topic subscribe/unsubscribe via XNotify’s topic APIs currently expects dashboard permissions (topics:manage). Do not embed those calls in a mobile app with a broad API key — run topic changes from your server or admin tooling.

Analytics / opens

When push data includes a UUID messageId, you can record opens:

await xnotify.trackNotificationEvent({
  eventType: 'opened',
  eventData: { messageId: payload.data.messageId },
  userIdentifier: userId,
});

Troubleshooting

| Issue | What to check | |--------|----------------| | SenderId mismatch | Client Firebase project must match the Firebase credentials mapped to your applicationId in XNotify. | | 400 Invalid … applicationId | applicationId must be an active customer_applications.id under the same org as authToken. | | 403 on unregisterDevice | API key or user needs devices:manage permission. | | iOS no token | Call registerDeviceForRemoteMessages() before getToken() (handled by preparePush()). Rich images may need a Notification Service Extension in the host app. |


React Hooks

useXNotify — Management API

import { useXNotify } from '@wk-xnotify/sdk/react';

function NotificationButton() {
  const { sendMessage, getRateLimitInfo } = useXNotify({
    apiKey: process.env.NEXT_PUBLIC_XNOTIFY_API_KEY!,
  });

  const handleSend = async () => {
    await sendMessage({
      to: '[email protected]',
      channel: 'email',
      content: 'Hello from React!',
    });
  };

  const rateLimit = getRateLimitInfo();

  return (
    <div>
      <button onClick={handleSend}>Send</button>
      {rateLimit && <p>{rateLimit.remaining}/{rateLimit.limit} requests remaining</p>}
    </div>
  );
}

useXNotifyWebPush — Browser Push

import { useXNotifyWebPush } from '@wk-xnotify/sdk/react';

function PushSubscribeButton() {
  const { requestPermission, subscribeToTopic } = useXNotifyWebPush({
    apiUrl: process.env.NEXT_PUBLIC_APP_URL!,
    applicationId: process.env.NEXT_PUBLIC_APP_ID!,
    apiKey: process.env.NEXT_PUBLIC_XNOTIFY_API_KEY!,
    vapidPublicKey: process.env.NEXT_PUBLIC_VAPID_KEY!,
  });

  const handleSubscribe = async () => {
    await requestPermission();
    await subscribeToTopic('announcements');
  };

  return <button onClick={handleSubscribe}>Enable Push Notifications</button>;
}

Error Handling

The SDK throws typed errors — catch them to handle specific failure scenarios:

import {
  XNotifySDK,
  AuthenticationError,
  RateLimitError,
  ValidationError,
  NotFoundError,
} from '@wk-xnotify/sdk';

const client = new XNotifySDK({ apiKey: 'xn_...' });

try {
  await client.sendMessage({ to: '[email protected]', channel: 'email', content: 'Hi' });
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log(`Rate limited. Retry after ${error.retryAfter}s`);
    console.log(`Resets at: ${new Date(error.resetTime)}`);
  } else if (error instanceof AuthenticationError) {
    console.error('Invalid API key');
  } else if (error instanceof ValidationError) {
    console.error('Bad request:', error.message, error.field);
  } else if (error instanceof NotFoundError) {
    console.error('Resource not found');
  } else {
    throw error; // re-throw unexpected errors
  }
}

API Reference

XNotifySDK methods

| Method | Description | |--------|-------------| | healthCheck() | Check API health | | validateApiKey() | Validate current API key | | getAccountInfo() | Get account + organization info | | getUsage(period?) | Get message usage stats | | sendMessage(msg) | Send a single message | | sendBulkMessages(msgs) | Send up to 500 messages | | getMessage(id) | Get message by ID | | getMessageStatus(id) | Get message delivery status | | getMessageHistory(params?) | List messages with filters | | createCampaign(campaign) | Create a campaign | | getCampaign(id) | Get campaign by ID | | updateCampaign(id, updates) | Update campaign fields | | deleteCampaign(id) | Archive a campaign | | listCampaigns(params?) | List campaigns | | startCampaign(id) | Start a draft/paused campaign | | pauseCampaign(id) | Pause a running campaign | | getCampaignAnalytics(id) | Get campaign delivery stats | | createContact(contact) | Create a contact | | getContact(id) | Get contact by ID | | updateContact(id, updates) | Update contact fields | | deleteContact(id) | Soft-delete (unsubscribe) contact | | listContacts(params?) | List contacts with search/tag filter | | bulkCreateContacts(contacts) | Upsert up to 1000 contacts | | bulkUploadContacts(payload, options?) | Call customer bulk upload endpoint (/api/customer/contacts/upload) | | bulkUploadProfilesByIdentifiers(records, options?) | Identifier-based bulk profile upload | | bulkUploadProfilesWithDeviceTokens(records, options?) | Bulk upload with required deviceToken per record | | createTemplate(template) | Create a message template | | getTemplate(id) | Get template by ID | | updateTemplate(id, updates) | Update template | | deleteTemplate(id) | Deactivate a template | | listTemplates(params?) | List active templates | | createWebhook(webhook) | Register a webhook endpoint | | getWebhook(id) | Get webhook by ID | | updateWebhook(id, updates) | Update webhook config | | deleteWebhook(id) | Delete a webhook | | listWebhooks() | List all webhooks | | testWebhook(id) | Send a test event to a webhook | | getAnalytics(params?) | Get delivery + engagement analytics | | createSegment(segment) | Create an audience segment | | getSegment(id) | Get segment by ID | | updateSegment(id, updates) | Update segment | | deleteSegment(id) | Deactivate a segment | | listSegments() | List active segments | | getRateLimitInfo() | Get current rate limit status |

XNotifyWebPush methods

| Method | Description | |--------|-------------| | initialize() | Register service worker, store config | | isPushSupported() | Check browser support | | requestPermission() | Prompt for permission + subscribe | | subscribeToPush() | Subscribe (permission already granted) | | unsubscribe() | Unsubscribe + deregister device | | subscribeToTopic(name) | Subscribe device to a topic | | unsubscribeFromTopic(name) | Unsubscribe device from a topic | | updateUserIdentifier(id) | Associate device with a user | | getSubscriptionStatus() | Get current permission + subscription |

XNotifyReactNative (import from @wk-xnotify/sdk/react-native)

| Method / export | Description | |--------|-------------| | createFirebaseMessagingAdapter(messaging) | Wraps messaging() from @react-native-firebase/messaging | | normalizeRemoteMessage(msg) | Maps FCM remote message → XNotifyPushPayload | | preparePush() | iOS remote registration + permission + FCM token | | getDeviceToken() | Returns normalized FCM token | | registerDevice(opts) | POST …/push-notifications/devices | | unregisterDevice(token) | DELETE …/push-notifications/devices (requires devices:manage) | | onForegroundMessage(cb) | Foreground FCM messages | | onNotificationOpenedApp(cb) | User opened app from notification | | getInitialNotification() | Cold-start notification | | onTokenRefresh(cb) | FCM token rotation | | trackNotificationEvent({ eventType, eventData }) | POST …/analytics/events (needs messageId in eventData) | | getApplicationStatus() | GET /api/v1/bootstrap/mobile-provider status check | | bootstrapProviderCredentials(input) | POST /api/v1/bootstrap/mobile-provider one-time credential bootstrap |


Configuration

XNotifySDK options

interface XNotifyConfig {
  apiKey: string;                                       // Required
  environment?: 'development' | 'staging' | 'production'; // Default: 'production'
  baseUrl?: string;                                      // Override API base URL
  timeout?: number;                                      // ms, default: 30000
  retryAttempts?: number;                                // Default: 3
  debug?: boolean;                                       // Default: false
}

Default management API base URLs

| Environment | Default base URL | |--------|--------| | development | http://localhost:3000/api/v1 | | staging | http://cenomi-xnotify-uat.thankfultree-83cac082.westeurope.azurecontainerapps.io/api/v1 | | production | https://app.xnotify.app/api/v1 |

baseUrl always overrides the environment default:

const client = new XNotifySDK({
  apiKey: process.env.XNOTIFY_API_KEY!,
  baseUrl: 'http://cenomi-xnotify-uat.thankfultree-83cac082.westeurope.azurecontainerapps.io/api/v1',
});

Environment variables (recommended)

XNOTIFY_API_KEY=xn_live_...
XNOTIFY_ENVIRONMENT=production

# For web push (client-side, safe to expose)
NEXT_PUBLIC_XNOTIFY_API_KEY=xn_pub_...
NEXT_PUBLIC_APP_ID=app_...
NEXT_PUBLIC_VAPID_KEY=BxxxxxxX...
NEXT_PUBLIC_APP_URL=https://your-app.com

Backward Compatibility

If you were previously using XNotifiSDK or XNotifiWebPush (with an 'i'), all previous names continue to work:

import { XNotifiSDK, XNotifiWebPush } from '@wk-xnotify/sdk';
import { useXNotifi, useXNotifiWebPush } from '@wk-xnotify/sdk/react';
// These are identical to XNotifySDK, XNotifyWebPush, useXNotify, useXNotifyWebPush

React Subpath Migration (1.1.1+)

Root imports are now Node-safe and no longer include React hooks at runtime.

// Before (1.1.0)
import { useXNotify } from '@wk-xnotify/sdk';

// After (1.1.1+)
import { useXNotify } from '@wk-xnotify/sdk/react';

Segment Criteria Migration

createSegment and updateSegment are now criteria-only. Legacy filters payloads are no longer accepted by v1 segment APIs.

Common migration shape:

// Before (removed)
{ filters: { platform: 'android', last_active_days_gt: 7 } }

// After
{
  criteria: {
    id: 'root',
    logic: 'AND',
    conditions: [
      { id: '1', attribute: 'platform', operator: 'equals', value: 'android' },
      { id: '2', attribute: 'last_active', operator: 'greater_than', value: '7' },
    ],
    groups: [],
  },
}

License

MIT © XNotify Team