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.

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:

  • conversationIdConversation 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:

  • conversationIdConversation 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:

  • conversationIdConversation 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:

  • conversationIdConversation 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"
  • "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"

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.

  • mediaId — number or null
  • url — string or null
  • fileCacheKey — string
  • contentType — string or null
  • length — number
  • maxViews — integer or null
  • looping — boolean or null
  • viewsRemaining — integer, optional

PrivateVideoBody

Body for "PrivateVideo" message type. Extends Video.

GaymojiBody

Body for "Gaymoji" message type.

  • imageHash — string

GiphyBody

Body for "Giphy" message type. URLs point at https://media0.giphy.com.

  • id — string
  • urlPath — full URL to gif file
  • stillPath — single frame, URL to gif file
  • previewPath — string
  • width — integer
  • height — integer
  • imageHash — string

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. Unknown, WIP.

  • imageHash — string
  • photoContentReply — string

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, unknown purpose, WIP
  • 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
  • maxViews — integer

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

  • conversationIdConversation ID
  • messageId — string, must be sent by you

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

  • conversationIdConversation ID
  • status — string, either "Typing" or "Cleared"

MessageReactionRequest

  • conversationIdConversation 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.