# Backend API updates

# Audience, messages, events, and operations

This guide covers the current `/v1` REST contracts for backend integrations. All app routes use `https://app.openpush.ai/v1/apps/{app_id}`. Send `X-OP-API-Key: <rest-key>` for administrative calls. An app's SDK key is limited to device and event ingestion. Keep REST keys on a trusted server.

The generated [API reference](/docs/api) has the route inventory and request schemas. Examples below use native OpenPush resource names and IDs.

## Create and maintain users

`POST /users` creates an account profile and, optionally, its first push subscriptions in one transaction. A subscription requires an explicit `consent` boolean. An alias value or `external_id` already held by another user returns `409`; a failed subscription leaves no new user behind.

```json
{
  "external_id": "player-42",
  "aliases": {"account": "42"},
  "tags": {"plan": "pro"},
  "language": "en",
  "subscriptions": [
    {"token": "device-token", "platform": "android", "consent": true}
  ]
}
```

Use `GET /users/{user_id}`, `GET /users/by-external-id/{external_id}`, or `GET /users/by-alias/{label}/{value}` to read a profile. `PATCH /users/{user_id}` changes `country`, `language`, `timezone`, or `email`; pass `null` to clear one of those properties. Email storage requires the app's email collection switch. Tag and alias writes continue to use their dedicated profile routes.

`DELETE /users/{user_id}` erases the profile and associated identity data and retires its subscriptions so future sends cannot target them. The receipt names retained deidentified delivery history and audit records. Existing aggregate counts are not reconstructed.

## Maintain push subscriptions

Use `POST /users/{user_id}/subscriptions` with the subscription body above to add a push device. `PATCH /subscriptions/{sub_id}` accepts `consent`, `new_token`, device metadata, and iOS `sandbox`. A consent value of `false` opts out without deleting delivery history. An invalid provider token needs a fresh device registration before opting back in.

`PATCH /subscriptions/by-token` accepts the current `token` in the JSON body along with update fields. Keep tokens out of URL paths and ordinary logs. `POST /subscriptions/{sub_id}/transfer` accepts `{"user_id":"usr_..."}` and preserves the subscription ID. The source and destination must belong to the same app. `DELETE /subscriptions/{sub_id}` retires the device; subsequent audience resolution excludes it.

## Target an audience directly

`POST /audience-preview` and `POST /messages` share the same selectors. Generic inline `filters` narrow the audience without saving a segment:

```json
{
  "title": "Pro update",
  "body": "New features are ready.",
  "filters": [
    {"field": "tag", "key": "plan", "op": "is", "value": "pro"},
    {"field": "country", "op": "is", "value": "US"},
    {"operator": "or"},
    {"field": "app_version", "op": "in", "value": ["2.4", "2.5"]}
  ]
}
```

Predicates reuse the saved-segment field/operator vocabulary, including tag, profile, device, session, test-user, and location fields. They combine with AND by default; an `{"operator":"or"}` entry starts another OR group, while predicates inside a group remain ANDed. Tag rules require `key`; bounded `in` and `not_in` lists work across supported tag and profile fields. Missing tags match `not_in` and `not_exists`, but not `in` or `exists`. Filters intersect with included segments, excluded segments, and a `target` selector.

For a known set of recipients, use one identity selector under `target`: `external_ids`, `subscription_ids`, or `aliases: {"label":"account","ids":["42","43"]}`. Each list accepts 1 to 100 IDs and duplicate values are counted once. Single `external_id`, `subscription_id`, and `alias` selectors remain supported. `platform` or `platforms` may narrow the result. Unknown or misspelled audience fields return `400`; they cannot turn a targeted request into an all-subscriber send. Preview reports matching subscriptions and invalid requested IDs separately.

For saved segments, `GET /segments/{seg_id}` returns one editable segment. Segment rules support bounded `in` and `not_in` lists for tags, language, country, and app version. Segment session metrics refer to users, rather than individual devices.

