JavaScript SDK
sdk/creator-notify.js is a dependency-free browser script (~10 KB unminified) that registers your
Service Worker, requests permission, creates a push subscription and uploads it to your Creator Notify
deployment. It contains no secret key and can be served as a plain static file.
Installation
Copy the two files from the repository to your site root and load the script with a normal tag:
cp sdk/creator-notify.js /var/www/example.com/creator-notify.js
cp sdk/service-worker.js /var/www/example.com/service-worker.js
<script src="/creator-notify.js"></script>
The script exposes a global CreatorNotify constructor and also supports CommonJS
(require('./creator-notify.js')) and AMD loaders. There is no npm package and no CDN URL that this
project publishes — host the file yourself, or build a CDN release from your own fork.
Initialization
const notify = new CreatorNotify({
websiteId: 'pub_8d1f0c...', // required — public identifier from /v1/sites/register
apiBaseUrl: 'https://YOUR-API-HOST', // required in practice — no trailing slash needed
serviceWorkerPath: '/service-worker.js', // default: '/service-worker.js'
serviceWorkerScope: '/', // default: '/'
vapidPublicKey: undefined, // optional — fetched from the API when omitted
onSubscribed: (result) => {}, // optional
onUnsubscribed: (result) => {}, // optional
onError: (error) => {}, // optional
fetchOptions: {} // optional — merged into every fetch()
});
| Option | Type | Description |
|---|---|---|
websiteId | string, required | Public identifier. Safe to publish. Grants no send rights. |
apiBaseUrl | string | Origin of your deployment. Empty means "same origin as the page". |
serviceWorkerPath | string | Absolute path of the Service Worker file on your origin. |
serviceWorkerScope | string | Registration scope. '/' requires the worker to be served from the root path or to send Service-Worker-Allowed. |
vapidPublicKey | string | Skips the bootstrap request to /v1/subscriptions/config. |
onSubscribed / onUnsubscribed / onError | function | Callbacks fired in addition to the returned promise. |
fetchOptions | object | Merged into every fetch() call — useful for mode, credentials or custom headers. |
init() performs feature detection only. It never prompts the user.
notify.init();
isSupported() and isSecureContext()
if (!notify.isSupported()) {
// Service Worker, Push API or Notification missing, or the page is not a secure context
}
notify.isSecureContext(); // boolean — HTTPS or localhost
isSupported() checks 'serviceWorker' in navigator, 'PushManager' in window,
the presence of Notification and window.isSecureContext.
Permission handling
notify.getPermission(); // 'default' | 'granted' | 'denied' | 'unsupported'
await notify.requestPermission(); // 'granted' | 'denied' | 'default'
Always call from a user gesture
Chrome requires a user activation for Notification.requestPermission(); Firefox shows a quieter
prompt; Safari only allows pushes for sites added to the Home Screen. Calling this on page load produces
dismissals and permanently denied permissions. subscribe() calls it for you — wrap the whole call
in a click handler.
subscribe()
try {
const { subscription, created } = await notify.subscribe();
console.log(created ? 'New subscriber' : 'Subscription refreshed', subscription.id);
} catch (error) {
console.error(error.code, error.message);
}
Resolution order inside subscribe():
- Feature detection and a secure-context check.
Notification.requestPermission().- Fetch the VAPID public key (unless supplied in the constructor).
- Register the Service Worker and wait until it is active.
- Reuse an existing
PushSubscriptionor create one withuserVisibleOnly: true. POST /v1/subscriptionswith{ website_id, subscription: { endpoint, keys, expiration_time } }.
Re-subscribing with the same endpoint is safe: the server updates the stored key material and reactivates the row.
unsubscribe()
const { removed } = await notify.unsubscribe();
Makes a DELETE /v1/subscriptions call for the current endpoint, then calls
pushSubscription.unsubscribe(). If the API call fails the browser subscription is still removed and
the error is passed to onError. Removal is idempotent: unsubscribing twice is not an error.
getSubscriptionStatus()
const status = await notify.getSubscriptionStatus();
// { supported: true, permission: 'granted', subscribed: true, endpoint: 'https://fcm.googleapis.com/...' }
This reflects browser state only. It does not ask the API whether the endpoint is still valid —
push services can invalidate an endpoint at any time. Use GET /v1/subscriptions/count on your
server for authoritative numbers.
Error handling
Every rejection is a CreatorNotifyError with a stable code:
| Code | Meaning | What to do |
|---|---|---|
MISSING_WEBSITE_ID | Constructor called without websiteId | Pass the public identifier |
UNSUPPORTED | No Service Worker / Push API, or insecure context | Hide the button; explain HTTPS |
PERMISSION_DENIED | The user dismissed or blocked the prompt | Show instructions for browser settings |
PERMISSION_ERROR | The browser threw while prompting | Retry on a later gesture |
SERVICE_WORKER_ERROR | Registration failed — wrong path, wrong origin, no HTTPS | Check that the file is served from your origin with text/javascript |
SITE_NOT_FOUND | Unknown websiteId, or the Origin does not match the registration | Re-check the identifier and the registered origin |
SITE_EXPIRED | The registration lapsed (no publish for seven days) | Reactivate server-side; subscribers keep working afterwards |
INVALID_SUBSCRIPTION | Malformed endpoint or keys | Re-subscribe; if it persists the payload is corrupt |
PUSH_ENDPOINT_NOT_ALLOWED | Endpoint host not in ALLOWED_PUSH_HOSTS | Add the push service host to the allow-list |
RATE_LIMIT_EXCEEDED | Too many subscription calls from one client | Back off; the limit resets within a minute |
NETWORK_ERROR | The API was unreachable or CORS blocked the request | Check apiBaseUrl and the CORS section of Troubleshooting |
SUBSCRIPTION_CHANGED | The browser rotated the subscription (delivered via onError) | Call subscribe() again on the next interaction |
Service Worker messages
notify.onError = (error) => {
if (error.code === 'SUBSCRIPTION_CHANGED') showRenewBanner();
};
When the browser fires pushsubscriptionchange, the Service Worker tells the page. The SDK surfaces
it as a SUBSCRIPTION_CHANGED error; re-subscribing from a gesture restores delivery.
Complete example
<button id="notify-toggle" type="button" hidden>Enable notifications</button>
<p id="notify-status" role="status"></p>
<script src="/creator-notify.js"></script>
<script>
const notify = new CreatorNotify({
websiteId: 'pub_8d1f0c...',
apiBaseUrl: 'https://YOUR-API-HOST'
}).init();
const button = document.getElementById('notify-toggle');
const status = document.getElementById('notify-status');
if (!notify.isSupported()) {
status.textContent = 'This browser cannot show push notifications.';
} else {
notify.getSubscriptionStatus().then((state) => {
button.hidden = false;
button.textContent = state.subscribed ? 'Disable notifications' : 'Enable notifications';
});
button.addEventListener('click', async () => {
button.disabled = true;
try {
const state = await notify.getSubscriptionStatus();
if (state.subscribed) {
await notify.unsubscribe();
button.textContent = 'Enable notifications';
status.textContent = 'Notifications disabled.';
} else {
await notify.subscribe();
button.textContent = 'Disable notifications';
status.textContent = 'Subscribed.';
}
} catch (error) {
status.textContent = `${error.code}: ${error.message}`;
} finally {
button.disabled = false;
}
});
}
</script>