# OpenPush overview


OpenPush is a hosted push notification platform, run by the OpenPush team and served from
`https://app.openpush.ai`. You hold the APNs and FCM credentials, and every notification leaves
OpenPush for Apple or Google on those credentials — the same call you would make yourself, with a
console on top. It delivers to iOS, Android and web browsers, and ships native SDKs for Android,
iOS and Unity. Everything you can do in the console is also reachable over the REST API.

> **What "open" means here.** It is a claim about the mechanism, not about the source code. Your
> credentials, your Apple and Google developer accounts, no delivery network of ours in between,
> and no per-message transport fee — because there is no transport of ours to charge for. It is
> not a claim about published code, and it is not an invitation to run the platform yourself:
> **the OpenPush server is not published, and it cannot be run by anybody but the OpenPush team.**
> The client SDKs are published separately. What that buys you in practice is verifiability — the
> sends show up in your own Firebase or Apple console, under your own service account, without
> taking our word for anything.

## When to use this page

Read this first if you are new to OpenPush. It defines the nouns the rest of the documentation
uses — *app*, *user*, *subscription*, *external ID*, *tag*, *segment*, *message* — and points at
the guide for each feature. If you want a working push in the next ten minutes, skip to the
[quickstart](quickstart.md).

## Architecture at a glance

- **The platform** exposes a versioned REST API under `/v1`, plus a browser console. Both are
  served from `https://app.openpush.ai`.
- **Delivery** goes straight to Apple over APNs with a p8 provider token, and to Google over FCM
  HTTP v1 with a service account. There is no third party between OpenPush and the provider.
- **A scheduler tick** runs every few seconds inside the platform. It fires scheduled messages,
  releases sends that were parked by quiet hours or per-user delivery timing, retries transient
  provider failures, and advances journeys.
- **The SDKs are receive-only.** No SDK can send a message. They register the device, report
  identity and tags, and post delivery receipts back to OpenPush.

## Core concepts

### App

An app is the unit of isolation. It owns its own subscriptions, users, segments, templates,
messages, credentials and API keys. An app id is a slug you choose (`acme-app`), and it appears in
every API path: `POST /v1/apps/acme-app/messages`.

Creating an app also creates its three API keys and two default segments. See
[apps, keys and settings](../api-handbook/01-apps-keys-settings.md).

### Subscription

A subscription is one addressable destination: one push token on one device on one platform.
It carries the token, the platform (`ios`, `android` or `web`), the app version, device model,
language, country, timezone, an optional sandbox flag, and a numeric status.

A subscription is targetable when its status is positive and it has not been retired. On iOS the
positive status doubles as Apple's authorization bitmask, so a provisional-authorization device
(bit 64) is subscribed and targetable like any other.

### User

A user is a person, and owns one or more subscriptions — a phone, a tablet, a browser. Tags,
country, language, session counts and aliases live on the user, not on the individual device.
This is why a tag you set on one device targets that person's other devices too.

Without an external ID, each subscription is effectively its own user record.

### External ID

The external ID is your own stable identifier for a person — your database's user id. Setting it
via `login()` on an SDK is what unifies several subscriptions into one user, and what lets you
target someone by identity instead of by token.

Getting the external ID right is the single highest-leverage thing in an OpenPush integration.
Segments, journeys, per-user personalization and cross-device targeting all key off it.

If you turn on identity verification for the app, an external ID claim must be accompanied by an
HMAC your server computes. See [users and subscriptions](users-and-subscriptions.md).

### Aliases

Aliases are extra `{label: id}` identifier pairs on a user, beyond the external ID — for example a
CRM id or a support-desk id. The product cap is 10 pairs per user, and the label `external_id` is
reserved. Aliases can be used as a direct send target.

### Tags

Tags are key/value pairs on a user. **Tag values are strings on every SDK** — there are no
numeric, boolean or date tag types. Segment rules can still compare a tag numerically, and a tag
whose value is a word simply does not match a numeric comparison rather than erroring.

