curl -X PATCH https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f \
-H "Authorization: Bearer $IBUBBL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Sam",
"email": "sam.rivera@example.com",
"city": null
}'
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
first_name: "Sam",
email: "sam.rivera@example.com",
city: null, // null clears the field
}),
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.first_name, data.email, data.city);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
contact_id = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f"
res = requests.patch(
f"https://api.ibubbl.com/v1/contacts/{contact_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"first_name": "Sam",
"email": "sam.rivera@example.com",
"city": None, # None (JSON null) clears the field
},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["first_name"], data["email"], data["city"])
{
"id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
"phone": "+14155551234",
"first_name": "Sam",
"last_name": "Rivera",
"email": "sam.rivera@example.com",
"city": null,
"state": "NY",
"timezone": "America/New_York",
"tags": [
"VIP"
],
"opted_out": false,
"opted_out_at": null,
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123Z"
}
{
"detail": {
"error": "validation_error",
"message": "phone: Extra inputs are not permitted"
}
}
Update a contact
Overwrite the fields you send. Fields you leave out are not touched; a field sent as null is cleared. Unlike the upsert, this replaces values a rep set.
phone can’t be changed. Tags have their own endpoints.
curl -X PATCH https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f \
-H "Authorization: Bearer $IBUBBL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Sam",
"email": "sam.rivera@example.com",
"city": null
}'
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
first_name: "Sam",
email: "sam.rivera@example.com",
city: null, // null clears the field
}),
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.first_name, data.email, data.city);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
contact_id = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f"
res = requests.patch(
f"https://api.ibubbl.com/v1/contacts/{contact_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"first_name": "Sam",
"email": "sam.rivera@example.com",
"city": None, # None (JSON null) clears the field
},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["first_name"], data["email"], data["city"])
{
"id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
"phone": "+14155551234",
"first_name": "Sam",
"last_name": "Rivera",
"email": "sam.rivera@example.com",
"city": null,
"state": "NY",
"timezone": "America/New_York",
"tags": [
"VIP"
],
"opted_out": false,
"opted_out_at": null,
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123Z"
}
{
"detail": {
"error": "validation_error",
"message": "phone: Extra inputs are not permitted"
}
}
Update a contact
Method:PATCH | Endpoint: /v1/contacts/{contact_id}
Description: Overwrite the fields you send. Fields you leave out are not touched, and a field sent as null is cleared. Unlike Create or update a contact, this replaces values a rep set.
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). Send at least one field; an empty body changes nothing and returns the contact.
| Field | Type | Required | Description |
|---|---|---|---|
first_name | string or null, at most 128 characters | No | New first name; null clears it. |
last_name | string or null, at most 128 characters | No | New last name; null clears it. |
email | string or null, at most 254 characters | No | Must look like an email address, or 422 invalid_email; null clears it. |
city | string or null, at most 128 characters | No | Trimmed; blank or null clears it. |
state | string or null, at most 128 characters | No | Trimmed, a 2-letter code upper-cased; blank or null clears it. |
timezone | string or null | No | One of the supported IANA zones, or 422 invalid_timezone. null makes it automatic again: iBubbl infers the zone from a US or Canadian area code. |
422 validation_error, and the message names the field. phone can’t be changed, and tags have their own endpoints: Add a tag and Remove a tag.
curl -X PATCH https://api.ibubbl.com/v1/contacts/c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f \
-H "Authorization: Bearer $IBUBBL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"first_name": "Sam",
"email": "sam.rivera@example.com",
"city": null
}'
const contactId = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f";
const res = await fetch(`https://api.ibubbl.com/v1/contacts/${contactId}`, {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.IBUBBL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
first_name: "Sam",
email: "sam.rivera@example.com",
city: null, // null clears the field
}),
signal: AbortSignal.timeout(10_000),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.detail?.error}`);
console.log(data.first_name, data.email, data.city);
import os
import requests
API_KEY = os.environ["IBUBBL_API_KEY"]
contact_id = "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f"
res = requests.patch(
f"https://api.ibubbl.com/v1/contacts/{contact_id}",
headers={"Authorization": f"Bearer {API_KEY}"},
json={
"first_name": "Sam",
"email": "sam.rivera@example.com",
"city": None, # None (JSON null) clears the field
},
timeout=10,
)
data = res.json()
if not res.ok:
raise RuntimeError(f"{res.status_code} {data['detail']['error']}")
print(data["first_name"], data["email"], data["city"])
{
"id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
"phone": "+14155551234",
"first_name": "Sam",
"last_name": "Rivera",
"email": "sam.rivera@example.com",
"city": null,
"state": "NY",
"timezone": "America/New_York",
"tags": [
"VIP"
],
"opted_out": false,
"opted_out_at": null,
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123Z"
}
{
"detail": {
"error": "validation_error",
"message": "phone: Extra inputs are not permitted"
}
}
Responses
200 OK{
"id": "c8a1f4e2-3b9d-4f7c-a8e5-1d2c3b4a5e6f",
"phone": "+14155551234",
"first_name": "Sam",
"last_name": "Rivera",
"email": "sam.rivera@example.com",
"city": null,
"state": "NY",
"timezone": "America/New_York",
"tags": [
"VIP"
],
"opted_out": false,
"opted_out_at": null,
"blocked": false,
"blocked_at": null,
"created_at": "2026-11-05T14:23:45.123Z"
}
first_name and email were replaced, city was cleared, and every other field kept its value.
401 Unauthorized
{
"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": "validation_error",
"message": "phone: Extra inputs are not permitted"
}
}
error values: validation_error for any other unknown field (including tags) or a value too long; invalid_email ('sam' is not an email address); invalid_timezone.Authorizations
Bearer API key. Format: Authorization: Bearer ib_pk_<64 hex> (70 characters). Generate keys from Dashboard → Settings → API Access.
Path Parameters
Body
Only the fields you send change. Send a field as null to clear it. phone can't be changed, and tags have their own endpoints. Fields not listed here are rejected with 422 validation_error.
128"Sam"
128"Rivera"
Must look like an email address, or 422 invalid_email.
254"sam@example.com"
Trimmed; blank clears it.
128"Brooklyn"
Trimmed, a 2-letter code upper-cased; blank clears it.
128"NY"
One of the supported IANA zones, or 422 invalid_timezone. null returns the contact to automatic: iBubbl infers the zone from a US or Canadian area code again.
"America/New_York"
Response
The updated contact.
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.

