curl -X POST https://api.ibubbl.com/v1/conversations/3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d/typing \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const conversationId = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d";
const res = await fetch(`https://api.ibubbl.com/v1/conversations/${conversationId}/typing`, {
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}`);
// "started", or "ignored" with ignored_reason where typing can't be shown.
console.log(data.status, data.ignored_reason, data.expires_at);
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}/typing",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
# "started", or "ignored" with ignored_reason where typing can't be shown.
print(data["status"], data["ignored_reason"], data["expires_at"])
{
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"typing": true,
"status": "started",
"ignored_reason": null,
"expires_at": "2026-11-11T16:04:49.293232Z"
}
{
"detail": {
"error": "contact_opted_out",
"message": "This contact has opted out."
}
}
Start typing
Show the contact the typing bubble (the three dots) in this conversation. It lasts 10 seconds: call again every 5 seconds to keep it going. Sending a message clears it on the contact’s phone; so does DELETE, or simply not calling again.
Typing shows only in a 1:1 iMessage conversation the contact has already messaged. Anywhere else the call answers 200 with status: "ignored" and an ignored_reason, and does nothing.
Typing is a signal to the contact, so it’s refused for a blocked contact or one who opted out.
Counts toward the conversation-actions rate limit (300 a minute per account); calls closer than 3 seconds apart are answered without doing anything new.
curl -X POST https://api.ibubbl.com/v1/conversations/3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d/typing \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const conversationId = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d";
const res = await fetch(`https://api.ibubbl.com/v1/conversations/${conversationId}/typing`, {
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}`);
// "started", or "ignored" with ignored_reason where typing can't be shown.
console.log(data.status, data.ignored_reason, data.expires_at);
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}/typing",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
# "started", or "ignored" with ignored_reason where typing can't be shown.
print(data["status"], data["ignored_reason"], data["expires_at"])
{
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"typing": true,
"status": "started",
"ignored_reason": null,
"expires_at": "2026-11-11T16:04:49.293232Z"
}
{
"detail": {
"error": "contact_opted_out",
"message": "This contact has opted out."
}
}
Start typing
Method:POST | Endpoint: /v1/conversations/{conversation_id}/typing
Description: Show the contact the typing bubble (the three animated dots) in an iMessage conversation, for example while your AI writes a reply. It lasts 10 seconds; call again every 5 seconds to keep it going. Sending a message clears it. See Typing while your AI thinks.
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 mark read. Over that, 429 rate_limited with a Retry-After header; see Rate limits. A refresh every 5 seconds is 12 calls a minute per conversation.
Request Body
None. The path parameterconversation_id is the conversation’s ID (a UUID): conversation_id on every message and on message.inbound.
How it behaves
- It lasts 10 seconds.
expires_atsays when it stops unless you call again. The dots appear on the phone within about a second. - Keep it going by calling every 5 seconds. Calls less than 3 seconds apart are answered
startedwithout doing anything new, and keep the earlierexpires_at. - Sending ends it. When you send a message, it replaces the dots straight away. If you stop calling instead, or call Stop typing, iBubbl stops typing, but the dots can stay on the contact’s iPhone for up to about a minute.
- iMessage, one-to-one only. Where typing can’t be shown, the call answers
200withstatus: "ignored"and does nothing:
ignored_reason | Meaning |
|---|---|
group | A group conversation. |
not_imessage | The contact is known not to use iMessage (an SMS or RCS contact). |
no_chat | The contact hasn’t messaged this line yet, so there’s no chat to type in. |
- Blocked and opted-out contacts are refused. Typing is a signal to the contact, so a blocked contact gets
422 contact_blockedand an opted-out one422 contact_opted_out. - No webhook fires for typing.
curl -X POST https://api.ibubbl.com/v1/conversations/3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d/typing \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const conversationId = "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d";
const res = await fetch(`https://api.ibubbl.com/v1/conversations/${conversationId}/typing`, {
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}`);
// "started", or "ignored" with ignored_reason where typing can't be shown.
console.log(data.status, data.ignored_reason, data.expires_at);
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}/typing",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
# "started", or "ignored" with ignored_reason where typing can't be shown.
print(data["status"], data["ignored_reason"], data["expires_at"])
{
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"typing": true,
"status": "started",
"ignored_reason": null,
"expires_at": "2026-11-11T16:04:49.293232Z"
}
{
"detail": {
"error": "contact_opted_out",
"message": "This contact has opted out."
}
}
Responses
200 OK| Field | Description |
|---|---|
conversation_id | The conversation. |
typing | true when the dots are on after this call. |
status | started (or kept going), or ignored. |
ignored_reason | group, not_imessage or no_chat when ignored; otherwise null. |
expires_at | When the dots stop unless you call again (UTC, 10 seconds after the call that started or extended them). null unless started. |
{
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"typing": false,
"status": "ignored",
"ignored_reason": "group",
"expires_at": null
}
{
"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": "contact_blocked",
"message": "This contact is blocked."
}
}
contact_blocked or contact_opted_out, in a one-to-one conversation. Nothing was shown. Group conversations are answered ignored instead.
429 Too Many Requests
{
"detail": {
"error": "rate_limited",
"message": "Too many conversation actions; wait 12s (limit 300 a minute per account)."
}
}
Retry-After header’s seconds, then call again.
503 Service Unavailable
{
"detail": {
"error": "opt_out_check_unavailable",
"message": "Couldn't check the contact's opt-out. Try again."
}
}
Authorizations
Bearer API key. Format: Authorization: Bearer ib_pk_<64 hex> (70 characters). Generate keys from Dashboard → Settings → API Access.
Path Parameters
Response
Typing started (or ignored).
The conversation.
Whether the typing bubble is on after this call.
started (or kept going), stopped, or ignored: typing can't be shown here and nothing was done; see ignored_reason.
started, stopped, ignored group (not available in group conversations), not_imessage (the contact is on SMS or RCS), no_chat (the contact hasn't messaged this line yet). Null otherwise.
group, not_imessage, no_chat When the bubble stops unless you call again (UTC), 10 seconds after the call. Null unless started.