Deleting a tag is expressed as setting it to an empty string. Empty and null tag values are
dropped from the personalization namespace, matching that convention.

Tags do two jobs: they build [segments](segments.md), and they are the variables available to
[Liquid personalization](personalization.md).

### Segment

A segment is a saved audience rule set, evaluated live at send time — never a frozen list. Rules
AND within a group and OR between groups, exactly two levels deep. Counts are exact, not
estimated. See [segments](segments.md).

### Message

A message is one send: content plus an audience plus timing. "Notification" is reserved for the
artifact that appears on the device. A message can carry per-language copy, A/B variants, an
image, a deep link, a custom data payload, up to three action buttons, and delivery options
(TTL, priority, collapse key). See [sending messages](sending-messages.md).

### Template

A template is reusable content — name, title, body, and optionally image, deep link and data.
Referencing it on a send fills in whatever the send body does not override. Liquid inside a
template is validated when you save it, not when you send. See [templates](templates.md).

## The two key kinds

Every app gets two credentials with deliberately different powers. Neither can do the other's job:
the key kind is part of the lookup, so an SDK key can never satisfy an admin route and a REST key
can never satisfy an ingest route.

| Kind | Header | What it does | Where it belongs |
|---|---|---|---|
| REST API key | `X-OP-API-Key` | Full admin of one app: send, read the audience, manage segments, templates, journeys, settings, export | Your backend only. Never in an app binary. |
| SDK key | `X-OP-SDK-Key` | Ingest only: register a device, post receipts, post custom events, register Live Activity tokens | Ships **inside** your mobile app binary, by design |
| Legacy key | `X-OP-API-Key` | Treated exactly like a REST key; exists for migration | Your backend only |

A REST key is scoped to a single app and cannot read or write another. It is not role-scoped —
console roles (`viewer` / `manager` / `admin`) apply to browser sessions only, never to API keys.

OpenPush also carries two platform-level keys, held by the OpenPush team, that match on every
app. They are part of how the platform is administered, not something you configure. See
[security and limits](security-and-limits.md).

## The three platforms

| Platform | Transport | SDK | Notes |
|---|---|---|---|
| `ios` | APNs directly, p8 provider token | `openpush-ios` (iOS 16+) | Per-device sandbox flag lets TestFlight and App Store builds coexist in one app |
| `android` | FCM HTTP v1, service account | `openpush-android` (API 26+) | Data-only messages; the SDK draws the notification |
| `web` | **also FCM** | none — use your own Firebase JS integration | There is no OpenPush web push JavaScript SDK, and no direct VAPID sender |

Unity is supported as a fourth SDK surface that targets iOS and Android.

Be honest with yourself about web: OpenPush stores and publishes your web push credentials, and it
delivers to browsers through FCM, but the browser-side subscribe flow is Firebase's, written by
you. See [web push](web-push.md).

`email`, `sms`, `huawei`, `amazon` and similar strings are recognised as skipped channels by the
CSV importer. They are not deliverable platforms here.

## The delivery ladder

OpenPush deliberately refuses to collapse four different facts into the word "delivered":

1. **Provider Accepted** — Apple or Google took the message off your hands.
2. **Device Received** — the SDK on the device actually got the data.
3. **Confirmed Receipt** — the notification was displayed.
4. **Clicked** — someone tapped it.

Stages 2 through 4 arrive from the device via `POST /v1/ingest`, so they only appear if your app
integrates the SDK's receipt path. Message reports show all four, plus `Failed`, `Capped`
(suppressed by a frequency cap) and `Held` (waiting for quiet hours to open).

## Feature map

