curl -X DELETE https://api.ibubbl.com/v1/messages/8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b/reactions/7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const messageId = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b";
const reactionId = "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d";
const res = await fetch(
`https://api.ibubbl.com/v1/messages/${messageId}/reactions/${reactionId}`,
{
method: "DELETE",
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.status, data.removed_at);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
message_id = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b"
reaction_id = "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
res = requests.delete(
f"https://api.ibubbl.com/v1/messages/{message_id}/reactions/{reaction_id}",
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["status"], data["removed_at"])
{
"id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"message_id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"type": "love",
"emoji": "❤️",
"direction": "outbound",
"status": "removed",
"failed_reason": null,
"created_at": "2026-11-11T16:03:25.403057Z",
"removed_at": "2026-11-11T16:04:04.236980Z"
}
{
"detail": {
"error": "not_found",
"message": "Reaction not found"
}
}
Remove a tapback
Take back a tapback you sent. It’s removed from the contact’s phone, and message.reaction.removed fires when that happens. Only a tapback from you, on this message, can be removed.
Idempotent: removing one already removed (or replaced, or failed) answers 200 with it unchanged.
Counts toward the conversation-actions rate limit (300 a minute per account).
curl -X DELETE https://api.ibubbl.com/v1/messages/8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b/reactions/7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const messageId = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b";
const reactionId = "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d";
const res = await fetch(
`https://api.ibubbl.com/v1/messages/${messageId}/reactions/${reactionId}`,
{
method: "DELETE",
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.status, data.removed_at);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
message_id = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b"
reaction_id = "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
res = requests.delete(
f"https://api.ibubbl.com/v1/messages/{message_id}/reactions/{reaction_id}",
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["status"], data["removed_at"])
{
"id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"message_id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"type": "love",
"emoji": "❤️",
"direction": "outbound",
"status": "removed",
"failed_reason": null,
"created_at": "2026-11-11T16:03:25.403057Z",
"removed_at": "2026-11-11T16:04:04.236980Z"
}
{
"detail": {
"error": "not_found",
"message": "Reaction not found"
}
}
Remove a tapback
Method:DELETE | Endpoint: /v1/messages/{message_id}/reactions/{reaction_id}
Description: Take back a tapback you added. It disappears from the message on the contact’s phone, and message.reaction.removed fires when it does.
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, mark read and typing. Over that, 429 rate_limited with a Retry-After header; see Rate limits.
Request Body
None. The path parameters are the message’smessage_id and the reaction’s id (reaction_id), both UUIDs.
How it behaves
- Only your own tapback, on that message. A contact’s reaction, or a reaction on another message, answers
404 not_found. - The answer comes first. It returns
200withstatus: "removed"at once.message.reaction.removedfires when the phone confirms. If your line can’t remove it,message.reaction.failedfires withaction: "remove"andfailed_reason: "retract_rejected", and the tapback may still show on the phone. - A tapback still
pendingwon’t be sent once you remove it. - It’s safe to call twice. Removing a tapback that’s already removed, replaced or failed answers
200with it unchanged. - Opt-outs don’t stop it. You can remove a tapback after the contact opted out.
curl -X DELETE https://api.ibubbl.com/v1/messages/8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b/reactions/7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const messageId = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b";
const reactionId = "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d";
const res = await fetch(
`https://api.ibubbl.com/v1/messages/${messageId}/reactions/${reactionId}`,
{
method: "DELETE",
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.status, data.removed_at);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
message_id = "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b"
reaction_id = "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d"
res = requests.delete(
f"https://api.ibubbl.com/v1/messages/{message_id}/reactions/{reaction_id}",
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["status"], data["removed_at"])
{
"id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"message_id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"type": "love",
"emoji": "❤️",
"direction": "outbound",
"status": "removed",
"failed_reason": null,
"created_at": "2026-11-11T16:03:25.403057Z",
"removed_at": "2026-11-11T16:04:04.236980Z"
}
{
"detail": {
"error": "not_found",
"message": "Reaction not found"
}
}
Responses
200 OK The reaction, nowremoved (the same object as Add a tapback returns). A tapback that had already failed keeps status: "failed".
{
"id": "7a8b9c0d-1e2f-3a4b-5c6d-7e8f9a0b1c2d",
"message_id": "8b3a4e72-1c2d-4e5f-9a0b-2c3d4e5f6a7b",
"conversation_id": "3a4b5c6d-7e8f-9a0b-1c2d-3e4f5a6b7c8d",
"type": "love",
"emoji": "❤️",
"direction": "outbound",
"status": "removed",
"failed_reason": null,
"created_at": "2026-11-11T16:03:25.403057Z",
"removed_at": "2026-11-11T16:04:04.236980Z"
}
{
"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": "Reaction not found"
}
}
{
"detail": {
"error": "target_unsent",
"message": "The contact unsent this message."
}
}
target_unsent or target_partly_unsent: the contact unsent the message (or part of it) after you reacted, so the tapback can’t be changed any more.
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 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.
Response
The reaction, now removed.
Reaction ID. The reaction_id in message.reaction.* events.
The message the tapback is on.
The conversation that message is in.
love, like, dislike, laugh, emphasize or question.
The tapback as an emoji, e.g. ❤️.
Always outbound: a tapback you sent.
outbound "outbound"pending: accepted, not yet on the contact's phone. sent: delivered to Messages (message.reaction.added fires then). failed: never sent, see failed_reason (message.reaction.failed fires). removed: you removed it, or a newer tapback of yours on the same message replaced it.
pending, sent, failed, removed Why a failed tapback wasn't sent: sms_target, target_not_found, bb_rejected, messages_error, expired, retract_rejected or target_unsent. Null otherwise.
When you sent the tapback (UTC).
When it was removed or replaced (UTC); null while active.

