curl -X POST https://api.ibubbl.com/v1/conversations/3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d/read \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const conversationId = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d";
const res = await fetch(`https://api.ibubbl.com/v1/conversations/${conversationId}/read`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.IBUBBL_API_KEY}` },
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.marked_count, data.read_receipt, data.read_receipt_skipped_reason);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
conversation_id = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d"
res = requests.post(
f"https://api.ibubbl.com/v1/conversations/{conversation_id}/read",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["marked_count"], data["read_receipt"], data["read_receipt_skipped_reason"])
{
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"marked_count": 2,
"read_receipt": "queued",
"read_receipt_skipped_reason": null
}
{
"detail": {
"error": "not_found",
"message": "Conversation not found"
}
}
Mark a conversation read
Mark every message you’ve received in this conversation read. It clears the unread badge in the iBubbl inbox and, when read receipts are on for your account, sends the contact a read receipt: they see “Read” under their latest message on iMessage, and on RCS (Android). SMS has no read receipts.
A receipt goes out only when there was something unread, and never to a group, a blocked contact or one who opted out. read_receipt says whether one was queued, and why not.
Idempotent: calling again with nothing new answers 200 with marked_count: 0.
Counts toward the conversation-actions rate limit (300 a minute per account).
curl -X POST https://api.ibubbl.com/v1/conversations/3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d/read \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const conversationId = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d";
const res = await fetch(`https://api.ibubbl.com/v1/conversations/${conversationId}/read`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.IBUBBL_API_KEY}` },
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.marked_count, data.read_receipt, data.read_receipt_skipped_reason);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
conversation_id = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d"
res = requests.post(
f"https://api.ibubbl.com/v1/conversations/{conversation_id}/read",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["marked_count"], data["read_receipt"], data["read_receipt_skipped_reason"])
{
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"marked_count": 2,
"read_receipt": "queued",
"read_receipt_skipped_reason": null
}
{
"detail": {
"error": "not_found",
"message": "Conversation not found"
}
}
Mark a conversation read
Method:POST | Endpoint: /v1/conversations/{conversation_id}/read
Description: Mark every message you’ve received in a conversation as read, and send the contact a read receipt: on iMessage they see Read under their latest message. It works on the whole conversation at once, because that’s how read receipts work on iMessage and RCS. See Tapbacks, read receipts and typing.
Authentication: API key in the Authorization header: Authorization: Bearer ib_pk_<64 hex>. Create keys in the dashboard under Settings → API access.
Rate limit: 300 conversation actions per 60 seconds per account, shared with tapbacks and typing. Over that, 429 rate_limited with a Retry-After header; see Rate limits.
Request Body
None. The path parameterconversation_id is the conversation’s ID (a UUID): conversation_id on every message and on every message.* webhook.
How it behaves
- The messages are marked read in iBubbl. The conversation’s unread badge clears in the dashboard too, for your whole team.
- A receipt goes out only when there’s something new.
read_receipt: "queued"means the contact will see Read within a few seconds."skipped"means no receipt goes out, andread_receipt_skipped_reasonsays why. - Never in a group, and never to a blocked or opted-out contact. The messages are still marked read in iBubbl.
- Read receipts are on by default. To turn them off for your account, email support@ibubbl.com; calls then answer
skippedwithreceipts_off. - It’s safe to call twice. A second call with nothing new answers
200withmarked_count: 0.
read_receipt_skipped_reason | Meaning |
|---|---|
nothing_unread | Nothing new to mark since the last read. |
receipts_off | Read receipts are turned off for your account. |
group | A group conversation: receipts are never sent in groups. |
contact_blocked | You blocked the contact. |
contact_opted_out | The contact opted out. |
opt_out_check_unavailable | iBubbl couldn’t check the contact’s opt-out, so it sent no receipt. Call again later if it matters. |
curl -X POST https://api.ibubbl.com/v1/conversations/3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d/read \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const conversationId = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d";
const res = await fetch(`https://api.ibubbl.com/v1/conversations/${conversationId}/read`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.IBUBBL_API_KEY}` },
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.marked_count, data.read_receipt, data.read_receipt_skipped_reason);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
conversation_id = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d"
res = requests.post(
f"https://api.ibubbl.com/v1/conversations/{conversation_id}/read",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["marked_count"], data["read_receipt"], data["read_receipt_skipped_reason"])
{
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"marked_count": 2,
"read_receipt": "queued",
"read_receipt_skipped_reason": null
}
{
"detail": {
"error": "not_found",
"message": "Conversation not found"
}
}
Responses
200 OK| Field | Description |
|---|---|
conversation_id | The conversation. |
marked_count | How many received messages this call marked read (0 if none were unread). |
read_receipt | queued: the contact will see Read. skipped: no receipt goes out. |
read_receipt_skipped_reason | Why it was skipped (table above), or null when queued. |
{
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"marked_count": 0,
"read_receipt": "skipped",
"read_receipt_skipped_reason": "nothing_unread"
}
{
"detail": {
"error": "unauthorized",
"message": "Invalid or revoked API key"
}
}
message values: Authorization Bearer token required, API key required (use Authorization: Bearer ib_pk_…), Invalid API key format, Customer account not found.
403 Forbidden
{
"detail": {
"error": "forbidden",
"message": "Account is suspended"
}
}
message is then Account is cancelled). Every API-key endpoint returns this.
404 Not Found
{
"detail": {
"error": "not_found",
"message": "Conversation not found"
}
}
{
"detail": {
"error": "rate_limited",
"message": "Too many conversation actions; wait 12s (limit 300 a minute per account)."
}
}
Retry-After header’s seconds, then send the same request again.Authorizations
Bearer API key. Format: Authorization: Bearer ib_pk_<64 hex> (70 characters). Generate keys from Dashboard → Settings → API Access.
Path Parameters
Response
Marked read.
The conversation.
How many received messages this call marked read (0 if they already were).
queued: the contact will see "Read" (iMessage, and RCS on Android). skipped: no receipt goes out; see read_receipt_skipped_reason.
queued, skipped Why no receipt: nothing_unread (nothing new since the last read), receipts_off (read receipts are off for your account), group (never sent in group conversations), contact_blocked, contact_opted_out, or opt_out_check_unavailable (the opt-out couldn't be checked, so none was sent). Null when queued.
nothing_unread, receipts_off, group, contact_blocked, contact_opted_out, opt_out_check_unavailable 
