Skip to main content
Three small things make a conversation feel like it’s with a person: a ❤️ on what they said, Read under their message, and the typing dots while a reply is on its way. iBubbl sends all three from your line through /v1: message_id and conversation_id are both on every message.inbound event, so you can act on a message the moment it arrives.

Tapbacks

A tapback is one of the six iMessage reactions: ❤️ love, 👍 like, 👎 dislike, 😂 laugh, ‼️ emphasize and ❓ question.
What to know:
  • Only on messages you received. A tapback on a message you sent is 422 invalid_target. Custom emoji tapbacks can’t be sent.
  • One per message. A different type replaces your previous tapback on that message, as on an iPhone. The same type again answers 200 and sends nothing, so a retry is safe. Tapbacks a teammate added in the dashboard count as yours.
  • Accepted, then sent. The answer is 201 with status: "pending". Your line sends it within seconds, and then:
    • message.reaction.added fires with direction: "outbound" once it’s on the contact’s phone;
    • or message.reaction.failed fires with a failed_reason if it can’t be sent. A tapback not sent within 10 minutes fails as expired.
  • Not over SMS. Tapbacks go out over iMessage, and over RCS to Android contacts. If the message came in by SMS, or the conversation is going out by SMS, the answer is 422 sms_not_supported. When iBubbl can’t tell up front, the line refuses it later with failed_reason: "sms_target".
  • Same rules as a send. In a one-to-one conversation, a blocked contact gets 422 contact_blocked, an opted-out one 422 contact_opted_out. Group conversations aren’t checked for blocks or opt-outs.
To take a tapback back, remove it with its id. message.reaction.removed fires when it’s gone from the phone.

Read receipts

Mark a conversation read marks every message the contact sent you as read, and sends them a read receipt.
  • It’s for the whole conversation. Read receipts on iMessage and RCS are per conversation: the contact sees Read under their latest message, which covers everything before it. That’s why the endpoint takes a conversation_id, not a message.
  • It also clears the unread badge in the iBubbl dashboard, for your whole team. Mark a conversation read when it really has been handled, by your code or a person.
  • Not in groups. In a group conversation the messages are marked read in iBubbl, but no receipt is sent (read_receipt_skipped_reason: "group"): a receipt there would reach only one participant, in the wrong chat.
  • Not to blocked or opted-out contacts, and not when nothing is new: the answer says skipped and why.
  • Read receipts are on by default. To turn them off for your account, email support@ibubbl.com.
Don’t confuse this with message.read: that event is the contact reading your message.

Typing indicators

Start typing shows the contact the three animated dots in a one-to-one iMessage conversation. They last 10 seconds, so call again every 5 seconds to keep them going. Calls closer than 3 seconds apart are answered without doing anything new. The dots go away when you send a message. If you stop calling, or call Stop typing, iBubbl stops typing straight away, but the contact’s iPhone can keep showing the dots for up to about a minute. So start typing when you’re going to reply, not “just in case”. Where typing can’t be shown, the call answers 200 with status: "ignored" and an ignored_reason: group (group conversations), not_imessage (the contact uses SMS or RCS), or no_chat (the contact hasn’t messaged this line yet). Treat ignored as normal, not as an error. A blocked or opted-out contact gets 422.

Typing while your AI thinks

The most common use: a contact texts you, your AI takes a few seconds to write the reply, and meanwhile they see you typing.
  1. On message.inbound, mark the conversation read and start typing right away, with the event’s conversation_id.
  2. While your model generates, call Start typing again every 5 seconds.
  3. Send the reply. It replaces the dots. Stop the refresh loop.
  4. If you decide not to reply (or the model fails), stop refreshing and call Stop typing.
This builds on the receiver in Receive messages: verify the signature, de-duplicate on the event id, act only on message.inbound, and use a deterministic idempotency_key. The code below is the work that receiver hands off once those checks pass.
  • Refresh every 5 seconds, not faster: the dots last 10 seconds, and calls closer than 3 seconds apart do nothing new. 12 calls a minute per conversation keeps you far inside the rate limit.
  • Your reply still follows the usual rules. It can wait for the contact’s sending hours, or be refused (contact_opted_out, unresponsive_contact, …). The code stops typing whenever the send fails.
  • A slow model is fine. The contact sees typing for as long as you refresh. If generating takes minutes, consider a short holding reply instead.

What the contact sees

On iMessage, your read receipt shows even if the contact turned off read receipts for their own messages: that setting only controls what they send.

Webhooks

Marking a conversation read and typing fire no events. Every event above carries conversation_id. Your own tapbacks are events about your own actions: never send a message because of one (see An auto-reply that cannot loop).

Rate limits

The five endpoints share one budget: 300 requests per 60 seconds per account, across all your API keys. It’s separate from the 120 a minute for list endpoints, so typing never slows your history reads, and the other way round. Over the limit, the answer is 429 rate_limited with a Retry-After header in seconds; wait that long, then call again. A refused call doesn’t count. At one typing refresh every 5 seconds, that’s room for about 20 conversations typing at the same moment, plus the read receipts and tapbacks around them.

Next