| I want to… | Guide | API reference |
|---|---|---|
| Get a first push out | [Quickstart](quickstart.md) | [Messages](../api-handbook/02-messages.md) |
| Install an SDK | [Android](sdk-android.md) · [iOS](sdk-ios.md) · [Unity](sdk-unity.md) | [Subscriptions and users](../api-handbook/03-subscriptions-users.md) |
| Upload push credentials | [APNs](platform-setup-apns.md) · [FCM](platform-setup-fcm.md) · [Web](web-push.md) | [Apps, keys, settings](../api-handbook/01-apps-keys-settings.md) |
| Compose and send | [Sending messages](sending-messages.md) | [Messages](../api-handbook/02-messages.md) |
| Show a message inside the app | [In-app messages](in-app-messages.md) | [In-app messages](../api-handbook/10-in-app-messages.md) |
| Build an audience | [Segments](segments.md) | [Segments](../api-handbook/04-segments.md) |
| Personalize copy | [Personalization](personalization.md) | [Templates and dynamic content](../api-handbook/05-templates-dynamic-content.md) |
| Reuse content | [Templates](templates.md) | [Templates and dynamic content](../api-handbook/05-templates-dynamic-content.md) |
| Test two versions | [A/B testing](ab-testing.md) | [Messages](../api-handbook/02-messages.md) |
| Send at each person's best hour | [Best-hour delivery](best-hour-delivery.md) | [Messages](../api-handbook/02-messages.md) |
| Understand identity | [Users and subscriptions](users-and-subscriptions.md) | [Subscriptions and users](../api-handbook/03-subscriptions-users.md) |
| Track behaviour | [Events](events.md) | [Events and ingest](../api-handbook/06-events-ingest.md) |
| Automate multi-step flows | [Journeys](journeys.md) | [Journeys](../api-handbook/07-journeys.md) |
| Ship iOS Live Activities | [Live Activities](live-activities.md) | [Live Activities](../api-handbook/08-live-activities.md) |
| Move data in or out | [Import and export](import-export.md) | [Import and export](../api-handbook/09-import-export.md) |
| Migrate off OneSignal | [Migrate from OneSignal](migrate-from-onesignal.md) | [API overview](../api-handbook/00-overview.md) |
| Handle keys safely | [Security and limits](security-and-limits.md) | [API overview](../api-handbook/00-overview.md) |

## Limits and honest notes

- **No notification inbox.** No inbox API exists on any of the three SDKs, and no compat module
  provides one. In-app messages, by contrast, **do** ship: the server carries an in-app message
  engine and nine routes, all three SDKs carry a real presenter and the impression/click receipt
  path, and the OneSignal v5 `InAppMessages` spellings resolve onto that engine rather than
  refusing at compile time. See [in-app messages](in-app-messages.md) and the
  [in-app messages API](../api-handbook/10-in-app-messages.md).
- **No outbound webhooks or event streams.** Nothing in the server POSTs to a customer URL.
- **No email or SMS channel.** Push only.
- **No conversion or attribution metrics.** The four-stage ladder above is the whole measurement
  surface for a message.
- **No idempotency on message creation.** Retrying a create-message call sends the campaign a
  second time. See [security and limits](security-and-limits.md).
- **Custom events do not reach segments.** They exist to trigger journeys. Behavioural targeting
  is not expressible as a segment rule.
- **There is no web push JavaScript SDK.** Web works through your own Firebase integration.

## FAQ

**Is a user the same thing as a subscription?**
No. A subscription is one device or browser; a user is the person who owns them. Set an external
ID to link several subscriptions into one user.

**Can I put the REST API key in my mobile app?**
No. It is full admin of the app. Ship the SDK key — it is ingest-only and designed to be public.

**What happens if I never set an external ID?**
Everything still works, but each subscription is its own user, so a tag set on the phone will not
be seen when targeting the tablet, and cross-device deduplication is impossible.

**Does OpenPush report "delivered"?**
It reports Provider Accepted, and separately Device Received and Confirmed Receipt when the SDK
posts them back. It never calls provider acceptance "delivered", because that would be untrue.

**Can one API key manage several apps?**
Not one of yours. Only the platform-level key held by the OpenPush team crosses apps. Per-app REST
keys are scoped to exactly one app.

## Related

- [Quickstart](quickstart.md)
- [Sending messages](sending-messages.md)
- [Users and subscriptions](users-and-subscriptions.md)
- [Security and limits](security-and-limits.md)
- [API overview](../api-handbook/00-overview.md)
