Content
A notification's content is a map of locale → copy, plus a _default pointer:
{
"en": {
"title": "Weekend flash sale ⚡",
"body": "{{firstName|there}}, 30% off everything until Sunday night.",
"image": "https://cdn.acme.example/push/weekend.jpg",
"url": "acme://sale/weekend",
"actions": [
{ "id": "shop", "title": "Shop now" },
{ "id": "later", "title": "Remind me later", "url": "acme://remind?sale=weekend" }
]
},
"tr": { "title": "Hafta sonu indirimi ⚡", "body": "…", "url": "acme://sale/weekend" },
"_default": "en"
}
Locale resolution
For each subscriber the worker picks, in order:
- the exact
language(en-GB), - its base (
en), _default.
_default must name a locale that exists in the map. Keys are BCP 47-ish (tr, en, en-GB).
Fields
| Field | Limits | Notes |
|---|---|---|
title | 1–200 chars | Required. Placeholders allowed. |
body | 1–1000 chars | Required. Placeholders allowed. |
image | URL | HTTPS. Android big picture, web image, iOS attachment via the NSE. Not shown on the iOS lock screen preview. |
url | ≤ 2048 | Deep link or URL opened on tap. Placeholders allowed. |
actions | ≤ 3, unique ids ^[a-z0-9_-]{1,32}$, titles ≤ 40 | Buttons. url overrides the notification's own for that button. |
Drafts in the wizard are held to a looser schema (empty fields allowed) so autosave never loses text; the strict rules above apply at send time.
Presentation
Locale-independent options in presentation:
| Field | iOS | Android | Web |
|---|---|---|---|
sound | aps.sound ("default", or a file in the app bundle); "none" → no key = silent | data.sound → the SDK's channel/setSilent | silent: true for "none" |
badge | aps.badge | setNumber | badgeCount (Badging API) |
threadId | aps.thread-id (groups in Notification Center) | notification group key | — |
icon | — | large icon when there is no image | notification icon |
interruptionLevel | aps.interruption-level: passive, active (default), time-sensitive, critical | — | — |
relevanceScore | aps.relevance-score, 0–1, orders the notification summary | — | — |
channelId | — | the Android channel the SDK posts to | — |
Anything a platform cannot show is ignored by that platform's transport.
Interruption level is what lets a push break through Focus (time-sensitive, needs the Time Sensitive Notifications capability in the app) or silent mode (critical, needs an entitlement from Apple). The server passes the value through and does not check the entitlement; without it iOS downgrades to active. passive drops into the list without a sound or banner.
Android channels are defined once in the dashboard and created on the device by the SDK the first time a push names one, so a new channel never needs an app release. A channel the device does not have and the push does not describe falls back to the SDK's default channel.
Silent pushes
silent: true on a campaign, notification or template sends a background push: nothing is drawn, the app is woken briefly and gets data.
| Platform | What goes out | What the SDK does |
|---|---|---|
| iOS | apns-push-type: background, priority 5, aps: { "content-available": 1 } — no alert, sound or mutable-content (Apple discards a background push that carries any of them) | didReceiveBackground(userInfo:) reports delivered and calls onReceive with message.silent == true |
| Android | the usual data message with silent: "1" | draws nothing, calls onReceive |
| Web | skipped at fan-out — a browser cannot take a push without showing something | — |
A silent campaign needs no content; data is the whole message (the wizard's Data card). iOS gives the app about 30 seconds and throttles apps that send too many — a few per hour is safe, a few per minute is not. A/B tests are not available on silent campaigns.
Data payload
data is a flat string → string map (max 64-char keys, 1024-char values) that rides along untouched and reaches your onOpen handler as message.data. Use it for routing (screen, ids). It is flat because FCM allows nothing else in data.
Platform rendering cheat sheet
| Feature | iOS | Android | Chrome/Edge | Firefox | Safari (mac/iOS) |
|---|---|---|---|---|---|
| Title / body | ✅ | ✅ | ✅ | ✅ | ✅ |
| Image | ✅ (NSE) | ✅ | ✅ | ✅ | ❌ |
| Buttons | ✅ ≤4 (NSE) | ✅ ≤3 | ✅ ≤2 | ✅ | ❌ |
| Sound | ✅ | ✅ | system | system | system |
| Badge | ✅ | ✅ | ✅ PWA | ❌ | ✅ PWA |
| Group / thread | ✅ | ✅ | via tag | via tag | ❌ |
No sound (sound: "none") | ✅ | ✅ | ✅ | ✅ | ❌ |
Background (silent: true) | ✅ | ✅ | skipped | skipped | skipped |
| Interruption level | ✅ iOS 15+ | — | — | — | — |
| Channels | — | ✅ Android 8+ | — | — | — |
Size
Web push payloads are encrypted and capped at 4 KB by the push services (3 993 bytes of plaintext with the record size used). APNs allows 4 KB; FCM 4 KB of data. The wizard's byte meter shows the largest locale. Keep data lean; if you must attach more, send an id and fetch on open.
Emoji and Unicode
Everything is UTF-8 end to end. Emoji in titles and bodies are fine on all platforms; counts are in characters, not bytes.