Skip to content

Messages ​

Get messages in a conversation ​

Requires Authorization.

Invoking this endpoint does not mark messages as read.

GET /v5/chat/conversation/{conversationId}/message

Query (optional):

  • pageKey — string, optional, return messages with IDs before specified value, optional
  • profile — boolean (profile=true | profile= + any other value), optional

Response:

  • lastReadTimestamp — unix timestamp of recipient's side in milliseconds or null; is not affected by your calls to mark messages as read
  • messages — array of Message
  • metadata — object
    • translate — boolean
    • hasSharedAlbums — boolean
    • isInAList — boolean
    • summary — object or null, Wingman AI conversation summary (contactDetails and summaries arrays)
  • profile — object if profile query parameter is set to true or null

Errors:

  • 403 — {"type":"urn:gr:err:unauthorized_action","title":"Action not permitted","status":403} when the conversation is no longer reachable — the other profile blocked you, or their account was deleted.

Get a single message in a conversation ​

Requires Authorization.

GET /v4/chat/conversation/{conversationId}/message/{messageId}

Response:

Send a message to a conversation ​

Requires Authorization.

Please don't use this for spam. Be civil.

When replyToMessageId is used in the HTTP API, it appears to cause a 400 Bad Request error.

See also: Send a message to a conversation via ws

POST /v4/chat/message/send

Body:

Response:

HTTP status 201. Message object.

Errors:

  • 400 — {"type":"urn:gr:err:bad_request","title":"Invalid request","status":400,"traceId":"…"} when body is not valid for type, e.g. a Giphy body whose URLs are not on GIPHY's media hosts.
  • 402 — Returns for free-tier users. Sending expiring photos is capped per day; a paid subscription lifts the cap. The body is an EntitlementLimitProblemResponse.

Unsend a message ​

Requires Authorization.

Turns a message in chat into "This message was unsent."

Repeated requests are completed without errors.

POST /v4/chat/message/unsend

Body:

  • conversationId — Conversation ID
  • messageId — string, must be sent by you

Response:

Empty.

Errors:

  • 402 — Returns for free-tier users. Unsending is gated server-side behind a paid subscription. The body is a TieredFeatureProblemResponse with featureValue = UnsentMessage.
  • 500 — Internal Error if conversation or message was not found or if it wasn't sent by you

Delete a message ​

Requires Authorization.

Deletes a message on your side. Does not delete message for other chat participant.

Repeated requests are completed without errors.

POST /v4/chat/message/delete

Body:

  • conversationId — Conversation ID
  • messageId — string, must be sent by you

Response:

Empty.

Errors:

  • 500 — Internal Error if conversation or message was not found

Send typing indicator, WIP ​

NOTE

This endpoint hasn't been researched yet

Requires Authorization.

WIP, does not seem to work.

POST /v4/chatstatus/typing

Body:

  • conversationId — Conversation ID
  • status — string, either "Typing" or "Cleared"

Response:

Empty.

Errors:

  • 403 — Action not permitted if conversation not found

React to a message ​

Requires Authorization.

There is no discovered way to undo the reaction as of yet.

Repeated requests are completed without errors.

POST /v4/chat/message/reaction

Body:

  • conversationId — Conversation ID
  • messageId — string
  • reactionType — integer, (1 is "🔥")

Response:

Empty.

Errors:

  • 500 — Internal Error if conversation or message was not found

MessageReaction ​

  • profileId — integer
  • reactionType — integer, (1 is "🔥")

Message type ​

Message type discriminator. There also appears to be a related chat1Type, which could be a legacy type.

  • "Album"
  • "AlbumContentReaction"
  • "AlbumContentReply"
  • "Audio"
  • "ExpiringAlbum"
  • "ExpiringAlbumV2"
  • "ExpiringImage"
  • "Video"
  • "Gaymoji"
  • "Generative"
  • "Giphy"
  • "Image"
  • "Location"
  • "PrivateVideo"
  • "ProfileLink"
  • "ProfilePhotoReply"
  • "Retract"
  • "RightNowRequest"
  • "Text"
  • "Unknown"
  • "NonExpiringVideo"
  • "VideoCall"

Chat1MessageType ​

