Customers
Read and create customers over the API. The read shape is a deliberate subset of the stored customer record: identity, contact, address, marketing flags, and spend rollups. Staff-only data (the internal note, the timeline, search index fields, audit metadata, and custom-field values flagged internal) is never returned.
Customer endpoints require a secret key (ibp_sk_). customers:read and
customers:write are outside the publishable-key allowlist, so a publishable
key can never hold them.
The customer model
Properties
- Name
id- Type
- string
- Description
Unique identifier (
cust_+ a UUID).
- Name
external_id- Type
- string
- Description
Your own reference, or
null. Matched exactly on create and on list.
- Name
external_ids- Type
- object
- Description
Free-form map of additional external references. Full shape only.
- Name
first_name / last_name / full_name- Type
- string
- Description
full_nameis the stored display name, falling back tofirst_name last_name.nullwhen neither is set.
- Name
company- Type
- string
- Description
Company name, or
null.
- Name
contact- Type
- object
- Description
email(stored as sent, with the original case, trimmed) andphone(E.164). Either may benull.
- Name
address- Type
- object
- Description
street,city,state,postcode,country, plus geocoding:formatted_address,latitude,longitude,place_id,geocode_failed.nullwhen no address field is set. Full shape only.
- Name
tags- Type
- array
- Description
Free-form tags.
[]when none.
- Name
marketing- Type
- object
- Description
emailandsmsbooleans. See Marketing consent before writing these.
- Name
preferred_channel- Type
- string
- Description
sms,email, ornull(the account's country default applies). Full shape only.
- Name
is_blacklisted- Type
- boolean
- Description
Blacklist flag, with the reason in
blacklist_reason(full shape only).
- Name
status- Type
- string
- Description
active,inactive, orblacklisted. Defaults toactive.
- Name
source- Type
- string
- Description
Provenance of the record.
apifor customers created through this endpoint unless you send your own value.
- Name
analytics- Type
- object
- Description
Server-owned spend rollups:
total_orders,total_spent,average_order_value,first_order_date,last_order_date. Recomputed from the order set, never writable.
- Name
custom_field_values- Type
- object
- Description
Customer custom-field values, keyed by field id. Filtered on read: a value is returned only when it maps to a custom-field definition that is not flagged internal. Anything else, including values with no matching definition, is dropped. Full shape only.
- Name
created_at / updated_at- Type
- string
- Description
ISO 8601 timestamps.
The list endpoint returns a lighter shape. It omits external_ids,
address, marketing, preferred_channel, blacklist_reason, and
custom_field_values. Retrieve a customer by id for the full shape.
List all customers
Retrieve a cursor-paginated list of customers, newest-updated first. Soft-deleted customers are excluded.
Required scope: customers:read
Query parameters
- Name
limit- Type
- integer
- Description
Page size (default 50, max 100). A non-numeric value falls back to 50.
- Name
cursor- Type
- string
- Description
Opaque cursor from
pagination.next_cursor.
- Name
email- Type
- string
- Description
Exact match. The value is lowercased before matching, so case does not matter.
- Name
external_id- Type
- string
- Description
Exact, case-sensitive match on your reference.
- Name
updated_after- Type
- string
- Description
ISO 8601. Returns customers whose
updated_atis greater than or equal to this value, ordered byupdated_atdescending. An unparseable value returns400.
- Name
order_by- Type
- string
- Description
updated_at(default, descending) orname(ascending, case-insensitive). Any other value is ignored and the default order applies.
Filter precedence
At most one of these shapes is applied per request, in this order: email, then external_id, then updated_after, then order_by. Lower-precedence parameters are silently ignored rather than rejected. ?email=...&updated_after=... returns the email match with no date restriction.
order_by=name sorts on a stored lowercased name field, and a customer
stored without a name is absent from order_by=name results entirely. It
is still returned under the default updated_at order. Do not use
order_by=name to enumerate the full customer set.
Pagination
Pass pagination.next_cursor back as cursor to get the next page. A cursor that is malformed, or that points at a document that no longer exists, returns 400 with code invalid_cursor. Treat that as an error and restart the list without a cursor; it is not the end of the list.
For delta sync, pass the highest updated_at you have seen as updated_after. The comparison is inclusive, so the last record of the previous sync is returned again.
Request
curl -G https://au.api.ibakepro.com/api/v2/customers \
-H "Authorization: Bearer {secret_key}" \
-d limit=10
Response
{
"data": [
{
"id": "cust_36cfd704-5f2a-4c1e-9b7d-2e8a1c9f4b60",
"external_id": null,
"first_name": "Jane",
"last_name": "Doe",
"full_name": "Jane Doe",
"company": null,
"contact": { "email": "jane@example.com", "phone": "+61400000000" },
"tags": ["vip"],
"is_blacklisted": false,
"status": "active",
"source": "api",
"analytics": {
"total_orders": 4, "total_spent": 512.40,
"average_order_value": 128.10,
"first_order_date": "2026-01-02T00:00:00.000Z",
"last_order_date": "2026-06-01T00:00:00.000Z"
},
"created_at": "2026-01-02T09:00:00.000Z",
"updated_at": "2026-06-01T10:30:00.000Z"
}
],
"pagination": {
"limit": 10,
"has_more": true,
"next_cursor": "eyJpZCI6ImN1c3RfMzZjZmQ3MDQtNWYyYS00YzFlLTliN2QtMmU4YTFjOWY0YjYwIn0",
"prev_cursor": null
}
}
Create a customer
Create a customer. There is no update endpoint: POST is the only write, and it refuses to touch an existing record.
Required scope: customers:write
Duplicate matching
Before writing, the endpoint looks for an existing customer, in this order:
external_id, exact match, when you send one.- The lowercased
contact.email.
If either finds a customer, the request returns 409 (type: "conflict") with the message A customer with this email already exists and nothing is written, whichever key matched. Soft-deleted customers still occupy both key spaces, so an email belonging to a deleted record also returns 409.
There is no PATCH or PUT on this resource. You cannot change a name,
an address, a tag, or a marketing flag on an existing customer over the
API, and a repeated POST will not merge into one. Edits happen in the
dashboard.
This is the only endpoint that writes to the customer collection. Creating an order does not create or match a customer from customer.email: an order links to a customer only through an explicit customer.id, which must already exist in the account, and an order sent without one stores its contact details on the order alone. Create the customer first if you need the order attributed to a customer record and counted in that customer's spend rollups.
Required attributes
- Name
first_name- Type
- string
- Description
Must be non-empty after trimming.
- Name
contact.email- Type
- string
- Description
Must contain an
@and a dot-separated domain. A whitespace-only value is rejected. Stored trimmed with its original case; the match key is the lowercased form.
Optional attributes
- Name
last_name / company- Type
- string
- Description
Surname and company.
- Name
contact.phone- Type
- string
- Description
Parsed and stored as E.164. The default country comes from the key's region (
auto AU,usto US,euto GB,nzto NZ); a number that does not parse against that country is retried against AU, US, GB, and NZ. A number that is invalid for the region returns400onmobile.
- Name
address- Type
- object
- Description
street,city,state,postcode,country. Any other key is discarded. Sending some but not all of street, city, state, and postcode still saves and adds anaddress_incompletewarning. The address is geocoded; a failure still saves and adds ageocode_failedwarning.
- Name
tags- Type
- array
- Description
Up to 50 tags. More returns
400.
- Name
marketing_email / marketing_sms- Type
- boolean
- Description
Marketing consent. Read Marketing consent first.
- Name
preferred_channel- Type
- string
- Description
smsoremail. Omit to use the account's country default.
- Name
external_id- Type
- string
- Description
Your reference. Used for duplicate matching.
- Name
external_ids- Type
- object
- Description
Additional references, stored as sent.
- Name
custom_fields- Type
- object
- Description
Custom-field values keyed by field id. Max 50 keys and 10KB serialized, or the request returns
400. Keys are sanitized: a key that is reserved, starts with_, exceeds 500 characters, or contains anything outsideA-Z a-z 0-9 _ -is dropped silently. Values that map to an active option-to-tag mapping also apply those tags.
- Name
notes- Type
- string
- Description
Write-only. Stored as the internal staff note and never returned by any endpoint.
- Name
source- Type
- string
- Description
Provenance label stored on the record. Defaults to
api.
All string values are trimmed and stripped of HTML before storage, except description and notes, which keep their markup.
Response
201 with the full customer shape. When a non-fatal issue occurred the body carries a warnings array of { code, message, field }; the codes are address_incomplete and geocode_failed.
Idempotency
Send an Idempotency-Key header. A replay of a stored key returns the original response with an Idempotent-Replayed: true header. Reusing a key with a different body, or on a different route, returns 422 (idempotency_key_reused), and a retry while the first request is still running returns 409 (request_in_flight). Keys expire 24 hours after first use.
Request
curl -X POST https://au.api.ibakepro.com/api/v2/customers \
-H "Authorization: Bearer {secret_key}" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: cust-0001" \
-d '{
"first_name": "Jane",
"last_name": "Doe",
"contact": { "email": "jane@example.com", "phone": "0400000000" },
"address": {
"street": "1 George St", "city": "Sydney",
"state": "NSW", "postcode": "2000"
},
"tags": ["vip"],
"external_id": "crm-7781"
}'
Response (201)
{
"id": "cust_36cfd704-5f2a-4c1e-9b7d-2e8a1c9f4b60",
"external_id": "crm-7781",
"external_ids": {},
"first_name": "Jane",
"last_name": "Doe",
"full_name": "Jane Doe",
"company": null,
"contact": { "email": "jane@example.com", "phone": "+61400000000" },
"address": {
"street": "1 George St", "city": "Sydney", "state": "NSW",
"postcode": "2000", "country": "Australia",
"formatted_address": "1 George St, Sydney NSW 2000, Australia",
"latitude": -33.8631, "longitude": 151.2092,
"place_id": "ChIJP3Sa8ziYEmsRUKgyFmh9AQM", "geocode_failed": false
},
"tags": ["vip"],
"marketing": { "email": false, "sms": false },
"preferred_channel": null,
"is_blacklisted": false,
"blacklist_reason": null,
"status": "active",
"source": "api",
"analytics": {
"total_orders": 0, "total_spent": 0, "average_order_value": 0,
"first_order_date": null, "last_order_date": null
},
"custom_field_values": {},
"created_at": "2026-06-12T09:00:00.000Z",
"updated_at": "2026-06-12T09:00:00.000Z"
}
Response (409)
{
"error": {
"type": "conflict",
"code": "conflict",
"message": "A customer with this email already exists"
}
}
Marketing consent
marketing_email and marketing_sms are consent records, not settings, and the API restricts what can be written through them.
What sending true on create does. The flag is written to the customer, and an initial consent entry is appended to that customer's append-only consent ledger with source upsert_create and initial: true. Sending false, or omitting the field, stores the flag as false and writes no ledger entry.
What is not captured. The ledger entry records method: null, ip: null, and no actor, because the API has no visibility of how the customer opted in. There is no field on this endpoint for supplying that evidence, so ibakepro holds no record of it.
What you cannot do.
- You cannot opt a customer out over the API. There is no update endpoint, and consent changes are only accepted from the surfaces that own them: the unsubscribe link, an SMS
STOP, the preference centre, or a staff edit. - You cannot change consent on an existing customer at all. A
POSTfor an email that already exists returns409and writes nothing, including the consent flags. - You cannot re-subscribe someone who opted out. Opt-outs from an unsubscribe link, an SMS
STOP, a spam report, a carrier opt-out, or a GDPR erasure lock the address against re-subscription by staff-driven and import-driven sources, which includes this API. Only the customer opting back in themselves, or a logged dashboard override, lifts the lock.
Suppression is separate from the flag. Opt-outs are stored per address at the account level, keyed on the email address or phone number, and survive the customer record. Marketing is sent only when both the customer flag is true and the address carries no suppression. Creating a new customer with marketing_email: true for an address that is already suppressed does not lift that suppression and does not make them mailable, and a GDPR erasure suppression blocks transactional mail to that address as well.
Retrieve a customer
Retrieve a single customer by id, in the full shape. Returns 404 if the customer does not exist, is soft-deleted, or belongs to another account.
Custom-field values are filtered against the account's active definitions on every read, so a value whose definition is flagged internal, or whose definition has been removed, is absent from custom_field_values.
Required scope: customers:read
Request
curl https://au.api.ibakepro.com/api/v2/customers/cust_36cfd704-5f2a-4c1e-9b7d-2e8a1c9f4b60 \
-H "Authorization: Bearer {secret_key}"
Encoding a + in an email query value: send it as %2B (standard URL
encoding), e.g. ?email=jane%2Btest@example.com.