Skip to main content
Everything a contact sends to one of your phone numbers reaches you two ways: as a webhook event the moment it arrives, and on the message and chat endpoints whenever you read them. This page is the inbound half of the API in one place. Payload fields are listed in full on Events.

Set up

  1. Create a webhook subscription with message.received. Add reaction.added, reaction.removed, message.edited, message.unsent and chat.typing_indicator.started if you want those too.
  2. Verify the signature on every delivery, store the event by event_id, and answer 2xx within 10 seconds.
  3. Do your work from your own queue. If you reply from inside the webhook handler, use ack-${event_id} as the Idempotency-Key so a redelivered event never sends a second reply. See Examples.
Polling works too: 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 as message.received with contact_id set, the text in text and in parts, and the chat_id to reply into:
Reply with 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 as media 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 a sender 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 as reaction.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 get message.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 parts list and no text. 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

  • Don’t overwrite newer state with an older event. Events can arrive out of order and more than once; dedupe by event_id and compare timestamps.
  • Don’t reply to every message forever. The reply bot in Examples acknowledges once per chat, then stays quiet.
  • Don’t treat a reaction as text. It has no text and no parts.
  • Don’t follow a full URL from a payload with your API key. Build the download URL from download_url on https://api.blujay.io.

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.