Push Subscriptions
To receive Push notifications in the user’s browser, you need to register a subscription from the frontend.
Requirements
Section titled “Requirements”- Your app’s VAPID public key (found in the app detail screen in the dashboard)
- An API key with
subscribe_onlyscope
Setting Up the Service Worker
Section titled “Setting Up the Service Worker”First, create a Service Worker file.
self.addEventListener("push", (event) => { const data = event.data?.json() ?? {}; event.waitUntil( self.registration.showNotification(data.title ?? "Notification", { body: data.body, icon: data.icon, badge: data.badge, data: { url: data.url }, }) );});
self.addEventListener("notificationclick", (event) => { event.notification.close(); if (event.notification.data?.url) { event.waitUntil(clients.openWindow(event.notification.data.url)); }});Implementation with the SDK
Section titled “Implementation with the SDK”import { PushCF } from "@todoke/sdk";
const client = new PushCF({ apiKey: "pk_subscribe_only_key",});
// Wait for Service Worker to be readyconst registration = await navigator.serviceWorker.ready;
// Register subscription (browser notification permission dialog appears)await client.subscribe({ registration });// 1. Register Service Workerconst registration = await navigator.serviceWorker.register("/sw.js");
// 2. Get Push subscriptionconst subscription = await registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: urlBase64ToUint8Array("BNfhTEO47qSR..."),});
// 3. Register with todoke API (app_id is derived automatically from API key)await fetch("https://api.todoke.dev/api/v1/subscriptions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer pk_subscribe_only_key", }, body: JSON.stringify({ endpoint: subscription.endpoint, p256dh: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey("p256dh")))), auth: btoa(String.fromCharCode(...new Uint8Array(subscription.getKey("auth")))), platform: "desktop", }),});Unsubscribing
Section titled “Unsubscribing”There is no SDK-style endpoint for unsubscribing; use the app-scoped endpoint that includes the appId in the URL (DELETE /api/v1/apps/:appId/subscriptions).
// REST API (appId in the URL, endpoint in the body)await fetch(`https://api.todoke.dev/api/v1/apps/${appId}/subscriptions`, { method: "DELETE", headers: { "Content-Type": "application/json", "Authorization": "Bearer pk_subscribe_only_key", }, body: JSON.stringify({ endpoint: subscription.endpoint }),});Duplicate Subscriptions (upsert)
Section titled “Duplicate Subscriptions (upsert)”Sending a registration request with the same endpoint does not create duplicate records. Records are upserted keyed on endpoint, and you can tell new from updated by the HTTP status in the response.
| Case | HTTP Status |
|---|---|
Registering a new endpoint | 201 |
Updating info for an existing endpoint | 200 |
App-Scoped Endpoints (explicit appId)
Section titled “App-Scoped Endpoints (explicit appId)”The examples so far used the SDK-style endpoint that determines the app automatically from the API key (POST /api/v1/subscriptions; the SDK-style endpoint is POST-only and has no unsubscribe endpoint). Separately, you can also use endpoints that include the appId in the URL.
# Register (POST /api/v1/apps/:appId/subscriptions)curl -X POST https://api.todoke.dev/api/v1/apps/{appId}/subscriptions \ -H "Authorization: Bearer pk_subscribe_only_key" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "...", "p256dh": "...", "auth": "..." }'
# Unsubscribe (DELETE /api/v1/apps/:appId/subscriptions)curl -X DELETE https://api.todoke.dev/api/v1/apps/{appId}/subscriptions \ -H "Authorization: Bearer pk_subscribe_only_key" \ -H "Content-Type: application/json" \ -d '{ "endpoint": "..." }'SDK-style (/api/v1/subscriptions) | App-scoped (/api/v1/apps/:appId/subscriptions) | |
|---|---|---|
| Supported operations | Register (POST) only | Register / unsubscribe (POST / DELETE) |
| Unsubscribe | None (use the app-scoped endpoint DELETE /api/v1/apps/:appId/subscriptions) | ✅ |
appId | Derived automatically from the API key | Specified as a URL parameter |
When the key’s app and appId don’t match | (N/A) | 403 APP_MISMATCH |
| Subscriber count limit check | None | Yes (see below) |
Subscriber Limit
Section titled “Subscriber Limit”The Free plan can hold up to 1,000 active subscriptions per app. This limit check is performed only on the app-scoped endpoint (POST /api/v1/apps/:appId/subscriptions), and if the limit has already been reached when you try to register a new endpoint, a 429 SUBSCRIBER_LIMIT_EXCEEDED (with upgrade_url) is returned.
For error code details, see Error Codes & Limits.
Browser Support
Section titled “Browser Support”| Browser | Status |
|---|---|
| Chrome / Edge | ✅ |
| Firefox | ✅ |
| Safari 16.4+ (macOS / iOS) | ✅ |
| Safari 16.3 and earlier | ❌ |