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

PushWhyiOSAndroid
RingAn incoming callVoIP push (PushKit token)FCM data message
Ring endedStop ringing—FCM data message
Call finishedA call this device knew about is over—FCM data message
Missed callYou missed a callAlert push (APNs token)FCM data message
New messageChat notificationAlert push (APNs token)FCM data message
Conversation readClear notifications read on another deviceBackground push (APNs token)FCM data message

What every push has in common:

  • Android pushes are FCM data messages with android.priority: "HIGH". There is no notification block, 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 with kind: "alert". See Devices. On iOS, numbers stay numbers.
  • Every payload has cv: "1" (the payload version) and a type. Most also have an event. 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…"
}
FieldMeaning
call_idThe call.
conversation_id, conversation_nameWhere it is. conversation_name is "" when the conversation has no name, for example a direct chat.
caller_user_id, caller_external_idWho is calling. caller_external_id is "" if unknown.
caller_nameThe caller's display_name, or "Incoming call" if they have none.
media"audio" or "video".
has_videomedia == "video". CallKit needs this before the user answers.
ring_deadlineEpoch 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_atEpoch 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.
grantThe 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, timeout or finished).
{
  "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, title is the conversation name and body reads "Missed video call from Alice".
  • Otherwise title is the caller's name and body is "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…"
}
FieldMeaning
group"1" for a group conversation, "0" for a direct one.
sender_nameThe sender's display_name, or "Someone".
textThe message, cut to 512 characters plus …. With no text: 📎 Attachment or 📎 N attachments.
mentioned"1" if this message mentions the recipient.
unread_totalThe recipient's unread total across all conversations, for the app badge.
reply_grantA 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:

PreviewEffect
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.

Push payloads · Events · poolse docs