Push payloads
poolse sends pushes to your users' phones through Apple (APNs) and Google (FCM). It signs them with the credentials you upload on the dashboard. Push notifications explains the setup. This page lists every payload.
@poolse/native handles all of these for you: ringing, notifications,
Reply and Mark as read. Read this page if you are writing your own
native handling, or debugging.
Overview
| Push | Why | iOS | Android |
|---|---|---|---|
| Ring | An incoming call | VoIP push (PushKit token) | FCM data message |
| Ring ended | Stop ringing | — | FCM data message |
| Call finished | A call this device knew about is over | — | FCM data message |
| Missed call | You missed a call | Alert push (APNs token) | FCM data message |
| New message | Chat notification | Alert push (APNs token) | FCM data message |
| Conversation read | Clear notifications read on another device | Background push (APNs token) | FCM data message |
What every push has in common:
- Android pushes are FCM data messages with
android.priority: "HIGH". There is nonotificationblock, so your app's code decides what to show, even when the app was closed. FCM turns every value into a string: numbers arrive as"1793952000000", flags as"0"/"1". - iOS pushes go to one of two tokens, depending on the kind of
push. VoIP pushes go to the PushKit token, which you register with
kind: "voip". Everything else goes to the regular APNs token, which you register withkind: "alert". See Devices. On iOS, numbers stay numbers. - Every payload has
cv: "1"(the payload version) and atype. Most also have anevent. Branch on those two fields. - A device whose token Apple or Google reports as gone is deleted. It gets nothing until the app registers it again.
Ring
An incoming call. Sent to every registered device of each person rung: everyone else in the conversation when a call is placed, or the people added with "Add people". Rooms never ring.
On iOS it goes to the PushKit token only (apns-push-type: voip,
priority 10, apns-expiration: 0, topic <bundle id>.voip). On
Android it goes to the FCM token with ttl: 60s.
{
"cv": "1",
"type": "call",
"event": "ring",
"call_id": "4a69…",
"conversation_id": "6f1c…",
"conversation_name": "Engineering",
"caller_user_id": "2b7e…",
"caller_external_id": "acme-alice",
"caller_name": "Alice",
"media": "video",
"has_video": true,
"ring_deadline": 1793952045000,
"sent_at": 1793952000000,
"grant": "eyJhbGciOi…"
}
| Field | Meaning |
|---|---|
call_id | The call. |
conversation_id, conversation_name | Where it is. conversation_name is "" when the conversation has no name, for example a direct chat. |
caller_user_id, caller_external_id | Who is calling. caller_external_id is "" if unknown. |
caller_name | The caller's display_name, or "Incoming call" if they have none. |
media | "audio" or "video". |
has_video | media == "video". CallKit needs this before the user answers. |
ring_deadline | Epoch milliseconds when the ring expires. Stop ringing at this time even if no other push arrives. It is the workspace's ring time (15–120 s, default 45). |
sent_at | Epoch milliseconds when poolse sent it. PushKit can deliver a queued push late, when the app launches. Compare against ring_deadline to drop a stale ring. |
grant | The ring grant: a token bound to this call, this user and this device. It lets a locked phone answer or decline with no user session. Valid for 180 seconds. See Calls. |
The ring is sent even when the app is open and connected. The app can
also see the call through the socket, so de-duplicate on call_id.
Ring ended
Tells a ringing Android phone to stop. Sent to the user's Android devices when:
- they answer on another device (
answered_elsewhere); the device that answered is skipped; - they decline on another device (
declined); - the call ends while their phone is still ringing (
cancelled,timeoutorfinished).
{
"cv": "1",
"type": "call_event",
"event": "ring_ended",
"call_id": "4a69…",
"conversation_id": "6f1c…",
"reason": "answered_elsewhere",
"sent_at": "1793952006000"
}
reason is one of answered_elsewhere, declined, cancelled (the
caller hung up), timeout (rang out) or finished. FCM ttl: 60s.
Not sent to iOS. On iOS, a ringing phone stops at ring_deadline,
or when the app hears call:ring_ended on the
user channel.
Call finished
Tells an Android device that a call is over. A backgrounded app gets no other signal. Sent when a call or room ends, to the Android devices of everyone involved: the caller, everyone rung, and everyone who joined.
{
"cv": "1",
"type": "call_event",
"event": "finished",
"call_id": "4a69…",
"conversation_id": "6f1c…",
"reason": "finished",
"sent_at": "1793952265000"
}
reason is finished, cancelled, no_answer, declined_all or
busy_all (see Calls & rooms).
FCM ttl: 60s.
Not sent to iOS. On iOS, the app learns this from call:finished
on the user channel.
Missed call
Sent when a call ends to each person rung who never joined (at the
start, or added later with "Add people"), except people who declined.
People whose phone refused the ring
because they were on another call (busy) do get it. Rooms never
produce one. Turn it off with the dashboard's Missed-call
notifications setting (missed_calls).
Android (FCM, ttl: 86400s):
{
"cv": "1",
"type": "call_event",
"event": "missed",
"call_id": "4a69…",
"conversation_id": "6f1c…",
"conversation_name": "",
"caller_user_id": "2b7e…",
"caller_name": "Alice",
"media": "audio",
"title": "Alice",
"body": "Missed voice call",
"sent_at": "1793952045000"
}
title and body are ready to show:
- In a group,
titleis the conversation name andbodyreads "Missed video call from Alice". - Otherwise
titleis the caller's name andbodyis "Missed voice call" or "Missed video call".
conversation_name is the conversation's name, or "" if it has none.
iOS (alert push to the APNs token, expires after 24 hours,
apns-collapse-id: missed-<call_id>):
{
"aps": {
"alert": { "title": "Alice", "body": "Missed voice call" },
"sound": "default",
"category": "poolse_missed_call",
"thread-id": "6f1c…"
},
"cv": "1",
"type": "call_missed",
"event": "missed",
"call_id": "4a69…",
"conversation_id": "6f1c…",
"conversation_name": "",
"caller_user_id": "2b7e…",
"caller_name": "Alice",
"media": "audio",
"sent_at": 1793952045000,
"body": {
"type": "call_missed",
"call_id": "4a69…",
"conversation_id": "6f1c…",
"media": "audio",
"caller_user_id": "2b7e…"
}
}
On iOS type is "call_missed", not "call_event". The top-level
body object repeats the routing keys, because expo-notifications
reads a remote push's data from there. To get a Call back button,
register the poolse_missed_call category with an action. The Loop
reference app uses the identifier call_back.
New message
A chat notification, sent when a text message is sent and the
workspace has Send chat notifications on. custom and system
messages never send one.
Who gets it: every member except the sender, minus members who muted the conversation, unless the message mentions them. If the workspace's Group conversations setting is Only @mentions, a group conversation notifies only the people mentioned. Direct conversations always notify.
Android (FCM, ttl: 86400s):
{
"cv": "1",
"type": "message",
"event": "new",
"message_id": "8d0e…",
"conversation_id": "6f1c…",
"conversation_name": "Engineering",
"group": "1",
"sender_user_id": "2b7e…",
"sender_external_id": "acme-alice",
"sender_name": "Alice",
"text": "ship it",
"mentioned": "0",
"sent_at": "1793952843112",
"unread_total": "4",
"reply_grant": "eyJhbGciOi…"
}
| Field | Meaning |
|---|---|
group | "1" for a group conversation, "0" for a direct one. |
sender_name | The sender's display_name, or "Someone". |
text | The message, cut to 512 characters plus …. With no text: 📎 Attachment or 📎 N attachments. |
mentioned | "1" if this message mentions the recipient. |
unread_total | The recipient's unread total across all conversations, for the app badge. |
reply_grant | A token that lets this device reply to this conversation, or mark it read, with no user session. It is bound to the user, the device and the conversation, and valid for 3 days. See Notification actions. |
The workspace's What a notification shows setting (preview)
changes what the push carries:
| Preview | Effect |
|---|---|
full (default, "Sender and message") | Everything above. |
sender ("Sender only") | text is "New message". |
hidden ("Nothing") | text is "New message", sender_name and conversation_name are "", and private: "1" is added. |
iOS (alert push to the APNs token, apns-collapse-id: <message_id>,
expires after 24 hours). The same fields at the top level (numbers
unquoted), plus:
{
"aps": {
"alert": { "title": "Engineering", "body": "Alice: ship it" },
"sound": "default",
"badge": 4,
"thread-id": "6f1c…",
"category": "poolse_message",
"mutable-content": 1,
"summary-arg": "Alice"
},
"body": {
"type": "message",
"event": "new",
"conversation_id": "6f1c…",
"message_id": "8d0e…",
"sender_user_id": "2b7e…",
"group": "1"
}
}
The alert text depends on the preview setting and the conversation:
hidden: no title, and the body is "New message".- A group with a name: the conversation name as title, and "Sender: text" as body.
- Anything else: the sender's name as title, and the text as body.
thread-id groups a conversation's notifications. The
poolse_message category is where @poolse/native puts its Reply and
Mark as read actions.
Conversation read
Sent to the reader's own devices when their read cursor moves forward past unread messages, so notifications they read on one device clear on the others and the badge updates.
Android (FCM, ttl: 86400s):
{
"cv": "1",
"type": "message",
"event": "read",
"conversation_id": "6f1c…",
"unread_total": "0"
}
iOS (background push to the APNs token,
apns-push-type: background, priority 5):
{
"aps": { "content-available": 1 },
"cv": "1",
"type": "message_read",
"event": "read",
"conversation_id": "6f1c…",
"unread_total": 0
}
Apple may delay or drop background pushes. Treat this one as best effort.