Legacy message type.

  • "map"
  • "image"
  • "expiring_album"
  • "expiring_image"
  • "private_video"
  • "expiring_video"
  • "video"
  • "gaymoji"
  • "giphy"
  • "audio"
  • "video_call"
  • "video_call_v3"
  • "audio_call"
  • "text"
  • "unknown"
  • "retracted"
  • "retracted_location"
  • "album_share"
  • "album_react"
  • "album_content_reaction"
  • "album_content_reply"
  • "profile_photo_reply"

AlbumMessageBody ​

Body for "Album" message type. Includes everything from AlbumPreview and AlbumExpiration.

  • everything from AlbumPreview
  • everything from AlbumExpiration
  • coverUrl — string or null
  • ownerProfileId — number or null if album has expired or was locked
  • isViewable — boolean
  • hasVideo — boolean
  • hasPhoto — boolean
  • viewableUntil — number or null

AlbumContentReactionBody ​

Body for "AlbumContentReaction" message type. Implies "🔥" reaction as there does not appear to be any choice.

  • albumId — integer
  • ownerProfileId — integer or null if album has expired or was locked
  • albumContentId — integer
  • previewUrl — string or null if album has expired or was locked, see Signed CDN files -> Chat media
  • expiresAt — unix timestamp in milliseconds or null
  • viewable — boolean

AlbumContentReplyBody ​

Body for "AlbumContentReply" message type. Extends AlbumContentReaction.

  • everything from AlbumContentReactionBody
  • albumContentReply — string
  • contentType — string or null if album has expired or was locked

AudioMessageBody ​

Body for "Audio" message type.

VideoMessageBody ​

Body for "Video" message type. Additionally, for expiring videos viewsRemaining is set; capped at 2147483647 for "unlimited" views.

In Get messages in a conversation, a video received from the other profile has mediaId, url, contentType and expiresAt set to null and length set to 0. Get a single message in a conversation returns them and uses one of viewsRemaining.

  • mediaId — number or null
  • url — string, signed URL of the video, or null
  • fileCacheKey — string; not sent in any observed response, optional
  • contentType — string, e.g. video/mp4, or null
  • length — number, duration in milliseconds as uploaded, 0 when url is null
  • maxViews — integer as sent in SendVideoBody, or null
  • looping — boolean or null
  • viewsRemaining — integer, starts at maxViews, optional
  • expiresAt — unix timestamp in milliseconds, 15 minutes, when url stops working, or null, optional

PrivateVideoBody ​

Body for "PrivateVideo" message type. Extends Video.

GaymojiBody ​

Body for "Gaymoji" message type. Sent and received as is.

  • imageHash — string, gaymoji/ followed by the id of an item in the Gaymoji manifest, e.g. gaymoji/1_2_Yass.png. The image is at https://cdns.grindr.com/grindr/chat/<imageHash>, see Get gaymoji asset.

GiphyBody ​

Body for "Giphy" message type. Sent and received as is. The URLs point at GIPHY's media hosts (media.giphy.com, media0.giphy.com to media4.giphy.com). Send a message to a conversation answers 400 when the URLs point at any other host; it does not check that the GIF exists on GIPHY. id and the URLs are those of a GiphyGif and its renditions.

  • id — string, GIPHY GIF ID, e.g. xT0xeChUR33EsYNNRK
  • urlPath — string, URL of the animated GIF, e.g. https://media2.giphy.com/media/v1.<…>/<id>/200.gif or https://media.giphy.com/media/<id>/200.gif
  • stillPath — string, URL of a still frame of the GIF, e.g. https://media.giphy.com/media/<id>/200_s.gif
  • previewPath — string, URL of the animated GIF, may equal urlPath
  • width — integer, width of urlPath in pixels
  • height — integer, height of urlPath in pixels
  • imageHash — string, giphy/ followed by id

ImageMessageBody ​

Body for "Image" message type. Additional fields are present only for regular (non-expiring) images.

  • mediaId — number
  • width — integer or null
  • height — integer or null
  • url — string
  • imageHash — string
  • takenOnGrindr — boolean or null
  • createdAt — unix timestamp in milliseconds or null

ChatImageBody ​

"ChatImage" message body.

  • mediaId — number
  • url — string
  • expiresAt — unix timestamp in milliseconds
  • takenOnGrindr — boolean
  • createdTs — unix timestamp in milliseconds

ExpiringImageBody ​

Body for "ExpiringImage" message type. Unlike ImageMessageBody this carries no imageHash, takenOnGrindr or createdAt, and adds the expiry fields below.

