Offline push
When a user's app isn't in the foreground, new messages, friend and group notifications and audio/video calls reach the device's notification tray through Apple, Google and phone vendors' push channels, and tapping a notification returns to the app. While the app is in the foreground with a long-lived connection, messages arrive over the connection in real time and aren't pushed.
Setting up offline push takes three steps:
- Upload push credentials: upload each channel's credentials for the app in the console (such as an APNs
.p8key or an FCM service account); each credential has a name, see Push configuration. - Clients register push tokens: after the user signs in, the app gets a push token from the system or vendor and registers it with this service together with the credential name, see Client push token registration.
- Customize as needed: when your server sends messages, use the
pushfield to set the notification title and text, skip the push or force it; use the push devices and settings APIs to set Do Not Disturb for users or troubleshoot their push devices.
Push is best effort
Offline push is only a reminder: it may not arrive when the device is off, the channel is congested or the user turned off notification permission. Messages themselves aren't affected and are filled in by sync when the user opens the app. Apps without uploaded credentials send no offline push.
Push channels
| Channel | channel | Device platforms | Credentials to configure in the console |
|---|---|---|---|
| Apple Push (APNs) | apns | ios, macos | A .p8 key (recommended; it doesn't expire, and the same key also serves VoIP push) or a .p12 certificate; production and development environments are separate |
| Google FCM | fcm | android | The Firebase project's service account JSON |
| Huawei | huawei | android, harmony | App ID, App Secret |
| HarmonyOS | harmony | harmony | Project ID, service account key JSON |
| Honor | honor | android | App ID, Client ID, Client Secret |
| Xiaomi | xiaomi | android | App Secret |
| OPPO | oppo | android | App Key, Master Secret |
| vivo | vivo | android | App ID, App Key, App Secret |
| Meizu | meizu | android | App ID, App Secret |
Each channel can also take parameters such as message categories and notification channels. For where to get each item and how to test, see Push configuration.
- One credential per client app: each app can have up to 20 credentials, several per channel. When one IM app serves several client apps (such as a main app and a lite version, or different package names), or iOS production and development builds, upload one for each, and the app names which one when registering tokens.
- Web, Windows, Linux and mini programs have no offline push and rely on the long-lived connection.
- Message categories for Chinese vendors: Chinese vendors require applying for message categories by content (such as instant messaging); pushes without the right category are throttled or even rejected. Apply in the vendor's console, then fill the category and notification channel into the credential.
Channels differ in capability:
| Channel | Update notification after recall | App badge | Calls |
|---|---|---|---|
apns | Supported | Supported | Devices with a VoIP token get a VoIP push, and the app reports the call to CallKit to show the system call screen; others get a high-priority notification |
fcm | Supported | Supported (whether the home screen shows it depends on the launcher) | A high-priority data message; the app shows its own call screen and notification |
huawei, xiaomi | Supported | No badge | A high-priority notification that opens the app when tapped |
honor, harmony, oppo, vivo, meizu | Not supported | No badge | A high-priority notification that opens the app when tapped |
Which devices get pushes
- Devices that registered a push token: after signing in, the app registers a push token on the device. Devices that didn't (including Web and similar platforms) get no offline push.
- Devices not in the foreground right now: devices without a long-lived connection, or whose connection is up but whose app is in the background (reported by the client). Devices with the app in the foreground get messages over the connection and aren't pushed, counted as
skipped_online. Each of a user's devices is judged separately: with the computer in the foreground, a phone in the background still gets pushes. - macOS: desktop apps holding a connection always count as foreground and aren't pushed; the app shows a local notification itself when a message arrives. Pushes start once the computer sleeps, the app quits or the connection drops.
- Tokens follow sign-in: signing out, being signed out or replaced, password changes, bans, user deletion, or the console requiring everyone to sign in again unbinds the tokens registered by that sign-in, so the device stops getting this user's pushes. The app registers again at the next sign-in.
- A device can have several tokens: up to 3 regular tokens, prioritized in registration order, for example an Android phone's own vendor channel first and FCM as a backup. Sending uses the first usable one: if a credential is disabled, deleted or paused, or a token is invalid, the next is used, and the same push never shows twice on one device. iOS devices can also register 1 VoIP token, used only for calls.
- A token belongs to one sign-in: if a different account signs in on the same phone and registers the same token, the previous account's registration on that phone is voided immediately, even if it never signed out, so its pushes stop.
- Invalid tokens: when a channel reports a token invalid (app uninstalled, token expired and so on), the token is deleted after about a minute. Apps register again on every launch, which is harmless when the token hasn't changed.
What gets pushed
Messages
| Message | Pushed to |
|---|---|
| One-to-one | The recipient. Not the sender's other devices |
| Group | Group members other than the sender. Not members who joined after the message was sent or who've been removed |
These aren't pushed, even with push.force:
- Group notices (member changes, group profile changes and other system messages).
- Messages with
exclude_from_unreadtrue. - Messages with
push.disabledtrue. - Online-only messages.
Chat room messages have no offline push. Messages processed more than 30 minutes after sending (for example during a server backlog) aren't pushed; pushes already handed to a channel are kept there for 24 hours and delivered once a device that was off or offline reconnects.
Merging: several messages in one conversation within a second become one notification. The first message is pushed immediately, and those arriving within the next second are pushed together once, showing the newest with the count before the text, such as "[5] Zhang San: Got it" for a group; if any mentions the user, the newest mentioning message is shown. When a group is flooded, each offline member gets roughly one notification a second.
Friend and group notifications
When push_notice_enabled in the app's runtime policy is off (on by default), none of these are pushed; when the platform turns off vendor offline push in the platform console (a global switch), these and call pushes all stop.
| Notification | Pushed to | Built-in title | Built-in text |
|---|---|---|---|
| Friend request received | The person requested | The requester | Wants to add you as a friend (without the request message) |
| Friend request accepted | The requester | The person who accepted | Accepted your friend request |
| Added directly as a friend (the user's friend add mode is "anyone") | The person added | The person who added them | Added you as a friend |
| Someone applies to join a group | Owner and admins | Group name | Li Si applied to join the group |
| A member invites someone, pending approval | Owner and admins | Group name | Li Si invited Wang Wu to the group, pending approval |
| Group invitation received | The invitee | Group name | Li Si invited you to the group |
| Removed from a group (including removed and blocked) | The person removed | Group name | You were removed from the group |
| Group dissolved | Members at dissolution, excluding the owner if they dissolved it | Group name | The group was dissolved |
Friends added by your server or console, requests the user accepted themself, leaving a group themself, and deleted accounts aren't pushed: the person already knows, or doesn't need to.
Calls
Incoming audio and video calls are pushed to the callee (invited members for group calls). When the call is canceled or not answered, the incoming call notification on the device is replaced with "Call canceled" or "[Missed call]". Channels that can't overwrite by notification ID (see "Update notification after recall" in Push channels) can't replace it and show another notification instead.
- Call notifications are valid for the remaining ring time; ones not delivered in time are dropped by the channel, so phones don't ring after the call ended.
- When a user answers or declines on one device, call notifications on their other devices are cleared. The HarmonyOS, OPPO, vivo and Meizu channels can't clear them; the app clears them next time it opens.
- Calls ignore conversation Do Not Disturb, follow global and quiet-hours Do Not Disturb, and don't count toward the app's push quota.
- The call record message written to the conversation after the call isn't pushed.
Notification title and text
Titles and text come from templates, distinguished by template key and language. Simplified Chinese (zh-CN), Traditional Chinese (zh-TW) and English (en) are built in, and you can override any template key in any language in the console, see Notification templates. The built-in message templates (Simplified Chinese):
| Case | Title | Text |
|---|---|---|
| One-to-one | Sender | Message summary |
| One-to-one, several merged | Sender | [5条] Message summary |
| Group | Group name | Sender: message summary |
| Group, several merged | Group name | [5条] Sender: message summary |
| Group message mentioning the user (including mention-all) | Group name | [有人@我] Sender: message summary |
| Group message sent as the system | Group name | Message summary |
- Sender: the sender's nickname, or username if none; the recipient's friend remarks and group nicknames aren't used. Deleted users appear as "已删除的用户", and messages sent as the system as "系统".
- Group name: groups without a name appear as "群聊".
- Message summary: the first 100 characters of text messages, with line breaks replaced by spaces; "[图片]", "[语音]" and "[视频]" for images, voice and video; "[文件] file name" for files; "[位置] place name" for locations; "[消息]" for custom and other types.
- Length: titles up to 64 characters and text up to 256, truncated with "…" beyond that; some channels have smaller limits and truncate further. Control characters are always removed.
- Language: the device's registered language is tried in order, using the first match: the exact language; the default form of the same language (
zh-TW,zh-HK,zh-MOand Traditional Chinese usezh-TW, other Chinesezh-CN, other languages their primary language, such asenforen-GB);push_default_languagein the runtime policy (zh-CNby default); built-inzh-CN. Each step checks console overrides before built-in templates. For example, after overriding the one-to-one template in Japanese (ja), Japanese devices get one-to-one messages in Japanese while other pushes still use the default language.
Preview mode
Users can choose what notifications show; without a choice, push_default_preview in the runtime policy applies (full by default):
| Preview | Shows | One-to-one title | One-to-one text |
|---|---|---|---|
full | Sender and content | Zhang San | Dinner tonight? |
sender_only | Only the sender | Zhang San | Sent you a new message |
none | Neither | Empty; the system shows the app name | You have a new message |
- The preview mode takes priority over title and text your server gives in
push: if the user chosesender_onlyornone, your content isn't shown either. - Friend and group notifications and calls don't contain message content, so
sender_onlyis the same asfull; withnonethey show "You have a new notification", "You have an incoming call" and "You have a missed call". - Push content passes through Apple, Google and phone vendors' servers. For privacy-sensitive apps (such as healthcare or finance), set
push_default_previewtonone.
The push field when sending
When your server sends a message, the request body can include a push object to control that message's offline push:
| Field | Type | Description |
|---|---|---|
disabled | Boolean | true to skip pushing this message; default false |
force | Boolean | true to ignore the recipient's Do Not Disturb (conversation, global and quiet hours); default false. Can't be combined with disabled: true |
title | String | Notification title, 1 to 64 characters, shown as is, without templates or placeholder substitution |
body | String | Notification text, 1 to 256 characters, same rules as title |
ext | Object | Custom data handed to the app with the notification, such as the page to open when tapped; at most 512 bytes of JSON |
pushmay only have the fields above, strings can't contain control characters, and the wholepushis at most 2 KB of JSON. Otherwise you get400 invalid_argumentwithdetails.reasoninvalid_pushand the field indetails.field.- If only one of
titleandbodyis given, the other still comes from the template. When the recipient's preview mode isn'tfull,titleandbodydon't apply, see Preview mode. forceonly bypasses Do Not Disturb; it doesn't make a silent message alert: messages also set withexclude_from_unreadstill aren't pushed. For notices that must arrive, such as account security or order problems, don't setexclude_from_unread.title,bodyandextgo to the channel and app as is, without moderation, so make sure the content is compliant yourself.- When users send from clients,
pushcan only setdisabled, so users can't fake system notifications on others' lock screens.
Send a maintenance notice that must arrive, to a group as the system:
curl -X POST "$IM_API/$ORG/$APP/messages" \
-H "Authorization: Bearer $APP_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"to_group": "100311272729346048",
"client_msg_id": "notice-20261005-01",
"type": "text",
"body": { "text": "今晚 23:00 至 24:00 系统维护,期间无法下单" },
"push": {
"force": true,
"title": "系统维护通知",
"body": "今晚 23:00 至 24:00 系统维护",
"ext": { "page": "notice", "notice_id": "N1001" }
}
}'When push is invalid:
{
"error": {
"code": "invalid_argument",
"message": "push 不符合推送模块的规则",
"details": { "field": "force", "reason": "invalid_push" },
"request_id": "TNDNXP7E23UBI3SARLL7P52XGP"
}
}Do Not Disturb
| Do Not Disturb | Description |
|---|---|
| Conversation | Set per one-to-one chat or group: none pushes nothing from that conversation; mention_only, groups only, pushes only when the user is mentioned. With push_mention_all_breaks_mute on in the runtime policy (on by default), mention-all counts as mentioning the user. Can last a set time and then revert, or stay on |
| Global | Push no messages or notifications for a while (such as "don't disturb for 8 hours") or indefinitely |
| Quiet hours | No pushes during a daily period (such as 22:00 to 07:30), which may cross midnight; the start time is included and the end time isn't. Calculated in the time zone submitted when setting it, which doesn't follow the device: users who travel to another time zone need to set it again |
Pushes and Do Not Disturb:
| Push | Conversation none | Conversation mention_only | Global | Quiet hours |
|---|---|---|---|---|
| One-to-one message | Not pushed | — | Not pushed | Not pushed |
| Group message without a mention | Not pushed | Not pushed | Not pushed | Not pushed |
| Group message mentioning the user | Not pushed | Pushed | Not pushed | Not pushed |
Message with push.force | Pushed | Pushed | Pushed | Pushed |
| Friend notifications | — | — | Not pushed | Not pushed |
| Join applications and invitations | Not pushed for that group | Pushed | Not pushed | Not pushed |
| Removed from group, group dissolved | Pushed | Pushed | Not pushed | Not pushed |
| Calls | Pushed | Pushed | Not pushed | Not pushed |
| Update after recall | Updated as usual | Updated as usual | Updated as usual | Updated as usual |
- Do Not Disturb only affects offline push, not receiving messages or unread counts. Whether the conversation list shows a mute icon, and whether muted conversations count toward the badge, is up to the app based on the synced settings.
- Users set their own Do Not Disturb in clients, and your server can set it for them with the push devices and settings APIs, for example to sync with your app's "message alerts" switch. Settings sync across all the user's devices.
- When a user leaves or is removed from a group, their Do Not Disturb for that group is deleted, so rejoining starts with normal pushes. Each user can have up to 10000 conversation Do Not Disturb settings.
App badge
The badge number on the app icon is per device, so a user's iPhone and iPad each have their own:
- When the app goes to the background, it reports the badge it should show (usually the total unread count; whether muted conversations count is up to the app). After that, each pushed message or friend or group notification adds one to that device's badge and sends it with the notification, adding the merged count for merged pushes.
- Calls, updates after recall and test pushes carry no badge, and pushes skipped by Do Not Disturb, foreground devices or the push quota don't count; pushes that were sent but not delivered aren't rolled back.
- The app updates the icon badge itself back in the foreground and reports again next time it goes to the background. If the user reads messages on another device, this device's badge only updates the next time the app opens.
- With
push_badge_enabledoff in the runtime policy (on by default), pushes carry no badge and the app handles it; channels that don't support badges (see Push channels) carry none.
Updating notifications after a recall
When a message is recalled (including by the group owner, an admin, your server or the platform), its notification on the device is replaced so the original content doesn't stay on the lock screen or in the tray: with "Zhang San recalled a message" when the sender recalled it, or "A message was recalled" when someone else did or the recipient's preview mode is none.
- Only messages recalled within 24 hours of the push are updated, and only through the token that sent the original push; the channel must support overwriting by notification ID (APNs, FCM, Huawei, Xiaomi), and the original notification stays on other channels.
- The replacement doesn't change the badge and tries not to make a sound: APNs sends it silently; on Android it depends on the notification channel, using the silent channel if the credential configures one and possibly sounding again if not. If the user already swiped the original away, the replacement reappears in the tray.
- Pushes not yet sent at recall time are no longer sent.
- Updates after recall aren't limited by Do Not Disturb, disabled apps or the push quota: once violating content is recalled, it should disappear from lock screens too. The exception is when the platform turns off vendor offline push (the global switch): no vendor pushes at all are sent while it's off.
Client push token registration
Only the app can register push tokens; your server can't register for users. These are client APIs called with the User Token, acting on the current signed-in device; the SDKs wrap them:
| Method | Path | Description |
|---|---|---|
| PUT | /client/v1/push/device | Register this device's push, replacing everything: tokens are regular tokens, up to 3, prioritized in order, each with credential (the credential name) and token; voip_token is a VoIP token, iOS only, optional; language is the notification language (BCP 47, up to 16 characters), optional; badge is the current badge (0 to 99999), optional. Returns a push device object plus changed |
| GET | /client/v1/push/device | Get this device's registration; 404 not_found if never registered |
| DELETE | /client/v1/push/device | Unregister this device's push, for when the user turns off notifications in the app; 204 even if never registered |
| PUT | /client/v1/push/device/badge | Report the badge with {"badge": 3}; while connected, reporting it with the connection's state is enough |
| GET, PATCH | /client/v1/push/settings | The user's own push settings, in the format of the push settings object |
| GET | /client/v1/push/conversations | The user's own conversation Do Not Disturb, with the same parameters as List conversation Do Not Disturb |
| PUT, DELETE | /client/v1/push/conversations/single/{username}, /client/v1/push/conversations/group/{group_id} | Set or remove conversation Do Not Disturb |
A registration request:
curl -X PUT "$IM_API/client/v1/push/device" \
-H "Authorization: Bearer $USER_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"tokens": [
{ "credential": "xiaomi-main", "token": "Lx9Qw2Er5Ty8Ui1Op4As7Df0Gh3Jk6Zx9Cv2Bn5Mq8We1Rt4Yu7Kd3P" },
{ "credential": "fcm-global", "token": "cR5tY8:APA91bF2gH4jK6lZ8xC0vB2nM4qW6eR8tY0uI2oP4aS6dF8gH0jK2lZ4xC6vB8nM0q" }
],
"language": "zh-CN",
"badge": 0
}'When registering, the app should note:
- When to register: after signing in, on every cold start, and when the vendor refreshes the token, submit the full registration. When tokens and language haven't changed,
changedisfalse, so repeated calls are fine. When signing out, just call the sign-out API without unregistering push separately, and clear notifications the app already showed so the next user doesn't see them. - Channels and platforms: the device platform is the
platformsubmitted at sign-in.apnsis only foriosandmacos;huaweiforandroidandharmony;harmonyonly forharmony; other channels only forandroid. VoIP tokens can only be registered on iOS devices, under APNs credentials that can send VoIP push. - Token format: 1 to 512 visible ASCII characters; APNs tokens are 64 to 200 hex digits.
- System notification permission: when users turn off the app's notifications in system settings, APNs and FCM still accept pushes (counted as
sent) but the device doesn't show them. Check permission on every cold start and return to the foreground; unregister the device's push when it's off and register again once it's back on. - Foreground and background: when the app goes to the background, report the background state and badge over the connection; until that report arrives, the server treats the device as foreground and doesn't push.
- Android notification channels: on Android 8 and later, the app creates the notification channels configured in the credential at startup (the message channel and a low-importance silent channel), with channel IDs matching the credential.
- Rate limits: per device per minute, 10 registrations and unregistrations combined and 60 badge reports; per user per minute, 60 reads and 60 changes each for push settings and conversation Do Not Disturb.
Registration failures return 400 invalid_argument with the reason in details.reason:
details.reason | Cause |
|---|---|
unknown_credential | The credential name doesn't exist or was deleted; details.credential is the name |
platform_mismatch | The credential's channel doesn't fit the device platform; or a VoIP token isn't on an iOS device, or its credential isn't APNs or can't send VoIP push |
platform_not_supported | The sign-in platform is Web, Windows, Linux or a mini program, which can't register push |
duplicate_credential | Two regular tokens belong to the same credential, or the same token appears twice |
invalid_token | A token has the wrong format, or there are more than 3 regular tokens |
no_token | No tokens at all. Use DELETE to unregister |
invalid_language | The language isn't valid BCP 47 |
invalid_badge | The badge isn't an integer from 0 to 99999 |
unknown_credential and platform_mismatch are app configuration errors that retrying won't fix; an expired sign-in returns 401 session_revoked, handled as in Sign-out reasons.
Tapping notifications: every push carries a JSON object for the app in the channel's custom data under the key im. Message notifications include the conversation type conversation_type, conversation ID conversation_id, message sequence seq, message ID message_id, sender username sender (omitted when the preview mode is none), and whatever your server gave in push.ext as ext, so the app can open the conversation, jump to the message or go to its own page.
Runtime policy
These runtime policy items control the app's offline push, see Runtime policy:
| Field | Description | Default | Values |
|---|---|---|---|
push_default_preview | The preview mode when the user hasn't chosen one | full | full, sender_only, none |
push_default_language | The notification language when the device registered none, or no template exists for it | zh-CN | A BCP 47 language tag, up to 16 characters |
push_badge_enabled | Whether pushes carry the app badge | true | true, false |
push_notice_enabled | Whether friend and group notifications are pushed | true | true, false |
push_mention_all_breaks_mute | For groups set to mention_only, whether mention-all is pushed too | true | true, false |
These aren't shown on the console's policy page yet and use their defaults; contact us to change them. There's also a global switch for vendor offline push: platform administrators can turn all apps' vendor pushes off or on at once on the "Push" page of the platform console. While off, messages are counted as skipped_disabled and console test pushes return an error; after turning it back on, pushes resume, and pushes skipped while it was off aren't resent.
Each app's pushes per minute are also limited by the platform quota push_per_min_limit, 60000 per minute by default, counted per device: messages and friend and group notifications count, while calls, updates after recall and test pushes don't. Devices over the limit aren't pushed this time and are counted as rate_limited. The quota deals with abuse per app and only stops messages and notifications; the global switch covers the whole deployment and stops every vendor push, calls and tests included.
Effect of app status
| Status | Offline push |
|---|---|
| App disabled or suspended, tenant suspended or in the account-closure cooling-off period | No messages, notifications or calls are pushed; updates after recall are still sent. Tokens, settings and credentials are kept and pushes resume afterwards; pushes skipped meanwhile aren't resent |
| The platform turned off vendor offline push (global switch) | No messages, notifications or calls are pushed, and updates after recall aren't sent either (unlike the platform disabling one app); client APIs work as usual, test pushes return an error, and pushes resume once it's back on; pushes skipped meanwhile aren't resent |
| Read-only (set by the platform or because of unpaid bills) | Pushes as usual; clients can still register tokens and change Do Not Disturb |
| App deleted, tenant closed | No more pushes, and the data is deleted |
Push statistics and troubleshooting
"Push statistics" in the console counts pushes by date, channel, category and result, kept for 180 days, see Push statistics. The server doesn't keep a record of every push or provide vendor delivery and click receipts; when a user doesn't get pushes, follow Troubleshooting missing pushes to check their devices and settings, and send a test push.
Push categories: message, notice (friend and group notifications), call (incoming, missed and canceled calls), dismiss (clearing call notifications), recall (updates after recall), test.
Push results:
| Result | Meaning | Counted per |
|---|---|---|
sent | Accepted by the channel. Only means the channel took it, not that the device showed it | Device |
failed | The channel rejected it (content too long, the device's daily limit reached and so on), a network error persisted after retries, or a credential error with no other usable token on the device | Device |
invalid_token | The channel reported the token invalid, the token was deleted, and the device has no other usable token | Device |
credential_unavailable | All the credentials of the device's tokens are disabled or deleted | Device |
channel_paused | The credential is paused (channel limits, daily quota full, repeated authentication failures and so on) and the device has no other usable token | Device |
rate_limited | Over the app's push quota push_per_min_limit | Device |
queue_full | The server's send queue was full and the push was dropped | Device or event |
expired | Too late: the message was processed more than 30 minutes after sending, or waited too long in the send queue | Event or device |
unsupported | The device's channel doesn't support this kind of push, such as updates after recall | Device |
recalled | The message was recalled before the push was sent | User or device |
skipped_online | The device is online in the foreground and doesn't need a push | Device |
skipped_dnd | Skipped by Do Not Disturb | User |
skipped_disabled | The platform turned off vendor offline push (global switch), stopping all vendor pushes | Event or device |
no_device | The recipient has no device registered for push | User |
The channel in statistics is that of the last token used; when no token was chosen (such as skipped_online, skipped_dnd, no_device, credential_unavailable), the channel is an empty string.
Limits
| Item | Limit |
|---|---|
| Push credentials per app | 20 |
| Push tokens per device | 3 regular, 1 VoIP |
| Conversation Do Not Disturb per user | 10000 |
| Conversation and global Do Not Disturb duration | 1 second to 10 years, or indefinite |
| Notification title, text | 64 characters, 256 characters |
push.ext | 512 bytes |
The whole push | 2 KB |
| Template overrides per app | 300 |
| App pushes per minute | 60000 by default, counted per device, platform quota push_per_min_limit |
| Message push window | 30 minutes after sending |
| Push validity in the channel | 24 hours; remaining ring time for calls |
| Push statistics | Kept 180 days, at most 92 days per query |
