Concepts
Everything in Open Notification is one of six things. Once these are clear, the dashboard, the SDKs and the REST API all read the same way.
App
An app is a tenant. It has a name, a slug, at most one set of credentials per platform (APNs, FCM, Web Push), its own API keys, delivery rules and webhooks. Every user, subscription, campaign and event belongs to exactly one app, and every query the system runs is scoped to one — there is no way for one app's key to read another app's rows.
Typical setups:
- one app per product (an iOS + Android + web product is one app with three credentials),
- a second app for staging, so test devices never get production campaigns.
User
A user is a person — the owner of identity and targeting properties:
| Property | Meaning |
|---|---|
externalId | Your user id ("user_123"). null while the user is anonymous. |
tags | Free-form key → string/number/boolean map. What segments filter on and what placeholders read. |
language | Picks which locale of a campaign's content the user receives. |
timezone | IANA zone. Drives quiet hours and "send at the user's 19:30". |
country | ISO 3166-1 alpha-2. |
Every device has a user, even before login. A device that subscribes without an externalId gets an anonymous user row. When your app later calls login("user_123"), that anonymous user is renamed — or, if user_123 already exists, the device moves under it. The rules are in Identity.
Subscription
A subscription is one device (or one browser profile): the row that holds the APNs token, the FCM registration token, or the web push endpoint plus its encryption keys. It also carries device facts (platform, appVersion, osVersion, deviceModel, browser, standalone) and a couple of switches:
optedIn—falseafter the user unsubscribes or after 90 days of inactivity. Not targeted.invalidatedAt— stamped when Apple/Google/the push service said the token is dead (410,Unregistered,UNREGISTERED). Not targeted.
A user with a phone and a laptop has two subscriptions. Tags set on the user reach both — the user's properties are copied onto each of their subscription rows so segments, fan-out and personalisation stay one-collection queries.
Campaign
A campaign is one send: content (per locale), a target, a schedule, delivery options, and running counters.
| Kind | How it starts | What it looks like |
|---|---|---|
| Campaign | Dashboard wizard or POST /v1/campaigns + /send | Named, segment-targeted, scheduled, reported on. |
| Transactional | POST /v1/notifications from your backend | Addressed to specific users/devices, sent immediately. Still gets a campaign row (transactional: true), so it still has a report. |
Statuses: draft → scheduled → sending → sent, with cancelled (before fan-out) and failed on the side.
Event
An event is one thing that happened to one push:
| Type | Written by | Meaning |
|---|---|---|
sent | worker | The transport accepted it. |
failed | worker | The transport rejected it; meta.code says why. |
skipped | worker | Held back by a delivery rule; meta.reason is quiet_hours or frequency_cap. |
delivered | SDK | The device drew the notification. |
opened | SDK | The user tapped it. |
clicked | SDK | The user tapped an action button. |
dismissed | SDK | The user swiped it away (Android and web only). |
Events live in a time-series collection with a 180-day TTL and roll up into the campaign's counters. Rates are computed with delivered as the denominator — see Analytics.
API keys
| Prefix | Name | Where it lives | Can |
|---|---|---|---|
| sk_live_… | secret key | your backend, CI | everything its scopes allow: send, manage users, read reports |
| pk_live_… | public key | app binary, web page | register a device; update that device's user |
The app id is embedded in the key, so verification is one indexed lookup plus a bcrypt compare. Keys have scopes (notifications:send, campaigns:write, …) and are revocable one at a time from Settings. See API overview for headers and errors.
The dashboard's own login
Dashboard members (owner/admin/editor/viewer) are separate from API keys. They log in with e-mail + password, and the dashboard reaches the API over an internal /v1/admin surface with a shared ADMIN_API_TOKEN that never leaves the server. See Members.