To get a single-use expiring image URL, you need to call Get a single message in a conversation. After one call, the url in the message body will be replaced with null and the image will no longer be viewable.

Both the viewing time (duration) and the view budget (viewsRemaining) are assigned server-side — in practice 10 seconds and a single view. A sender only chooses whether an image expires.

  • mediaId — number, optional
  • url — string or null, single-use signed CDN URL — replaced with null once the image has been consumed, optional
  • width — integer or null, optional
  • height — integer or null, optional
  • duration — integer, how long the recipient's client shows the image once opened, in milliseconds — observed to be 10000 (10 seconds). Chosen by the server, a sender cannot set it (see SendExpiringImageBody); -1 is a sentinel meaning already expired, and the official app treats any value below 100 the same way, optional
  • viewsRemaining — integer or null, remaining views — starts at 1, as an expiring photo is allowed exactly one view. The image is viewable while this is greater than 0; chosen by the server, like duration, optional
  • expiresAt — unix timestamp in milliseconds or null, optional
  • viewed — boolean or null, optional

LocationMessageBody ​

Body for "Location" message type.

  • lat — number
  • lon — number

ProfilePhotoReplyBody ​

Body for "ProfilePhotoReply" message type.

  • imageHash — media hash of the recipient's profile photo being replied to
  • photoContentReply — string, reply text

RetractMessageBody ​

Body for "Retract" message type. Unknown, WIP.

  • targetMessageId — string

RightNowRequestMedia ​

Element of medias in RightNowRequestBody. Unrelated to RightNowMedia, which is the Right Now feed object.

  • mediaHash — string
  • isNsfw — boolean

RightNowRequestBody ​

Body for "RightNowRequest" message type. Receive-only — there is no send payload for it.

  • requestId — long integer
  • requestCreatedAt — unix timestamp in milliseconds
  • requestUpdatedAt — unix timestamp in milliseconds
  • postStatus — string or null. Opaque: the chat body carries it as a plain string, and nothing compares it against the Right Now post statuses (AWAITING_PAYMENT, AWAITING_MODERATION, ACTIVE, EXPIRED, HIDDEN), so reusing those values is a suspicion rather than a fact.
  • postId — long integer or null
  • medias — array of RightNowRequestMedia

TextMessageBody ​

Body for "Text" message type.

  • text — string

VideoCallMessageBody ​

Body for "VideoCall" message type. WIP. Only for "status" messages.

  • result — string or null, appears to have the following values: SUCCESSFUL, Duration:, Busy, BUSY, Cancelled, Declined, DECLINED, Missed, AB_Unsupported, No_Answer, UNANSWERED, Lite_Unsupport
  • videoCallDuration — number or null

UnknownMessageBody ​

Empty type.

The official client renders both unrecognized message types and retracted messages as a locally generated "system message" row. SystemMessage is therefore a client-side UI concept and never a wire value — it has no message type constant, no body type and no adapter.

Message contents ​

Message ​

  • messageId — string, appears to be a unix timestamp in milliseconds and UUIDv4 separated by :, e.g. "1774296692000:843daee8-1e93-47d6-bc7f-3d981925a393"
  • conversationId — string, see Conversation
  • senderId — number
  • timestamp — unix timestamp in milliseconds, appears to be same as in messageId
  • unsent — boolean, if this is true, body is set to null
  • reactions — array of MessageReaction
  • type — string, see Message type
  • body — object with Message contents or null
  • replyToMessage — object, see Message, or null
  • dynamic — boolean, true for Album messages and false for Text, Image, ExpiringImage, Video, Giphy and Gaymoji messages. Current bodies of dynamic messages are returned by Refresh messages
  • chat1Type — string, see Message type
  • replyPreview — unknown or null

ConversationMessagesResponse ​

  • lastReadTimestamp — unix timestamp of recipient's side in milliseconds or null; is not affected by your calls to mark messages as read
  • messages — array of Message
  • metadata — object
    • translate — boolean
    • hasSharedAlbums — boolean
    • isInAList — boolean
    • summary — object or null, Wingman AI conversation summary (contactDetails and summaries arrays)
  • profile — object if profile query parameter is set to true or null

MessageTarget ​

  • type — string, Direct, Group, HumanWingman
  • targetId — integer

SendImageBody ​

