Notifications
From event to message: message types, templates and locales, consent, quiet hours and caps, built-in sending, webhooks and connectors (Klaviyo, Braze, OneSignal, FCM), the delivery log and unsubscribe links.
Orishare turns what happens in your programs (a streak about to break, a league promotion, a friend's nudge) into messages to the player: email, web push or mobile push, sent by Orishare or handed to your own messaging tool. You decide which events become messages and with which copy; Orishare applies consent, unsubscribes, quiet hours and frequency caps and records every outcome.
Reads need notifications:read and changes notifications:write. In the dashboard,
notifications are under Engagement.
From event to message
- A mechanic records an event in the same transaction as the fact (for example
league.promoted,streak.at_risk,friend.nudged). Every event also goes to your webhook endpoints, as before. - Each active message type whose
event_typescontain that event makes one message per route (channel). The same event never makes a second message on the same channel, across retries and replays. - The policy runs, in order: recipient (deleted customers get nothing; anonymous customers get
no social messages), expiry, reachability (an email address, an active device), consent,
frequency caps, quiet hours. The first refusal wins and is recorded as
suppressedwith its reason; quiet hours hold the message until the window ends. - The message is rendered in the player's language and sent; the outcome is stored.
An environment with no message type behaves exactly as before: webhooks go out, nothing else.
Message types
GET /v1/notification_catalog lists the built-in triggers with their event types, variables,
default copy and whether one is configured: streak_at_risk, streak_lost, streak_milestone,
streak_freeze_used, level_up, badge_earned, quest_completed, quest_ending,
reward_ready, league_period_started, league_promoted, league_demoted, league_ending,
friend_request, friend_accepted, friend_nudge, friend_high_five,
friend_quest_completed, friend_streak_at_risk and team_quest_completed.
Create a message type with POST /v1/notification_message_types:
{
"key": "league_promoted",
"routes": [
{ "channel": "mobile_push", "path": "builtin" },
{ "channel": "email", "path": "builtin" }
],
"max_per_day": 2,
"respect_quiet_hours": true
}A catalogue key fills name, event_types and class from the catalogue. Any other key is a
custom type and needs event_types (up to 20). class is marketing (the default; consent,
opt-outs and caps apply) or transactional (for messages the player must get, such as a reward
they bought; marketing opt-outs do not apply). routes has up to 8 entries, each built-in channel
at most once: { "channel": "email" | "web_push" | "mobile_push", "path": "builtin" } or a
connector route (below). PATCH /v1/notification_message_types/{id} changes everything but the
key; POST /v1/notification_message_types/{id}/pause and /resume take
{ "expected_revision" }. A message type is paused, never deleted.
Templates and languages
POST /v1/notification_message_types/{id}/templates sets the copy for one channel and locale:
{
"channel": "mobile_push",
"locale": "fr",
"title": "Promotion en ligue {{tier}}",
"body": "Tu as fini {{rank}}e et tu montes en ligue {{tier}}."
}Email takes subject, body and optionally html; push takes title, body and optionally
url. Placeholders are {{name}} only, with no logic: the event's variables (see the catalogue),
customer.display_name, actor.display_name (the other player of a social event, shown as the
recipient is allowed to see them), unsubscribe_url, preferences_url and message_type. When
the player (or the other player) has no display name, customer.display_name (or
actor.display_name) renders A player. An
unknown placeholder renders empty, and values are escaped in HTML. The template is chosen by the
player's locale (their contact locale, else their device's, else the environment default): exact
match, then the language, then default_locale, then en, then the catalogue's default copy. A
custom type with no template fails with no_template.
Which placeholders an event offers. A catalogue type lists its variables in
GET /v1/notification_catalog. A custom type's variables is always [] in that read, because
its variables come from the event that triggers it:
- A domain event (the economy's
chest.opened,item.purchased,boost.started, ... and otherorishare.domain_event/v1events): every top-level scalar field of itsdata(string, number, boolean, null). Lists and objects are left out. Forchest.openedthat is{{item}},{{loot_table}},{{chests}}and{{award_count}}, but notawards. Thedataof each economy event is in Game economy, events. - A program fact without a catalogue entry (
orishare.program_fact/v1):{{program_key}}and{{program_name}}. - The program facts the catalogue knows keep their own variables even in a custom type:
streak.at_risk(streak_length,period_ends_at),streak.broken(previous_length),streak.milestone_reached(milestone,streak_length),streak.freeze_consumed(streak_length,freezes_left),tier.changed(level,previous_level,direction),badge.granted(badge_name,rank: the rank that grant awarded, so a contribution that crosses several ranks sends one message per rank),quest.completed(quest_name,claim_by;claim_byis empty for a quest program, whosequest.completedmeans nothing is left to claim).
{{level}} and {{previous_level}} are the level's name (its display.title, else its key;
Level 3 in the game starter), not a number: write You reached {{level}}, never Level {{level}} (it renders
Level Level 3).
GET /v1/notification_message_types/{id}/templates lists them and
DELETE /v1/notification_message_types/{id}/templates/{channel}/{locale} removes one.
Consent, quiet hours and caps
PATCH /v1/notification_settings (with If-Match) holds the environment-wide rules:
| Field | Meaning |
|---|---|
email_consent_mode | opt_in (marketing email needs the player's explicit consent) or opt_out. |
quiet_hours | { start_minute, end_minute } of the local day (it may wrap midnight), in the player's time zone, else the environment's. |
max_per_day, max_per_week | Marketing messages per player across all types. Each message type has its own as well. |
default_locale | The fallback language. |
from_name, reply_to | Email sender display name and reply address. |
Set expires_at when timing matters: a held message whose expiry passes is suppressed with
quiet_hours or expired rather than sent late, and nothing is held more than 24 hours.
Recipients
The player's address and devices belong to the notification engine; other features never see them.
- Contact:
PATCH /v1/customer/notification_contact(from the session:email,locale,email_consent,push_consent) orPOST /v1/customers/{id}/notification_contactfrom your backend. A changed address is reachable again. - Devices:
POST /v1/customer/notification_deviceswith{ "kind": "web_push" | "fcm" | "apns", "token", "platform"?, "locale"? };DELETE /v1/customer/notification_devices/{id}removes one. Tokens are never returned; a token the provider rejects is markedinvalid. - Preferences:
GET /v1/customer/notification_preferencesandPATCH /v1/customer/notification_preferenceswithunsubscribed_all,email_consent,push_consentandtypes: [{ "type_key", "channel", "opted_out" }](type_key*for every type,channelallfor every channel). The hosted widget'snotification_preferencessurface is this page, ready made.
Every marketing email carries a signed one-click unsubscribe link (with the
List-Unsubscribe and List-Unsubscribe-Post headers) and a preferences link:
POST /v1/notifications/unsubscribe?token=... unsubscribes in one click (safe to repeat), while
GET /v1/notifications/unsubscribe?token=... and GET /v1/notifications/preferences?token=...
only read what a link refers to, for your own landing page. They need no credential; a missing,
empty or forged token is 404, and a preferences link can never unsubscribe. An unsubscribe is
honoured before the next send, even for a message already scheduled.
Three ways out
- Webhook only (the default): no message type, your endpoints receive the event and you send whatever you like.
- Built-in sending: Orishare sends the email or push itself. Test environments always use
a fake provider: the message is recorded as
sentwith providerfakeand nothing leaves Orishare, so you can test copy, consent and caps safely. In a live environment a built-in route needs Orishare's providers to be connected for your deployment; until they are, built-in messages of a claimed project fail withprovider_not_configuredand a connector is the way to send. In a project nobody has claimed yet (agent signup), built-in messages in a live environment are not failed butsuppressedwith reasonproject_unclaimed; claiming allows sends from then on and never replays a message suppressed before. - Connectors: Orishare hands the message to your own tool, with your own credentials.
Connectors (Klaviyo, Braze, OneSignal, FCM)
POST /v1/notification_connectors stores the credentials sealed; reads return only the last four
characters (credential_hint) and public settings:
kind | credentials | What Orishare sends |
|---|---|---|
klaviyo | { "api_key" } | An event orishare.<type key> on the profile with your external_id; build the flow in Klaviyo. |
braze | { "api_key", "rest_endpoint" } (your Braze REST instance) | A custom event on the user with your external_id, or with a route template a campaign trigger with that campaign id. |
onesignal | { "app_id", "api_key" } | A push to the subscription whose external id alias is your external_id; with a route template, that OneSignal template, otherwise the rendered title and body. |
fcm | { "service_account": { "project_id", "client_email", "private_key", ... } } | A push through FCM HTTP v1 to the player's registered fcm devices. |
Then route a message type to it:
{ "channel": "connector", "path": "connector", "connector_id": "ncn_...", "template": "optional" }Your tool owns reachability, its own consent records and timing; Orishare still refuses deleted
recipients, social messages to anonymous players, expired messages and, for marketing types,
players who unsubscribed or opted out in Orishare. Connectors send for real from live environments;
in a test environment the send is simulated (recorded as sent, nothing reaches the vendor). POST /v1/notification_connectors/{id}/disable and
/enable (with { "expected_revision" }) switch one off and on; last_error shows the latest
vendor refusal. A deployment without a sealing key cannot store connectors
(501 capability_unavailable). See the recipe Push through your own OneSignal.
Delivery log
GET /v1/notification_messages (filters customer_id, type_key, status, channel, newest
first) and GET /v1/notification_messages/{id} show every message with its rendered copy (never
the address or token), attempts, provider and reason. GET /v1/notification_message_stats?since=
counts by type, channel, status and reason.
status | Meaning |
|---|---|
scheduled | Waiting for its send time (quiet hours, a retry). |
sending | Being sent now. |
sent | Accepted by the provider or connector. |
delivered | Confirmed delivered. |
failed | Not sent: no_template, no_device, invalid_target, provider_not_configured, provider_rejected, attempts_exhausted. |
bounced | The email bounced after sending. |
suppressed | Not sent on purpose: unsubscribed, opted_out, no_consent, no_address, address_bounced, address_complained, frequency_cap, quiet_hours, expired, anonymous_recipient, recipient_deleted, blocked (either player blocked the other), not_friends (a friend message after the friendship ended), project_unclaimed (built-in email or push in a live environment before the project is claimed; the claim never replays it). |
Retryable provider errors are retried with backoff, up to 6 attempts.
Email bounces and complaints (Resend)
Built-in email goes through Resend. Resend reports bounces and complaints to
POST /v1/notification_provider_events/resend, signed with Svix; a hard bounce or a complaint
marks the address (address_bounced, address_complained) so it is not mailed again, and marks
the message bounced. Whoever runs the API sets this up once:
- In the Resend dashboard, add a webhook to
https://<your API origin>/v1/notification_provider_events/resendfor the eventsemail.bouncedandemail.complained. - Copy its signing secret (
whsec_...) into the API'sNOTIFICATIONS_RESEND_WEBHOOK_SECRETand restart the API. Without it the route answers501 capability_unavailable. - Send a test event from Resend: the answer is
200withreceived: true. A request with a bad or old signature (more than 5 minutes) changes nothing.
Changing notifications through change sets
In a live environment without auto autonomy an agent proposes notification configuration as change-set
commands (POST /v1/change_sets): notification_message_type.create (the create body),
notification_message_type.update, .pause and .resume (key, expected_version and, for
update, the PATCH fields), notification_template.put (key, channel, locale and the
template) and notification_template.delete (key, channel, locale; irreversible), and
notification_settings.update (expected_version and the settings fields). Connectors hold
credentials and are created directly by a person or a secret key, not through change sets.
The template fields sit at the top level of the command, beside type, exactly like the direct
POST /v1/notification_message_types/{id}/templates body; a nested "template": { ... } object
is refused:
{
"commands": [
{
"type": "notification_template.put",
"key": "level_up",
"channel": "mobile_push",
"locale": "fr",
"title": "Niveau {{level}}",
"body": "Bravo, tu passes au niveau {{level}}."
}
]
}These commands pay nothing, so in a live environment on limits they apply on request (a
notification_template.delete is a deletion and is refused like any other).
Friends and teams
Friend codes, requests, blocks, nudges and high-fives, friend standings, friend quests, friend streaks, teams and team quests, with the privacy rules and what anonymous sessions can do.
Match results
Report finished matches from an authoritative game server: plausibility bounds, per-player rate windows, held results and review, and the trusted match.completed event rules turn into XP, scores and items.