curl -X POST https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f/tags \
-H "Authorization: Bearer $IBUBBL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Spring promo"}'
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}/tags`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Spring promo" }),
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.tags);
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}/tags",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"name": "Spring promo"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["tags"])
{
"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": false,
"opted_out_at": null,
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123Z"
}
{
"detail": {
"error": "invalid_tag",
"message": "A tag name needs 1–64 characters and no invisible characters"
}
}
Add a tag to a contact
Add one tag. Idempotent: a tag the contact already has answers 200 unchanged. The name matches an existing tag trimmed and case-insensitively; a new name creates the tag. A newly added tag can start workflows with a “Tag added” trigger.
curl -X POST https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f/tags \
-H "Authorization: Bearer $IBUBBL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Spring promo"}'
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}/tags`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Spring promo" }),
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.tags);
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}/tags",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"name": "Spring promo"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["tags"])
{
"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": false,
"opted_out_at": null,
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123Z"
}
{
"detail": {
"error": "invalid_tag",
"message": "A tag name needs 1–64 characters and no invisible characters"
}
}
Add a tag to a contact
Method:POST | Endpoint: /v1/contacts/{contact_id}/tags
Description: Add one tag to a contact. Calling it again with the same name changes nothing, so it is safe to retry. A newly added tag can start workflows with a “Tag added” trigger.
Authentication: API key in the Authorization header: Authorization: Bearer ib_pk_<64 hex>. Create keys in the dashboard under Settings → API access.
Request Body
The path parametercontact_id is the contact’s ID (a UUID).
| Field | Type | Required | Description |
|---|---|---|---|
name | string, 1–64 characters | Yes | The tag name, with no invisible characters. It matches an existing tag ignoring case and outer spaces; a new name creates the tag. |
422 validation_error, and the message names the field.
curl -X POST https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f/tags \
-H "Authorization: Bearer $IBUBBL_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Spring promo"}'
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}/tags`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "Spring promo" }),
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.tags);
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}/tags",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"name": "Spring promo"},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["tags"])
{
"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": false,
"opted_out_at": null,
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123Z"
}
{
"detail": {
"error": "invalid_tag",
"message": "A tag name needs 1–64 characters and no invisible characters"
}
}
Responses
200 OK{
"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": false,
"opted_out_at": null,
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123Z"
}
{
"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"
}
}
{
"detail": {
"error": "invalid_tag",
"message": "A tag name needs 1–64 characters and no invisible characters"
}
}
error values: validation_error for a bad shape or an unknown field (color: Extra inputs are not permitted); duplicate_tag, rare, when the name collides with an existing tag in a way iBubbl can’t resolve.
500 Internal Server Error
{
"detail": {
"error": "internal_error",
"message": "The tag was deleted while it was being added; retry"
}
}
Authorizations
Bearer API key. Format: Authorization: Bearer ib_pk_<64 hex> (70 characters). Generate keys from Dashboard → Settings → API Access.
Path Parameters
Body
Tag name, 1–64 characters with no invisible characters. Matches an existing tag trimmed and case-insensitively; a new name creates the tag.
"VIP"
Response
The contact, with its tags.
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.

