curl -X POST https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f/opt-out \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}/opt-out`, {
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.opted_out, data.opted_out_at);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
contact_id = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f"
res = requests.post(
f"https://api.ibubbl.com/v1/contacts/{contact_id}/opt-out",
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["opted_out"], data["opted_out_at"])
{
"id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
"phone": "+14155551234",
"first_name": "Sam",
"last_name": "Rivera",
"email": "sam@example.com",
"city": "Brooklyn",
"state": "NY",
"timezone": "America/New_York",
"tags": [
"Spring promo",
"VIP"
],
"opted_out": true,
"opted_out_at": "2026-11-11T09:12:03.418+00:00",
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123+00:00"
}
{
"detail": {
"error": "not_found",
"message": "Contact not found"
}
}
Opt a contact out
Record that this person opted out of messages, as a STOP reply does. This can’t be undone, through the API or the dashboard: every later send to this number answers 422 contact_opted_out, even after the contact is deleted and created again.
The first opt-out fires a contact.opted_out webhook with source: "api". New sends are refused from then on, but a message accepted earlier and still waiting (held for sending hours, or queued) can still go out; cancel it in the dashboard to stop it.
Idempotent: a contact already opted out (in any way) answers 200 with its current state, and no second webhook fires.
curl -X POST https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f/opt-out \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}/opt-out`, {
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.opted_out, data.opted_out_at);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
contact_id = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f"
res = requests.post(
f"https://api.ibubbl.com/v1/contacts/{contact_id}/opt-out",
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["opted_out"], data["opted_out_at"])
{
"id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
"phone": "+14155551234",
"first_name": "Sam",
"last_name": "Rivera",
"email": "sam@example.com",
"city": "Brooklyn",
"state": "NY",
"timezone": "America/New_York",
"tags": [
"Spring promo",
"VIP"
],
"opted_out": true,
"opted_out_at": "2026-11-11T09:12:03.418+00:00",
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123+00:00"
}
{
"detail": {
"error": "not_found",
"message": "Contact not found"
}
}
Opt a contact out
Method:POST | Endpoint: /v1/contacts/{contact_id}/opt-out
Description: Record that this person doesn’t want your messages, the same as when they reply STOP. Use it to bring opt-outs from your CRM or another channel into iBubbl.
422 contact_opted_out, from the API, the dashboard and automations alike. It belongs to the phone number: deleting the contact and creating it again doesn’t clear it. Only call this for a person who asked not to be messaged.Authorization header: Authorization: Bearer ib_pk_<64 hex>. Create keys in the dashboard under Settings → API access.
Request Body
None. The path parametercontact_id is the contact’s ID (a UUID).
What happens
opted_outbecomestrueandopted_out_atis set.- The first opt-out for this person fires a
contact.opted_outwebhook withsource: "api". - It’s safe to call twice. If the person has already opted out, in any way and under any form of their number (a STOP reply, a rep, GoHighLevel Do Not Disturb, or an earlier call), you get
200with the contact as it is, and no second webhook fires. - Messages already accepted can still go out. New sends are refused from now on, but a message accepted earlier and still waiting (held for sending hours, or queued) is not withdrawn. Cancel it in the dashboard if it must not be sent. See Suppression & Opt-Outs.
curl -X POST https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f/opt-out \
-H "Authorization: Bearer $IBUBBL_API_KEY"
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}/opt-out`, {
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.opted_out, data.opted_out_at);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
contact_id = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f"
res = requests.post(
f"https://api.ibubbl.com/v1/contacts/{contact_id}/opt-out",
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["opted_out"], data["opted_out_at"])
{
"id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
"phone": "+14155551234",
"first_name": "Sam",
"last_name": "Rivera",
"email": "sam@example.com",
"city": "Brooklyn",
"state": "NY",
"timezone": "America/New_York",
"tags": [
"Spring promo",
"VIP"
],
"opted_out": true,
"opted_out_at": "2026-11-11T09:12:03.418+00:00",
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123+00:00"
}
{
"detail": {
"error": "not_found",
"message": "Contact not found"
}
}
Responses
200 OK The contact, withopted_out: true. On a repeat call, opted_out_at is the time of the first opt-out.
{
"id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
"phone": "+14155551234",
"first_name": "Sam",
"last_name": "Rivera",
"email": "sam@example.com",
"city": "Brooklyn",
"state": "NY",
"timezone": "America/New_York",
"tags": [
"Spring promo",
"VIP"
],
"opted_out": true,
"opted_out_at": "2026-11-11T09:12:03.418+00:00",
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123+00:00"
}
{
"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": "Contact not found"
}
}
Authorizations
Bearer API key. Format: Authorization: Bearer ib_pk_<64 hex> (70 characters). Generate keys from Dashboard → Settings → API Access.
Path Parameters
Response
The contact, now opted out.
Unique contact identifier (UUID).
Contact's phone number as first seen. Usually E.164; a contact first created by an inbound SMS can carry the sender's bare national form (e.g. 4155551234). Upserting any form of the same number returns this contact.
True if this person has opted out: a STOP reply, a rep in the dashboard, a Do Not Disturb in your connected GoHighLevel account, or POST /v1/contacts/{id}/opt-out. When true, every send to this contact returns 422 contact_opted_out. An opt-out can't be undone, and it belongs to the number: a contact deleted and created again is still opted out.
ISO 8601 UTC timestamp the contact was created.
IANA timezone used for this contact's sending hours, or null if not set yet. When it is null and the number is a US or Canadian one, iBubbl infers the zone from the area code and saves it here when the contact's first message is processed; otherwise your account's timezone is used. Once set, an upsert never replaces it, so pass timezone on the first upsert, before you send, or change it with PATCH.
The contact's tag names, sorted case-insensitively.
ISO 8601 UTC timestamp of the opt-out, or null.
True if you blocked this contact (dashboard or POST /v1/contacts/{id}/block). Sends to them return 422 contact_blocked until you unblock them. Unlike an opt-out, a block is yours to undo, and it fires no webhook.
ISO 8601 UTC timestamp of the block, or null.