Request body for sending an "Image" message. mediaId is the only field sent — the rest of ImageMessageBody (url, imageHash, width, height, takenOnGrindr, createdAt) is filled in server-side and appears only on the message you receive back.

  • mediaId — number

SendExpiringImageBody ​

Request body for sending an "ExpiringImage" message.

A sender chooses only whether the image expires. The viewing time (duration) and the view budget (viewsRemaining) seen on ExpiringImageBody are assigned by the server and cannot be set here — the recipient gets 10 seconds and a single view.

  • mediaId — number
  • expiring — boolean, always true in the official app — setting it is what makes the image expiring

SendAudioBody ​

Request body for sending an "Audio" message. url, contentType, length and expiresAt of AudioMessageBody are server-populated.

  • mediaId — number

SendVideoBody ​

Request body for sending a "Video" message. Unlike the other media types the sender does control playback here — looping and maxViews are chosen client-side. Everything else on VideoMessageBody is server-populated.

  • mediaId — number
  • looping — boolean, whether the video plays in a loop
  • maxViews — integer, how many times the recipient can play the video, e.g. 1 or 2. Other values are accepted as well, e.g. 50; 0 leaves no plays.

SendAlbumContentReactionBody ​

Request body for sending an "AlbumContentReaction" message. ownerProfileId, previewUrl, expiresAt and viewable of AlbumContentReactionBody are server-populated.

  • albumId — integer
  • albumContentId — integer

SendAlbumContentReplyBody ​

Request body for sending an "AlbumContentReply" message. contentType, ownerProfileId, previewUrl, expiresAt and viewable of AlbumContentReplyBody are server-populated.

  • albumId — integer
  • albumContentId — integer
  • albumContentReply — string

SendMessageBody ​

What you may put in body when sending a message. This is not the same shape you get back: server-owned fields (signed urls, hashes, dimensions, timestamps, expiry counters) exist only on Message contents.

Five types send exactly what they receive — Text, Location, Gaymoji, Giphy, ProfilePhotoReply — and are reused as-is below. A Giphy body must point at GIPHY's media hosts, see GiphyBody. The media and album-reaction types send a strict subset.

Types absent from this list are receive-only: Album/ExpiringAlbum are shared through Share an album, and Retract, VideoCall, PrivateVideo, Generative, ProfileLink, NonExpiringVideo, Unknown arrive as server notifications.

SendMessageRequest ​

Additional body fields for websocket only:

  • ref — string, client-generated correlation reference; echoed back as refValue or null
  • replyToMessageId — string or null

MessageMutationRequest ​

  • conversationId — Conversation ID
  • messageId — string, must be sent by you

EntitlementLimitProblemResponse ​

RFC-7807 problem body returned on a 402 when the account has used up an allowance that a paid subscription lifts.

  • type — string, error URN — urn:gr:err:entitlement_limit, optional
  • title — string, User has reached their entitlement limits, optional
  • status — integer, 402, optional
  • traceId — string, optional

TieredFeatureProblemResponse ​

RFC-7807 problem body returned on a 402 when the requested feature is not part of the account's subscription tier.

  • type — string, error URN — urn:gr:err:tiered_feature, optional
  • title — string, optional
  • status — integer, 402, optional
  • traceId — string, optional
  • detail — string, the principal and the feature, optional
  • profileId — long integer, optional
  • featureValue — string, e.g. UnsentMessage for Unsend a message, optional

TypingIndicatorRequest ​

  • conversationId — Conversation ID
  • status — string, either "Typing" or "Cleared"

MessageReactionRequest ​

  • conversationId — Conversation ID
  • messageId — string
  • reactionType — integer, (1 is "🔥")

ExpiringAlbumBody ​

Body for "ExpiringAlbum" message type. Extends Album.

ExpiringAlbumV2Body ​

Body for "ExpiringAlbumV2" message type. For AlbumExpirationType = ONCE but might have other values if expiration settings were changed later. Extends ExpiringAlbum.

NonExpiringVideoBody ​

NOTE

This type hasn't been researched yet

Body for "NonExpiringVideo" message type. Unknown, WIP.

GenerativeBody ​

NOTE

This type hasn't been researched yet

Body for "Generative" message type. Unknown, WIP.

ProfileLinkBody ​

NOTE

This type hasn't been researched yet

Body for "ProfileLink" message type. Unknown, WIP.

Open Grind is not affiliated with Grindr in any way.