Set up
- Create a webhook subscription with
message.received. Addreaction.added,reaction.removed,message.edited,message.unsentandchat.typing_indicator.startedif you want those too. - Verify the signature on every delivery, store the event by
event_id, and answer2xxwithin 10 seconds. - Do your work from your own queue. If you reply from inside the webhook handler, use
ack-${event_id}as theIdempotency-Keyso a redelivered event never sends a second reply. See Examples.
GET /v1/messages?status=received&updated_after=… returns inbound messages across every chat, and GET /v1/chats/{chat_id}/messages returns one chat. An inbound message has status: "received". Both lists are ordered by updated_at, most recently updated first, so a read receipt or an edit moves an older message up; that is what makes updated_after a reliable sync cursor. To show a conversation in order, sort the page by created_at.
A text reply
A direct reply arrives asmessage.received with contact_id set, the text in text and in parts, and the chat_id to reply into:
POST /v1/chats/{chat_id}/messages. A reply from the contact lifts the pre-reply rules for that number, so your next message can carry links and files.
If the person isn’t a contact yet, Blujay creates one when their first message arrives, so contact_id is always present on a direct message.
Photos, videos and files
Media arrives asmedia parts. The file may still be processing when the event is sent, so a part can carry only type and attachment_id at first. The event is a snapshot and never changes: read GET /v1/messages/{message_id} or GET /v1/attachments/{attachment_id} again until filename, content_type, size_bytes and download_url are there. Until then the attachment endpoints return 409 ATTACHMENT_NOT_READY. There is no “attachment ready” event, so poll with a short backoff.
download_url is a path on https://api.blujay.io. Call it with your API key. Never send the key to a URL from any other host. See Media.
Voice memos arrive as audio media, and a contact card someone shares arrives as a text/vcard media part.
Messages in a group
A message in a group chat has asender object instead of contact_id:
sender.contact_id is present only when the sender is a contact in your workspace. type is phone or email. A reply from anyone in the group counts as a reply for the group’s sending rules.
To follow who is in the group, subscribe to participant.added, participant.removed and chat.group_name_updated, and read GET /v1/chats/{chat_id}/participants for the current state. See Group chats.
Reactions
A tapback is not a message. It arrives only asreaction.added or reaction.removed with the message_id it applies to, the reaction type (love, like, dislike, laugh, emphasize, question) and part_index. It never arrives as message.received, and GET /v1/messages doesn’t list it. Reactions can arrive about a minute after the person tapped.
A reaction from the contact counts as a response for the 6-text rule.
Replies to a specific message
When a contact replies to one of your messages in a thread,reply_to on the inbound message names the message and part they replied to. Read the whole thread with GET /v1/messages/{message_id}/thread. See Threaded replies.
Edits and unsends
When a contact edits or unsends an iMessage, you getmessage.edited with the new text or message.unsent with no text. Only messages up to a week old are tracked. When a message is unsent, delete its text on your side too: earlier events you stored still carry it. See Edit and unsend.
Typing
chat.typing_indicator.started and stopped tell you a contact is typing in an iMessage chat, with an expires_at to clear the indicator by. See Typing and read receipts.
What you don’t get
- Content the API has no part type for. A message Blujay stores with no text and no file, which is how iMessage app content such as polls, Apple Pay and app cards usually arrives, has an empty
partslist and notext. Stickers sent as images, voice memos and shared contact cards arrive as media. - Effects and text styles on inbound messages aren’t reported.
- Delivery and read receipts for your messages are their own events (
message.delivered,message.read), not inbound messages. - Calls aren’t part of the messaging API.
Common mistakes
Next steps
Events
Every event type with its full payload.
Examples
A complete reply bot you can run.
Send messages
Reply into the chat.
Sending rules
What a reply unlocks.