Push Notifications
Grindr delivers background notifications through Firebase Cloud Messaging as data-only messages: the payload is a flat map of strings, and the client decides what to render. Nothing arrives pre-rendered, so every field below is the client's responsibility.
While the app is in the foreground the same events also arrive over the WebSocket as notification events; push is the out-of-process path for when it is not.
Register a token with Register push token and stop delivery with Unregister push token.
Envelope
version selects the payload shape. Anything that is not exactly "2" — including a missing key — is the legacy V1 shape.
Version 2
Every value is a string.
version—"2"notificationId— UUIDv4, the dedupe keytitle— display text, or a text key whentranslateTitleis"true"titleArgs— JSON array of format arguments for a translated titletranslateTitle—titleis a key when this parses as true;Boolean.parseBooleanignores case, so"TRUE"counts and every other value is falsebody— display text, or a text key whentranslateBodyis"true"bodyArgs— JSON array of format arguments for a translated bodytranslateBody— astranslateTitleaction— deeplink; must start withgrindr://channel— channel the notification belongs toimageUrl— absolute CDN URL for the sender's avatar, or absentsenderId— profile ID of the sender, as a stringtimestamp— unix timestamp in millisecondsquickReply—"true"when the notification may offer an inline reply
A payload with no body, an unknown channel, or an action outside the grindr:// scheme is dropped by the official client rather than rendered.
Text keys
When the matching translate* flag is "true", the field holds a key rather than text. Keys relevant to chats and taps:
SOMEONE_TITLE— SomeoneTAP_NOTIFICATION_BODY— Tapped youCHAT_IMAGE_NOTIFICATION_BODY— Sent you a pictureCHAT_VIDEO_NOTIFICATION_BODY— Sent you a videoCHAT_AUDIO_NOTIFICATION_BODY— Sent you an audio messageCHAT_GIF_NOTIFICATION_BODY— Sent you a GIFCHAT_GAYMOJI_NOTIFICATION_BODY— Sent you a GaymojiCHAT_LOCATION_NOTIFICATION_BODY— Sent you their locationCHAT_EXPIRING_IMAGE_NOTIFICATION_BODY— Sent you an expiring imageCHAT_ALBUM_NOTIFICATION_BODY— Shared their albumCHAT_ALBUM_CONTENT_REACTION_NOTIFICATION_BODY— Liked your album contentCHAT_MESSAGE_REACTION_NOTIFICATION_BODY—<name>tapped your message, with the name as the firstbodyArgsitem
Other key families exist for A-List updates, Boost, album updates, Right Now, Discover, subscription offers, and online and activity alerts (*_ONLINE_NOTIFICATION_*, UNREAD_CHATS_NOTIFICATION_*, VIEWED_PROFILE_NOTIFICATION_*).
A direct text message carries its text in plaintext with translateBody set to "false"; only media and system messages use a key.
Actions
grindr://conversation?id=<conversationId>&senderId=<profileId>— open a conversation; both parameters are requiredgrindr://taps-inbox— tapsgrindr://viewed-me— viewed megrindr://right-now-inbox— Right Now inboxgrindr://fresh-albums?albumIds=<id,…>&albumProfileId=<profileId>— album updatesgrindr://boost?action=<action>&source=<source>— Boost resultsgrindr://favorite-profile?profileID=<profileId>— a favorite came online; the parameter is also spelledprofileIdgrindr://growth-notification?type=<type>&profileID=<profileId>— online and activity alerts;typeisfavorite_online,tapped_online,recommended_online,multiple_favorites_online,unread_chats,viewed_profileormedia_recipient_online;profileID(alsoprofileId) names the profile for the profile types
Two actions are cancel-only and must never be rendered:
grindr://clear?profileIds=<id,…>— withdraw every notification whosesenderIdis listedgrindr://unsend?notificationId=<id>— withdraw one notification
Channels
channel names an Android notification channel. A payload naming any other channel is dropped.
id_grindr_notifications_channel_individual_v2— chatsid_grindr_notifications_channel_tap_v2— tapsid_grindr_notifications_channel_alist— A-Listid_grindr_notifications_channel_boost— Boostid_grindr_notifications_channel_fresh_albums— album updatesid_grindr_notifications_channel_promotions— promotionsid_grindr_notifications_channel_subscriptions— subscriptionsid_grindr_notifications_channel_video_chat— video callsid_grindr_notifications_channel_ads— adsevent_calendar_channel_id— event calendar
Dedupe and grouping
- Two payloads sharing a
notificationIdreplace each other; the official client posts with the channel as the tag andnotificationId.hashCode()as the id - Conversation notifications are grouped by the
idof the conversation deeplink - There is no server-side dedupe: the same message can be delivered more than once, so dedupe on
notificationId
Campaign markers
pinpoint.campaign.campaign_idis present on ordinary messages too, carrying the sentinel_DIRECT. Only a value other than_DIRECTmarks a campaign — treating the key's presence as "marketing" discards every real message (observed 2026-09-17)campaignIdbehaves the same way_abmarks a Braze push, which the official client hands to the Braze SDK and never renders itselfaf-uinstall-trackingmarks an AppsFlyer uninstall probe, which is discarded
Version 1
The legacy shape dispatches on notificationType instead of an action:
chat-platform— direct message;messageholds the full Message JSON,senderDisplayNameandsenderProfileImageMediaHashsit alongside itoffline-tap-sent-event-v1,offline-tap-sent-event-v2—tapholds the tap JSONfresh-albums—payloadholds an album-update object shaped like a V2 payloadPUSH_EVENT—pushEventholds{type, title, body, silent, data}, used for Boost session ends, banned-profile cleanup and trial reminderschat— legacy, no longer rendered
Acknowledging
Acknowledge notifications reports a notification as seen with source PUSH, or WEBSOCKET when the same notification arrived over the socket instead.
Polling instead of push
A client without FCM can rebuild chat and tap notifications over REST:
- Get conversations,
POST /v4/inbox?page=<n>— the next page exists while the response carriesnextPage. Eachentries[].datacarriesconversationId,name,lastActivityTimestamp,unreadCount,muted,participants[].profileIdand apreviewof the last message withmessageId,senderId,typeandtext - Get received taps,
GET /v2/taps/received— eachprofiles[]entry carriesprofileId,displayNameandtimestamp
profileId and senderId can arrive as a number or a numeric string. The peer of a conversation is the first participant that isn't the signed-in profile.
preview holds only the last message, so several messages sent between two calls show up as one. When preview.senderId is the signed-in profile, the last message is the user's own and the unread messages are older than the preview; their content isn't in the response.
unreadCount 0 is the only sign that a conversation was read elsewhere. The inbox has no unsend signal. Once a profile blocks another, the blocked profile gets 403 urn:gr:err:unauthorized_action for that conversation's messages and the conversation is cleared.
Each case maps to a V2 payload:
- An unread conversation —
actiongrindr://conversation?id=<conversationId>&senderId=<peer>,titlename,bodypreview.text. A preview without text maps to theCHAT_*_NOTIFICATION_BODYtext key for itspreview.type, e.g.ImagetoCHAT_IMAGE_NOTIFICATION_BODYandGiphytoCHAT_GIF_NOTIFICATION_BODY - A message from Get messages in a conversation — the same payload with the message's
timestamp, andbodyfrombody.text,body.albumContentReplyorbody.photoContentReply, else the text key for itstype - A conversation read elsewhere — Grindr has no chat-scoped withdrawal:
grindr://clear?profileIds=<peer>also withdraws that profile's tap notifications. Open Grind's poll emits its owngrindr://clear?conversationId=<conversationId>instead, which the official client does not understand - A received tap —
actiongrindr://taps-inbox,titledisplayName,bodyTAP_NOTIFICATION_BODY - A shown message that Refresh messages returns with
unsent: trueor no longer returns —actiongrindr://unsend?notificationId=<id> - A shown conversation after a block — the same request answers
403urn:gr:err:unauthorized_action, and the poll emitsgrindr://clear?conversationId=<conversationId>. For notifications without a resolved message, checked with Get messages in a conversation.