Integration guides
Two separate jobs: collecting subscriptions in the browser (public identifier, no secrets) and sending
notifications from a server (secret key, never in the browser). Every example below works as written; the full
files live in examples/ in the repository.
The one rule
The secret API key belongs on a server you control. Not in HTML, not in a public JavaScript file, not in a
Git repository, not in localStorage. If a key ever reaches the browser, rotate it immediately with
POST /v1/sites/rotate-key.
Plain HTML
The minimum viable integration: a button, the SDK, and a status line. Copy
sdk/creator-notify.js and sdk/service-worker.js to your web root first
(Service Worker guide).
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Example site</title>
</head>
<body>
<h1>Example site</h1>
<button id="enable" type="button">Enable notifications</button>
<p id="status" role="status" aria-live="polite"></p>
<script src="/creator-notify.js"></script>
<script>
const notify = new CreatorNotify({
websiteId: 'pub_8d1f0c...',
apiBaseUrl: 'https://YOUR-API-HOST',
serviceWorkerPath: '/service-worker.js'
}).init();
const button = document.getElementById('enable');
const status = document.getElementById('status');
if (!notify.isSupported()) {
button.disabled = true;
status.textContent = 'This browser does not support push notifications.';
}
button.addEventListener('click', async () => {
button.disabled = true;
status.textContent = 'Requesting permission…';
try {
await notify.subscribe();
status.textContent = 'Subscribed. Thanks!';
button.textContent = 'Notifications enabled';
} catch (error) {
status.textContent = `Could not subscribe (${error.code}): ${error.message}`;
button.disabled = false;
}
});
</script>
</body>
</html>
JavaScript admin panel
The browser form never sees the secret key. It posts the message to your endpoint; your server adds the
key and calls Creator Notify. The snippet assumes an Express-style handler at /api/send.
<form id="notify-form">
<label>Title <input name="title" maxlength="160" required></label>
<label>Body <textarea name="body" maxlength="1200"></textarea>
<label>Destination <input name="url" type="url" placeholder="https://example.com/offers"></label>
<button type="submit">Send notification</button>
<p id="result" role="status"></p>
</form>
<script>
const form = document.getElementById('notify-form');
const result = document.getElementById('result');
form.addEventListener('submit', async (event) => {
event.preventDefault();
result.textContent = 'Sending…';
const payload = Object.fromEntries(new FormData(form).entries());
try {
const response = await fetch('/api/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify(payload)
});
const body = await response.json();
if (!response.ok || body.success === false) {
result.textContent = `Failed: ${body.error?.code || response.status} ${body.error?.message || ''}`;
return;
}
result.textContent =
`Accepted ${body.accepted_subscriptions}, succeeded ${body.successful_submissions}, ` +
`failed ${body.failed_submissions}, invalid ${body.invalid_subscriptions}.` +
(body.warnings?.length ? ` Warnings: ${body.warnings.join(', ')}` : '');
} catch (error) {
result.textContent = `Network error: ${error.message}`;
}
});
</script>
Server side (Node.js, Express):
// server.mjs — the key comes from the environment, never from the request
import express from 'express';
const app = express();
app.use(express.json());
app.use(express.static('public'));
app.post('/api/send', async (req, res) => {
const { title, body, url } = req.body;
if (!title || typeof title !== 'string') {
return res.status(400).json({ success: false, error: { code: 'INVALID_REQUEST', message: 'title is required' } });
}
const response = await fetch(`${process.env.CREATOR_NOTIFY_URL}/v1/notifications`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CREATOR_NOTIFY_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ title, body, url: url || undefined })
});
const payload = await response.json().catch(() => ({}));
return res.status(response.status).json(payload);
});
app.listen(3000);
PHP
Full working file: examples/php/send-notification.php. It reads the key from an environment
variable, validates the response and never echoes internals.
<?php
declare(strict_types=1);
$apiBase = getenv('CREATOR_NOTIFY_URL') ?: 'https://YOUR-API-HOST';
$apiKey = getenv('CREATOR_NOTIFY_KEY') ?: '';
if ($apiKey === '') {
http_response_code(500);
error_log('CREATOR_NOTIFY_KEY is not configured');
exit('Notification service is not configured.');
}
$payload = [
'title' => 'New article published',
'body' => 'A new post is available on the blog.',
'url' => 'https://example.com/blog/new-post',
'tag' => 'blog-post',
'data' => ['post_id' => 42],
];
$ch = curl_init($apiBase . '/v1/notifications');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $apiKey,
'Content-Type: application/json',
'Accept: application/json',
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_SLASHES),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($raw === false) {
error_log('Creator Notify transport error: ' . $curlErr);
http_response_code(502);
exit('Could not reach the notification service.');
}
$response = json_decode($raw, true);
if (!is_array($response)) {
error_log('Creator Notify returned a non-JSON response');
http_response_code(502);
exit('Unexpected response from the notification service.');
}
if ($status >= 400 || ($response['success'] ?? false) !== true) {
error_log('Creator Notify error: ' . ($response['error']['code'] ?? 'unknown'));
http_response_code(502);
exit('The notification could not be published.');
}
printf(
'Accepted %d, succeeded %d, failed %d, invalid %d',
$response['accepted_subscriptions'] ?? 0,
$response['successful_submissions'] ?? 0,
$response['failed_submissions'] ?? 0,
$response['invalid_subscriptions'] ?? 0
);
Environment (.env, never committed):
CREATOR_NOTIFY_URL=https://YOUR-API-HOST
CREATOR_NOTIFY_KEY=cn_live_REPLACE_WITH_YOUR_KEY
Existing admin panels
Most admin panels already have a controller that handles "publish post" or "save product". Add one outbound call after the database write, and make it non-fatal:
- Read the API base URL and key from configuration (environment, secrets manager,
config.phpoutside the web root). - Build the smallest payload that makes sense:
title,body,url, and a stabletag. - Send with a short timeout (5–15 s) and log the
request_idon failure. - Never let a notification failure roll back the user's content write.
- If the response is
SITE_EXPIRED, alert the operator — the registration needsPOST /v1/sites/reactivate.
Custom backends
Anything that can make an HTTPS request works. The contract is: POST /v1/notifications,
Authorization: Bearer <key>, Content-Type: application/json, JSON body.
# Python (requests)
import os, requests
response = requests.post(
f"{os.environ['CREATOR_NOTIFY_URL']}/v1/notifications",
headers={
"Authorization": f"Bearer {os.environ['CREATOR_NOTIFY_KEY']}",
"Content-Type": "application/json",
},
json={"title": "Deploy finished", "body": "Build 142 is live.", "url": "https://example.com/status"},
timeout=15,
)
response.raise_for_status()
print(response.json()["successful_submissions"])
# Go (net/http)
# req, _ := http.NewRequest("POST", apiURL+"/v1/notifications", body)
# req.Header.Set("Authorization", "Bearer "+apiKey)
# req.Header.Set("Content-Type", "application/json")
Relaying subscriptions through your backend
Collective sites sometimes proxy the subscription call so they can attach their own user id. Authenticate that call with the secret key instead of the public identifier, and keep the same body shape:
curl -X POST https://YOUR-API-HOST/v1/subscriptions \
-H "Authorization: Bearer $CREATOR_NOTIFY_KEY" \
-H 'Content-Type: application/json' \
-d '{
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/XXXX",
"keys": { "p256dh": "BOr...", "auth": "k9Q..." }
}
}'
In this mode no Origin header is required — the key already proves the tenant. Do not forward raw
browser subscriptions from untrusted clients without your own abuse controls.
Integration checklist
- Service Worker served from your origin at a path that covers the pages you care about.
- Permission requested inside a click handler, not on load.
- Public identifier in the page; secret key only in server configuration.
- Publishing at least once every seven days, or a scheduled reminder.
- Error handling that shows the API's
error.coderather than a generic failure. - An alert when the API returns
SITE_EXPIREDorSITE_SUSPENDED.