Start typing to search the documentation.

to navigate·to open

Push

Push API

The WRNexus Web Push HTTP API — the public install surface (SDK loader, service-worker template, config), sessionless subscription / preference / tracking endpoints, and the workspace-scoped site, campaign, and segment management endpoints.

The Push API has two surfaces:

  • Public (sessionless) — the install assets, config, subscription registration, preferences, and first-party tracking. These are reached by the on-page SDK and the service worker from arbitrary customer origins. They take no session and no API key: each is tenant-scoped by the non-secret site key, and every response carries Access-Control-Allow-Origin: *.
  • Authenticated (workspace-scoped) — site, campaign, and segment management plus analytics. These use your dashboard session (or an API key) and are filtered on the caller’s active workspace. Mutations require the owner or admin workspace role.

Base URL

https://api.wrnexus.com

The public install assets live under /push/*; everything else lives under /api/push/*.


Public install surface

GET /push/sdk.js

The on-page SDK loader, served verbatim and cacheable (max-age=600). Loaded via <script src="…/push/sdk.js" data-site-id="SITE_KEY">. See Install the snippet.

GET /push/sw.js

The service-worker template you copy to your own origin. Short cache so you always grab the latest. See Service worker setup.

GET /push/config

Resolve a site’s runtime configuration from its public site key. Called by the SDK; needs no per-site build.

Query Type Notes
siteKey string Required. The site’s publicKey. Aliases: publicKey, siteId, site_id.
curl "https://api.wrnexus.com/push/config?siteKey=SITE_KEY"

Response 200 OK:

{
  "ok": true,
  "siteKey": "SITE_KEY",
  "vapidPublicKey": "BPx…",
  "apiBase": "https://api.wrnexus.com",
  "domain": "your-domain.example",
  "icon": "https://your-domain.example/icon.png"
}

An unknown or inactive site returns 404 with { "ok": false, "code": "UNKNOWN_SITE" }.


Public subscription endpoints

POST /api/push/subscriptions

Register (or re-activate) a browser subscription. The owning tenant is resolved from the site identified by key — the client never supplies an org/workspace id. A re-subscribe with the same endpoint reactivates the existing row and shallow-merges the incoming metadata.

The body is read as raw bytes, so the SDK sends it as a CORS-simple text/plain request (no preflight). The JSON shape:

Field Type Notes
key string Required. The site key. Aliases: siteKey, publicKey.
endpoint string Required. The W3C PushSubscription.endpoint.
keys object { p256dh, auth } (base64url). Required unless sent flat.
p256dh / auth string Flat alternative when not nested under keys.
metadata object Optional. Page/visitor metadata, shallow-merged on re-subscribe.
curl https://api.wrnexus.com/api/push/subscriptions \
  -H "Content-Type: text/plain;charset=UTF-8" \
  --data '{
    "key": "SITE_KEY",
    "endpoint": "https://fcm.googleapis.com/fcm/send/abc…",
    "keys": { "p256dh": "BMx…", "auth": "k9…" },
    "metadata": { "url": "https://your-domain.example/", "lang": "en-US" }
  }'

Response 201 Created: { "ok": true, "id": "…" }

Error HTTP Meaning
INVALID_BODY 400 Malformed JSON.
INVALID_SUBSCRIPTION 400 key, endpoint, p256dh, or auth missing.
UNKNOWN_SITE 404 No site for that key.
SITE_INACTIVE 403 The site is not accepting subscriptions.

POST /api/push/subscriptions/unsubscribe

CORS-simple unsubscribe keyed by (siteKey, endpoint) — the SDK calls this after it unsubscribes the browser’s PushSubscription. Deactivates the matching subscription (soft delete) and records an unsubscribed event. Idempotent: an already-removed subscription still returns success.

Field Type Notes
key string Required. Site key. Aliases: siteKey, publicKey.
endpoint string Required. The subscription endpoint.
curl https://api.wrnexus.com/api/push/subscriptions/unsubscribe \
  -H "Content-Type: text/plain;charset=UTF-8" \
  --data '{ "key": "SITE_KEY", "endpoint": "https://fcm.googleapis.com/fcm/send/abc…" }'

Response 200 OK: { "ok": true }

DELETE /api/push/subscriptions/{id}

Unsubscribe by the server-side subscription id. An optional key query parameter must match the subscription’s site (an extra guard).

curl -X DELETE "https://api.wrnexus.com/api/push/subscriptions/SUB_ID?key=SITE_KEY"

Response 200 OK: { "ok": true, "id": "SUB_ID" }. A missing subscription returns 404; a mismatched key returns 403.


Public preferences endpoints

Per-topic opt-in state, resolved by the (siteKey, endpoint) pair. A campaign tagged with a topic suppresses subscribers who have opted out of it.

GET /api/push/preferences

Query Type Notes
key string Required. Site key.
endpoint string Required. The subscription endpoint.
{
  "ok": true,
  "preferences": [
    { "topic": "order-updates", "optedIn": true },
    { "topic": "promotions", "optedIn": false }
  ]
}

POST /api/push/preferences

Set preferences. Accepts a bulk topics map or a single topic + optedIn. Topics are trimmed and bounded to 100 characters; empties are dropped.

Field Type Notes
key string Required. Site key.
endpoint string Required. The subscription endpoint.
topics object Bulk form: { "promotions": false }.
topic + optedIn string + bool Single-topic form.
curl https://api.wrnexus.com/api/push/preferences \
  -H "Content-Type: text/plain;charset=UTF-8" \
  --data '{ "key": "SITE_KEY", "endpoint": "https://…", "topics": { "promotions": false } }'

Response 200 OK: { "ok": true, "preferences": [ … ] }. NO_TOPICS (400) when no valid topic is supplied; UNKNOWN_SUBSCRIPTION (404) when the pair does not resolve.


First-party tracking endpoints

Tracking links carry an HMAC-signed token holding (subscriptionId, campaignId?, url?), so the endpoints need no lookup table and nobody can forge an event for another subscription or turn the click redirect into an open redirector — the destination is fixed inside the signature. Tokens are valid for one year (the life of a notification in the browser). The send pipeline builds these URLs and embeds them in each push payload; the service worker and the click itself call them.

Endpoint Method Behaviour
/api/push/track/click/{token} GET Records a clicked event and 302-redirects to the signed destination (http(s) only).
/api/push/track/delivered/{token} POST Records a delivered receipt (fired by the worker on push).
/api/push/track/open/{token} POST Records an opened/show receipt.
/api/push/track/unsubscribe/{token} POST Deactivates the subscription and records unsubscribed.

Recording is best-effort and idempotent: an event resolves the owning tenant from the subscription’s own row, appends a push_events row, advances the matching per-recipient send row + campaign counters once per transition (guarded on the timestamp column being NULL), and freshens the subscriber’s lastSeenAt. An invalid or expired click token returns 410 Gone with a small HTML page; the other receipts always return { "ok": true }.


Site management

Workspace-scoped. Creating a site provisions its VAPID keypair and site key.

GET /api/push/sites · GET /api/push/sites/{id}

List or read the workspace’s sites. The site object:

{
  "id": "…",
  "name": "Acme Store",
  "domain": "your-domain.example",
  "publicKey": "SITE_KEY",
  "iconUrl": "https://…/icon.png",
  "defaultUrl": "https://your-domain.example/",
  "status": "active",
  "frequencyCap": { "max": 3, "windowHours": 24 },
  "quietHours": { "start": 22, "end": 7, "tz": "Asia/Kolkata" },
  "vapidPublicKey": "BPx…",
  "createdAt": "2026-06-18T14:30:00.000000",
  "updatedAt": "2026-06-18T14:30:00.000000",
  "deletedAt": null
}

POST /api/push/sites

Create a site (role owner/admin). Returns the site object including its freshly minted vapidPublicKey with 201 Created.

Field Type Notes
name string Required. Display name.
domain string Required. The site’s domain.
iconUrl string Default notification icon.
defaultUrl string Default click destination.
vapidSubject string VAPID sub claim (a mailto: or origin).
frequencyCapMax int Max notifications per subscriber per window. 0/null ⇒ unlimited.
frequencyCapWindowHours int Window length (default 24).
quietHoursStart / quietHoursEnd int Local-clock hours [start, end) for the quiet window.
quietHoursTz string IANA timezone for quiet hours (default UTC).

PUT /api/push/sites/{id} · DELETE /api/push/sites/{id}

Update or soft-delete a site (role owner/admin).


Subscribers

GET /api/push/subscribers

List the workspace’s subscriptions.

{
  "subscribers": [
    {
      "id": "…",
      "siteId": "…",
      "endpoint": "https://fcm.googleapis.com/fcm/send/abc…",
      "active": true,
      "userAgent": "Mozilla/5.0 …",
      "metadata": { "url": "https://your-domain.example/", "lang": "en-US" },
      "lastSeenAt": "2026-06-18T14:30:00.000000",
      "unsubscribedAt": null,
      "createdAt": "2026-06-10T09:00:00.000000"
    }
  ],
  "total": 1
}

Campaigns

GET /api/push/campaigns · GET /api/push/campaigns/{id}

List or read campaigns. The campaign object carries live counters:

{
  "id": "…",
  "siteId": "…",
  "segmentId": null,
  "name": "Weekend sale",
  "title": "20% off this weekend",
  "body": "Tap to shop the sale.",
  "iconUrl": "https://…/icon.png",
  "imageUrl": "https://…/hero.jpg",
  "targetUrl": "https://your-domain.example/sale",
  "topic": "promotions",
  "status": "scheduled",
  "stats": {
    "recipients": 1200,
    "sent": 1200,
    "delivered": 1150,
    "clicked": 240,
    "opened": 300,
    "failed": 12,
    "unsubscribed": 8
  },
  "scheduledAt": "2026-06-20T09:00:00.000000",
  "sentAt": null,
  "canceledAt": null,
  "createdAt": "2026-06-18T14:30:00.000000",
  "updatedAt": "2026-06-18T14:30:00.000000",
  "deletedAt": null
}

POST /api/push/campaigns · PUT /api/push/campaigns/{id} · DELETE /api/push/campaigns/{id}

Create, update, or soft-delete (role owner/admin).

Field Type Notes
name string Required. Internal name.
siteId string Required. The owning site.
title / body string Notification title and body.
iconUrl / imageUrl string Notification icon / hero image.
targetUrl string Click destination (wrapped in a signed tracking redirect at send time).
topic string When set, subscribers opted out of this topic are suppressed.
segmentId string Target a saved segment.
audience object Ad-hoc audience predicates (alternative to segmentId).

POST /api/push/campaigns/{id}/send-test

Resolve the test recipients and preview the rendered payload (role owner/admin).

{
  "ok": true,
  "campaignId": "…",
  "attempted": 2,
  "delivered": 2,
  "failed": 0,
  "deactivated": 0,
  "results": [  ],
  "payload": { "title": "…", "body": "…", "clickUrl": "…" }
}

POST /api/push/campaigns/{id}/schedule

Schedule a send (role owner/admin).

Field Type Notes
scheduledAt string Required. RFC3339 timestamp for the send.

Returns the updated campaign object.


Segments

A segment is a reusable, named audience: an AND-list of whitelisted predicates evaluated server-side over the workspace’s subscriptions (and, for behavioural rules, its events).

Endpoint Method Purpose
/api/push/segments GET / POST List / create.
/api/push/segments/{id} GET / PUT / DELETE Read / update / delete.
/api/push/segments/{id}/preview GET Resolve the size of a saved segment.
/api/push/segments/preview POST Resolve the size of an ad-hoc predicate set.

The segment object:

{
  "id": "…",
  "siteId": "…",
  "name": "Engaged mobile users",
  "description": "Clicked in the last 30 days, on mobile",
  "predicates": { "…": "…" },
  "rules": "clicked in last 30d AND device = mobile",
  "size": 842,
  "createdAt": "2026-06-18T14:30:00.000000",
  "updatedAt": "2026-06-18T14:30:00.000000"
}

POST body: name (required), description, siteId, and predicates (a JSON object of whitelisted field/op/value rules — field and operator names are matched against a fixed table and every value is bound, never interpolated).


Analytics

GET /api/push/analytics

Aggregate engagement for the workspace over a rolling window.

Query Type Notes
siteId string Optional. Scope to one site.
days int Window length, clamped to 1365 (default 30).

The response includes a subscriber snapshot (total, active, new-in-window), a delivery funnel (sent ≥ delivered ≥ clicked, plus failed / unsubscribed), first-party event totals, daily opt-ins, browser/device breakdowns (parsed from the stored user-agent), and the top campaigns by reach.


Error contract

Public surfaces return { "ok": false, "code": "…", "message": "…" } with the permissive CORS header. Authenticated surfaces use the standard WRNexus error envelope:

{ "code": "FORBIDDEN", "message": "This action requires the owner or admin role." }
Code HTTP Description
UNAUTHENTICATED 401 Missing or invalid session / API key.
FORBIDDEN 403 Caller lacks the owner/admin role for a mutation.
INVALID_BODY 400 Malformed request body.
INVALID_SUBSCRIPTION 400 Required subscription fields missing.
INVALID_SITE_KEY 400 siteKey missing on /push/config.
UNKNOWN_SITE 404 No site for the supplied key.
SITE_INACTIVE 403 Site not accepting subscriptions.
UNKNOWN_SUBSCRIPTION 404 (siteKey, endpoint) did not resolve.
NO_TOPICS 400 No valid topic in a preferences write.
NOT_FOUND 404 Site / campaign / subscription not found.

See also