Notifications
This page describes how to send players in-game (inbox) and push notifications: provider, template and rule setup in the panel; the game’s device registration, inbox reading and direct sending from the server. Read it if you want to use notification infrastructure without writing it yourself.
What it does
The notifications module (notifications) first writes a notification to a queue, then a background worker sends it to the provider (FCM, APNs or OneSignal). The API has no synchronous push send: when your request is written to the queue you get 200; delivery happens afterwards.
The notification text comes from a template. A template is written in the panel; the game sends only the template key and the fill data. You can produce a notification in two ways:
- Sending from the server: Your game server calls
POST /v1/notifications. - Rule: A rule you set up in the panel produces a notification automatically when a game event happens (for example a level up, quest completion or store purchase).
If the module is disabled, the endpoints return 404 with {"code":"module_disabled","module":"notifications"}. Enable the module on the Game settings › Modules page.
Where it lives in the panel
| What you want to do | Panel page |
|---|---|
| Enter push provider (FCM, APNs, OneSignal) credentials | Notifications › Providers |
| Write a template (title and body per language, placeholders) | Notifications › Templates |
| Set up an event-driven rule, preview the rule live | Notifications › Rules |
| Send a test notification to a player, see the send history and devices | Notifications › History, Devices |
Provider credentials are entered in the panel only by an authorized role and are stored sealed on the server; the panel shows only the fingerprint.
Note
Panel help pages are not translated yet; the panel links above point to the Turkish source, and the panel tabs are shown in Turkish.
Data model (in plain words)
- Provider:
fcm,apns,one_signal. Each is switched on and off separately per environment. - Device: A player’s registered token (
token) at a provider and its platform (ios,android,web). The same (environment, provider, token) can be registered again; the response says which branch handled it (created,revived,refreshed). - Template: A key, a title and body per language,
{{placeholder}}fields. If the requested language is missing, English (en) is used. Each notification is bound to the template version at the moment it was sent. - Channel:
in_game(inbox only),push(push only),both. - Notification (inbox row):
id,template_key,title,body,data,channel,status,created_at,sent_at,read_at. Status:queued,sent,delivered,failed,read. - Dedup key (
dedup_key): The same (environment, player, key) is not written a second time.
Calls your game makes
There are two credential types: a player session (cookie or session token + x-crx-env: <environment uuid>) and a server key (Authorization: Bearer crx_live_<prefix>_<secret>).
| Method and path | Credential | What it does |
|---|---|---|
POST /v1/notifications | server key | Writes a notification for a player to the queue |
GET /v1/notifications | player session | Inbox (status, cursor, limit) |
POST /v1/notifications/{id}/read | player session | Marks as read |
POST /v1/devices | player session | Registers a push device |
DELETE /v1/devices/{token}?provider= | player session | Revokes a device registration (204) |
GET /v1/push/config | no credential (x-crx-env required) | Settings exposed to the client for web push |
GET /v1/ws | player session | Live notification push socket (server to player only; the environment is given with the x-crx-env header, so it cannot be connected with the browser WebSocket API; frames arrive at most once, the durable surface is the inbox) |
On the player endpoints the x-csrf-token and Origin checks are not applied; the session cookie or session token is enough.
The player side is ready in the TypeScript SDK: under player.notifications there are getPushConfig, registerDevice, unregisterDevice, enableWebPush, disableWebPush, inbox and markRead. There is no typed client for sending from the server; use raw REST. See TypeScript SDK.
Sending a notification from the server
POST /v1/notifications
Authorization: Bearer crx_live_<prefix>_<secret>
Content-Type: application/json
{
"player_id": "0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f",
"template_key": "daily_reward_ready",
"locale": "en",
"data": { "amount": 250 },
"channel": "both",
"dedup_key": "daily-2026-11-03"
}
{ "id": "0192f400-…", "duplicate": false }
locale: a short language code (tr,en…). If the requested language is not in the template,enis used.dedup_key(optional, 1–256 characters): if the same key arrives a second time, no new row is written;200andduplicate: trueare returned with the ID of the first notification.403: a client key or a player session.404: the template does not exist in any language.400:template_keyis empty or longer than 128 characters, thelocaleformat is invalid, or a placeholder in the template is missing fromdata.
Reading the inbox
GET /v1/notifications?status=queued&limit=20
x-crx-env: 0192f000-0000-7000-8000-00000000e001
{
"items": [
{ "id": "0192f400-…", "template_key": "daily_reward_ready", "template_version": 2, "title": "Your daily reward is ready", "body": "250 gold is waiting for you.", "data": { "amount": 250 }, "channel": "both", "status": "sent", "created_at": "2026-11-03T09:00:00Z", "sent_at": "2026-11-03T09:00:02Z", "read_at": null }
],
"next_cursor": null
}
The list is ordered newest to oldest (created_at, id). limit is 1–100; when next_cursor is null, it is the last page. Rows whose delivery failed are listed too. POST /v1/notifications/{id}/read makes the row read; a second call returns the same row and produces no new event.
Registering a push device
POST /v1/devices
x-crx-env: 0192f000-0000-7000-8000-00000000e001
Content-Type: application/json
{ "provider": "fcm", "platform": "android", "token": "<device token>", "device_meta": { "model": "Pixel 8" } }
{ "id": "…", "registration": "created", "provider": "fcm", "platform": "android", "created_at": "2026-11-03T09:00:00Z" }
provider is apns, fcm or one_signal; platform is ios, android or web. The token cannot be empty. The player ID comes from the session, not from the body. app_key separates the app in multi-app environments.
Note
The generated OpenAPI document writes the device provider as
onesignal; the server and the SDK useone_signal. This is a known documentation deviation; sendone_signal.
For web push, player.notifications.enableWebPush runs the browser permission flow with the settings it gets from GET /v1/push/config; on sign-out, session.logout() automatically revokes the web device registered from this page.
Rules: automatic notifications when an event happens
In the panel, a rule consists of a trigger (a game event from a closed list; for example level_up, quest_completed, store_purchase_completed, vip_tier_up), an optional condition, a template, a language, a channel and the recipient player path. Rule versions appear on the panel’s history tab. A notification itself does not trigger a new rule. The rule editor offers a “live preview” and a test send. For the detailed fields, see the panel page.
Versioning and replay safety
- Sending: replay safety with
dedup_key; there is noIdempotency-Keyheader. - Device registration and the read mark are naturally idempotent.
- The template version is bound to the notification; changing the template later does not affect an old notification.
Events
When a notification is written to the queue, a notification_queued event is produced and the send worker consumes it. Among the types you can select as a webhook filter there is no notification type; endpoints registered without a filter receive this internal event too, but the envelope format is not a documented contract.
Limits
dedup_keyis 1–256 characters; inboxlimitis 1–100.- Push delivery is best-effort: if the provider rejects it, the row becomes
failed. If a device is permanently invalid (for example the app was uninstalled), the device is revoked automatically. - If provider credentials are not defined, the notification waits in the queue and the provider status in the panel shows a warning.
Common errors
| Symptom | Cause | Fix |
|---|---|---|
403 (send) | A client key instead of a server key | Use the server key |
404 (send) | template_key does not exist in any language | Create and publish the template in the panel |
duplicate: true | The same dedup_key was already written | Expected behavior; use a new key for a new notification |
The notification stays queued | The provider is disabled or the credential is wrong | Check the provider status in the panel |
400 (device) | Empty token or unknown provider/platform | Fix the input |
Working example
Draining the inbox on the player side:
/** Oyuncu tarafı: gelen kutusunu okur ve okunmamış bildirimleri okundu işaretler. */
import type { PlayerClient } from "@ferdiunal/open-play-ts";
/** İlk sayfadaki okunmamış bildirimleri yazdırır ve okundu yapar. */
export async function drainInbox(player: PlayerClient): Promise<number> {
const page = await player.notifications.inbox({ limit: 20 });
let marked = 0;
for (const item of page.items) {
if (item.readAt !== null) continue;
console.log(item.title, item.body, item.data);
await player.notifications.markRead(item.id);
marked += 1;
}
return marked;
}
The curl example for sending from the server is above.