Apps, keys and settings
An app is the top-level container in OpenPush. Every subscription, message, segment, template and journey belongs to exactly one app, and every per-app API key is scoped to one. This page covers creating and listing apps, reading and rotating their keys, and reading and updating their settings.
Examples use https://app.openpush.ai, the OpenPush API base URL.
Apps
App ids are slugs you choose: lowercase, no surrounding whitespace. They appear in every
API path, so pick something stable (runner-club, not runner-club-v2-final).
The two routes in this section are the only ones on /v1 that are not scoped to a
single app, and both therefore require the platform-level key (OP_API_KEY), which is
held by the OpenPush team and is not issued to customers. A per-app REST key is refused
with 401 "bad X-OP-API-Key (org routes need the global key)". There is deliberately no
workspace-scoped variant: a per-app key knows one app, and listing or creating a
workspace's apps is a console operation that resolves the workspace from the signed-in
session. In practice: create and list your apps in the console. They are documented
here so the 401 is not a mystery.
GET /v1/apps
Lists every app on the platform, across every workspace.
Auth: X-OP-API-Key — platform-level key only; not reachable with a customer key.
Query parameters: none.
Code
Code
| Field | Type | Description |
|---|---|---|
apps[] | array | One object per app: the stored app row plus the computed fields below |
apps[].id | string | The app slug used in every path |
apps[].name | string | Display name |
apps[].subscribed | int | Count of currently sendable subscriptions (status above zero and not retired) |
apps[].ctr | string | Lifetime click-through rate, preformatted |
apps[].mau | int | Monthly active users, rounded |
apps[].mau_exact | int | The unrounded figure, so you can reconcile it against your own query |
apps[].mau_window_days | int | Length of the counting window |
apps[].mau_counted_at | number | Epoch seconds when the figure was computed |
mau_definition | string | Plain-language statement of exactly what MAU counts |
Errors
| Status | Body | Cause |
|---|---|---|
401 | bad X-OP-API-Key (org routes need the global key) | A per-app REST key was used, or OP_GLOBAL_KEYS=off |
POST /v1/apps
Creates an app.
Auth: X-OP-API-Key — platform-level key only; not reachable with a customer key. Use the console.
Body
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes (or slug) | The app slug. Lowercased and trimmed before use |
slug | string | yes (or id) | Accepted as an alias for id |
name | string | no | Display name. Defaults to the slug with hyphens replaced by spaces and title-cased |
Code
Code
Creating an app also creates, in the same call:
- a settings row carrying the defaults documented under Settings
- the app's default segments, including
Total Subscriptions - initial API keys — record secret values when created or rotated; later list reads contain metadata only
Where the app lands. A REST call carries no console session and therefore no workspace, so a new app is created in the default workspace. Creating an app inside a specific workspace is a console action, where the workspace comes from your membership.
Re-creating an existing app is not an error. If the slug already exists, the existing
app row is returned unchanged — no 409, and nothing about the app is overwritten. Treat
this route as "ensure this app exists".
Errors
| Status | Body | Cause |
|---|---|---|
400 | id (slug) required | Neither id nor slug was supplied, or both were blank |
401 | bad X-OP-API-Key (org routes need the global key) | A per-app REST key was used, or OP_GLOBAL_KEYS=off |
Keys
An app can hold multiple REST and SDK keys. A REST key may have full or
read scope; only full-scope keys can change resources or list key metadata.
SDK keys are ingest-only. Key secrets appear once in create and rotation
responses.
GET /v1/apps/{app_id}/keys
Lists key metadata, including kind, scope, enabled state, expiry and allowed IP
ranges. Secret values are omitted by default. Through 2026-10-23 UTC,
?include_values=true is available for clients transitioning from older key
listing behavior. On 2026-10-24 UTC it returns 410.
Code
Code
Create and update keys
POST /v1/apps/{app_id}/keys accepts kind: "rest" | "sdk", an optional
name, a REST scope: "full" | "read", and up to 16 allowed_ips CIDR
ranges. It returns the secret once. PATCH /keys/{key_id} changes name,
scope, enabled, or allowed_ips; DELETE /keys/{key_id} revokes it.
The last active full administrative key cannot be disabled or deleted.
IP restrictions use the request peer address. A forwarded address is honored only when the immediate peer is in the server's trusted proxy ranges.
POST /v1/apps/{app_id}/keys/{key_id}/rotate
The path accepts a key ID, or a kind for the most recently created active key
of that kind. Rotation returns a new secret once. The old REST secret works for
24 hours; the old SDK secret works for seven days. The response includes
old_key_expires_at.
Code
Code
App metadata
GET /v1/apps/{app_id} returns the app's ID, name, icon URL, tile color,
store URL and creation time. PATCH /v1/apps/{app_id} accepts name,
icon_url, tile_color or play_store_url. It does not change the app
slug or platform credentials.
Settings
App settings hold the delivery guards (quiet hours and frequency capping), the three optional data-collection switches, and the identity-verification flag. Reading them also reports per-app platform readiness, which is the number you want when you are debugging "why did nothing send".
GET /v1/apps/{app_id}/settings
Auth: X-OP-API-Key (this app's REST key, or the global key).
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
app_id | string | yes | The app slug |
Code
Code
Delivery guards
| Field | Type | Default | Description |
|---|---|---|---|
quiet_enabled | int (0/1) | 0 — off | Whether quiet hours apply to this app |
quiet_start | string HH:MM | "08:00" | Start of the allowed window, in each device's local time |
quiet_end | string HH:MM | "21:00" | End of the allowed window |
freq_cap | int | 10 | Maximum provider-accepted sends per device per window. On by default. 0 disables the cap |
freq_window_h | int | 24 | Length of the cap window, in hours |
The quiet window stores the interval during which sending is allowed, not the interval
during which it is muted. quiet_start == quiet_end means always allowed, and the window
may wrap past midnight. A device inside quiet hours is held, not dropped — it is
released when its window opens. Full semantics, including how they compose with per-user
delivery timing: Messages.
Data-collection switches
| Field | Type | Default | Description |
|---|---|---|---|
collect_ad_id | int (0/1) | 0 | Whether advertising identifiers are stored |
collect_location | int (0/1) | 0 | Whether latitude/longitude are stored |
collect_email | int (0/1) | 0 | Whether email addresses are stored |
collection | object | — | The same three switches as booleans, keyed ad_id, location, email |
collected | object | — | How many rows currently hold a value for each category |
collected answers the question collection cannot: "off" and "off, and there are 12,400
of them already in the table" are different facts.
Other fields
| Field | Type | Description |
|---|---|---|
identity_verification | int (0/1) | Whether identity verification is required on registration. Off by default. Changing this is a console action — the PATCH route below does not accept it |
android_channels | string | null | JSON list of registered Android notification channels — a vocabulary registry the composer and op_android_channel draw from. Console-managed |
default_tz | string | null | Present in the stored row and in exports. It is not currently consulted by the send pipeline; a device that reports no usable timezone is treated as UTC |
ip_storage | string | full, truncated or off. Server-wide, set by OP_IP_STORAGE, not per app |
platforms | object | Per-app delivery readiness — see below |
platforms
| Field | Type | Description |
|---|---|---|
fcm | bool | FCM can deliver for this app |
apns | bool | APNs can deliver for this app. This is a real check: the .p8 is parsed and a provider token is actually signed |
web | bool | A web push credential is stored for this app |
dry_run | bool | The server is running without real provider delivery |
fcm_source / apns_source | string | Where the credential came from — the app's own record, an environment fallback, none, or unreadable |
fcm_error | string | Present only when FCM is not ready and the server is not in dry-run: why |
apns_error | string | Present only when APNs is not ready: why. Reported even under dry-run, because "no iOS credential at all" is a normal state that deserves a reason |
This is the per-app answer. GET /healthz gives the platform-wide one — see
API overview.
Errors
| Status | Body | Cause |
|---|---|---|
401 | bad X-OP-API-Key | Missing, wrong, disabled, or wrong-app key |
PATCH /v1/apps/{app_id}/settings
Updates the delivery guards and the collection switches. Send only the fields you want to change.
Auth: X-OP-API-Key (this app's REST key, or the global key).
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
app_id | string | yes | The app slug |
Body
| Parameter | Type | Required | Description |
|---|---|---|---|
quiet_enabled | int (0/1) | no | Turn quiet hours on or off |
quiet_start | string HH:MM | no | Start of the allowed window |
quiet_end | string HH:MM | no | End of the allowed window |
freq_cap | int | no | Provider-accepted sends allowed per device per window. 0 disables |
freq_window_h | int | no | Cap window length in hours |
collect_ad_id | bool | no | Advertising-identifier collection (flat spelling) |
collect_location | bool | no | Location collection (flat spelling) |
collect_email | bool | no | Email collection (flat spelling) |
collection | object | no | The same three switches nested: {"ad_id": true, "location": false, "email": false} |
Collection switches are accepted either flat or nested, because a console form posts one shape and an integration usually posts back the object it just read. Only the switches you actually name are touched; the others are left alone.
Code
Code
Turning a collection switch off erases the data already stored for that category.
This is not a "stop writing from now on" toggle — it deletes. The purged object in the
response reports how many rows were cleared per category; it is empty when nothing went
from on to off in this request. There is no undo.
The response is the full settings object, in the same shape as GET, plus purged.
Errors
| Status | Body | Cause |
|---|---|---|
400 | nothing to update | The body named none of the accepted fields |
401 | bad X-OP-API-Key | Missing, wrong, disabled, or wrong-app key |
Limits
GET /v1/appsandPOST /v1/appsaccept only the platform-level key, which customers do not hold; when the platform runs withOP_GLOBAL_KEYS=offthey are unreachable over the API at all. Either way, listing and creating apps is a console operation for you.- There is no
DELETE /v1/apps/{app_id}. Deleting an app is not an API operation. PATCH /settingsaccepts the delivery guards and the collection switches only.identity_verification,android_channelsanddefault_tzare read-only over the API and are managed in the console.- Individual key routes can create, disable, delete and rotate keys.
ip_storageis server-wide configuration, reported per app for convenience. You cannot set it per app.- Rotation has a 24-hour REST or seven-day SDK overlap window.
FAQ
How do I create an app inside a specific workspace?
Use the console. A REST call has no session, so POST /v1/apps always lands the app in
the default workspace.
Is POST /v1/apps safe to run repeatedly in a provisioning script?
Yes. An existing slug returns the existing app row unchanged, so the call is effectively
"ensure this app exists". It will not reset keys or settings.
I rotated the SDK key. When should devices adopt the new value? Shipped binaries can keep ingesting with the previous value for seven days. Ship a build with the new value during that overlap; devices still receive pushes to stored tokens.
Why is freq_cap on by default but quiet_enabled off?
A cap protects users from an accident with no configuration; the value is a sensible
ceiling rather than a policy. Quiet hours are a policy — the right window depends on your
audience — so they stay off until you set them.
How do I turn identity verification on? In the console. The API reports the flag but does not accept a change to it, deliberately: flipping it on for a fleet that has not yet shipped the hashing code silently downgrades every subsequent registration.