@wk-xnotify/sdk
v1.3.5
Published
Official XNotify SDK — web push client + management API + React Native (FCM)
Maintainers
Readme
@wk-xnotify/sdk
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 clientXNotifyReactNative— native FCM device registration + listeners via@wk-xnotify/sdk/react-native- React hooks via the
@wk-xnotify/sdk/reactsubpath
Installation
npm install @wk-xnotify/sdk
# or
yarn add @wk-xnotify/sdk
# or
pnpm add @wk-xnotify/sdkReact 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.messageIdSend 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 IDsCreate & 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:
- Identifier-only upload (contact upsert + fallback identifier linking)
- Device-token-linked upload (explicit
deviceTokentodevice_tokens.contact_idlinkage)
// 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:
deviceTokenLinkeddeviceTokenRelinkeddeviceTokenNotFound
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, .timelineWeb 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.jsQuickstart
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/messagingQuickstart (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.
- Initialize the SDK with
apiUrl,authToken, andapplicationId. - Check app status:
const status = await xnotify.getApplicationStatus();
if (status.data?.status === 'pending') {
// run bootstrap once
}- Bootstrap provider credentials once:
await xnotify.bootstrapProviderCredentials({
bootstrapToken: '<one-time-token-from-dashboard>',
firebaseProjectId: 'my-firebase-project',
firebaseProjectName: 'My Firebase Project',
serviceAccount: serviceAccountJson,
});- After status becomes
active, callregisterDevice(...).
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_keyvalue (typically starts withxn_) asauthTokenin SDK calls. - Do not use
api_secretas 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.comBackward 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, useXNotifyWebPushReact 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