## Message content and delivery

`platform_options` accepts the supported iOS and Android presentation fields and validates them before a message is created. For example:

```json
{
  "title": "Match ready",
  "body": "Tap to join.",
  "target": {"external_ids": ["player-42", "player-43"]},
  "platform_options": {
    "ios": {"subtitle": "Your team is waiting", "badge": 1},
    "android": {"channel": "matches", "group": "team"}
  }
}
```

Platform display can depend on device settings and the installed client version. Provider acceptance is recorded separately from device receipts. The API rejects unsupported presentation fields.

`POST /templates` and `PATCH /templates/{tpl_id}` accept `languages`, `default_language`, and `platform_options` alongside the basic title, body, image, link, and custom data. Each localized entry requires a title and body. `GET /templates/{tpl_id}` returns the saved content and version. On send, explicit message content and platform fields override template defaults. A created message stores its effective content, so later template edits do not alter a scheduled message.

`delivery_policy` can tighten the app's `frequency_cap` or `throttle_per_minute` for one message. Values must be positive integers within the app's limits. It cannot bypass the app's safety settings; message throttling cannot be combined with best-hour delivery. Preview and dispatch apply the stored policy.

`GET /messages` supports a stable cursor and exact `status` and `name` filters. `DELETE /messages/{message_id}` cancels a message only while its status is `Scheduled`. If dispatch has begun, the call returns `409`; it cannot recall a push already sent.

## Retry custom events

`POST /events` accepts up to 50 events per batch with an SDK key or a writable REST key. Give an event an `idempotency_key` when a caller may retry. Reusing a key with identical content returns the stored event with `duplicate: true` and does not trigger a second journey entry. Reusing it with different content is rejected for that item. The response retains `accepted` as the count of accepted items and adds `inserted` and `duplicates`; `dropped` and `dropped_reasons` report rejected items. Rate accounting is shared by server instances.

## Export message activity

`POST /messages/{message_id}/activity-exports` creates an immutable snapshot. Supply `types` from `accepted`, `received`, `confirmed`, `clicked`, and `failed`. `GET /activity-exports/{export_id}` returns counts, expiry, and a download path. The download is newline-delimited JSON with a schema version, message and subscription IDs, timestamps, and an evidence source. Provider acceptance does not mean the device displayed the message.

Snapshots expire after 24 hours. Each app may keep three active snapshots; a message may have at most 50,000 delivery rows and the stored file at most 20 MB. A missing cross-app export returns `404`, and an expired export returns `410`.

## App metadata and keys

`GET /v1/apps/{app_id}` reads app metadata. `PATCH /v1/apps/{app_id}` changes `name`, `icon_url`, `tile_color`, or `play_store_url`.

`POST /keys` creates a REST or SDK key and returns its secret once. `GET /keys` lists metadata without secret values. `PATCH /keys/{key_id}` changes the name, REST scope (`full` or `read`), enabled state, or up to 16 CIDR `allowed_ips` ranges. `DELETE /keys/{key_id}` revokes a key. The last active full administrative key cannot be disabled or deleted.

`POST /keys/{key_id}/rotate` returns the new secret once and leaves the old REST secret valid for 24 hours or the old SDK secret valid for seven days. The response reports the old secret's expiry. IP restrictions are checked against the request peer address, or a forwarded address only when the server trusts that proxy network.

### Compatibility transition

Existing send and device registration requests keep their native fields. The event batch's `accepted` count retains its previous meaning; `inserted` and `duplicates` are additional fields. Calls that used to recover secrets from `GET /keys` must read the secret at creation or rotation. Until **2026-10-24 UTC**, `GET /keys?include_values=true` provides explicit legacy readback; after that date it returns `410`.

Targeting is intentionally strict: previously ignored audience misspellings now return `400` to prevent an accidental broad send. Test request bodies against `POST /audience-preview` before scheduling campaigns.
