Push (REST)
Manage push campaigns, credentials, devices, and webhooks. Except for the transactional endpoint, the routes on this page are dashboard APIs authenticated with a WorkOS access token and the documented workspace role. Transactional sends use a scoped backend-only server key.
Campaigns
GET /v1/apps/:appId/campaigns?type=push
POST /v1/apps/:appId/campaigns
PATCH /v1/apps/:appId/campaigns/:campaignId
DELETE /v1/apps/:appId/campaigns/:campaignIdCreate
{
"name": "Continue watching",
"mode": "transactional",
"status": "draft",
"variants": [
{
"title": "{{p.first_name}}, continue {{p.show_title}}",
"body": "Your next scene is waiting.",
"weight": 1,
"openAction": {
"action": "json",
"actionJson": {
"type": "open_show",
"show_id": "{{p.show_id}}",
"position_seconds": "{{p.position_seconds}}"
}
},
"personalization": {
"version": 1,
"missingData": "skip",
"sources": [
{ "id": "profile", "label": "User profile", "kind": "user_property" },
{ "id": "movie", "label": "This send's movie", "kind": "send_data" }
],
"bindings": [
{
"id": "first_name", "label": "First name", "sourceId": "profile",
"key": "first_name", "type": "string", "fallback": "there"
},
{
"id": "show_title", "label": "Show title", "sourceId": "movie",
"key": "show_title", "type": "string"
},
{
"id": "show_id", "label": "Show ID", "sourceId": "movie",
"key": "show_id", "type": "string"
},
{
"id": "position_seconds", "label": "Position", "sourceId": "movie",
"key": "position_seconds", "type": "number"
}
]
}
}
]
}Every {{p.binding_id}} token references a binding in that variant's
personalization specification. The example uses a saved profile name and a
backend-selected movie supplied with each transactional send. A whole-value JSON
token preserves its declared type: show ID stays a string and position becomes
a number. Configure the host's JSON action handler to open the movie.
Other source kinds include trigger_event and latest_event; latest activity
also requires eventName and lookbackDays (1–90), with optional typed filters.
Reuse one source ID for related movie fields. A missing required binding skips
delivery; explicit fallbacks handle missing values, while type mismatches remain
errors. See Personalize messages.
Send a transactional campaign
POST /v1/apps/:appId/push/transactional
Authorization: Bearer rtk_your_server_key
Content-Type: application/json
{
"campaignId": "1ac74fd0-a8ad-4e0b-b244-4493cd56567c",
"externalId": "user_42",
"data": {
"show_id": "00123",
"show_title": "Midnight Orbit",
"position_seconds": 1234
},
"idempotencyKey": "resume-user-42-session-1042"
}Use a backend-only server key with the push.transactional scope. Exactly one
of externalId or anonymousId is required. Reusing an idempotency key for
the same app returns the original delivery instead of enqueueing a duplicate.
The campaign must belong to the app, use transactional mode, and be active.
The optional data object supplies flat string, finite number, boolean or null
values to Transactional send data (send_data) fields. Sensitive values still
follow the app's privacy settings. Supply values for every required send-data
binding; selecting that source does not populate it from event history. Use a new
idempotency key for a new logical send. The queued response does not confirm
APNs/FCM receipt. Recipient snapshots keep resolved content stable across retries.
To test a draft from the dashboard API, call
POST /v1/apps/:appId/campaigns/:campaignId/test-send with an anonymousId.
That test endpoint does not accept transactional data; use a profile/latest-event
test campaign, or preview supplied example data through the personalization API.
Preview personalized content
POST /v1/apps/:appId/personalization/preview uses dashboard authentication and
requires viewer access to the app. Send surface: "push", the variant as content,
and one subject reference (anonymousId, externalId or subjectId). Optionally
include triggerEventId for that user's event, or data for a transactional
preview. Preview resolves content without sending or consuming frequency limits;
the actual send can resolve newer values. The dashboard's Preview as user
supports recipient selection and a triggering event ID.
Credentials
GET /v1/apps/:appId/push/credentials
POST /v1/apps/:appId/push/credentials # upload .p8 or FCM JSON
DELETE /v1/apps/:appId/push/credentials/:credIdApple (.p8)
{
"platform": "ios",
"p8": "-----BEGIN PRIVATE KEY-----\n…",
"keyId": "ABCD1234EF",
"teamId": "0123456789",
"bundleId": "com.acme.app",
"environment": "production"
}Android (FCM)
{
"platform": "android",
"serviceAccountJson": "{ ...the complete service-account JSON... }"
}Devices
GET /v1/apps/:appId/users/:anonymousId/push
POST /v1/apps/:appId/users/:anonymousId/push/test-sendThe first route returns consent and registered-device metadata for the full
linked subject. The second sends an ad-hoc test notification and accepts
title, body, and optional imageUrl and deepLink fields.
Channels
GET /v1/apps/:appId/push/channels
PUT /v1/apps/:appId/push/channels
DELETE /v1/apps/:appId/push/channels/:channelIdThe upsert body uses channelId, displayName, numeric importance, and the
optional defaultSound, defaultVibrate, defaultBadge, description, and
category settings.
Webhooks
GET /v1/apps/:appId/push/webhooks
POST /v1/apps/:appId/push/webhooks { "url": "https://hooks.acme.com/push", "eventTypes": ["delivered", "failed"] }
PATCH /v1/apps/:appId/push/webhooks/:webhookId
DELETE /v1/apps/:appId/push/webhooks/:webhookId
POST /v1/apps/:appId/push/webhooks/:webhookId/rotate
POST /v1/apps/:appId/push/webhooks/:webhookId/testCreation returns the HMAC secret once. Store it immediately and verify every delivery signature; rotating returns a replacement secret once.
See API → Webhooks for the outbound payload schema.