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_name is the stored display name, falling back to first_name last_name. null when 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) and phone (E.164). Either may be null.

  • Name
    address
    Type
    object
    Description

    street, city, state, postcode, country, plus geocoding: formatted_address, latitude, longitude, place_id, geocode_failed. null when no address field is set. Full shape only.

  • Name
    tags
    Type
    array
    Description

    Free-form tags. [] when none.

  • Name
    marketing
    Type
    object
    Description

    email and sms booleans. See Marketing consent before writing these.

  • Name
    preferred_channel
    Type
    string
    Description

    sms, email, or null (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, or blacklisted. Defaults to active.

  • Name
    source
    Type
    string
    Description

    Provenance of the record. api for 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.


GET/api/v2/customers

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_at is greater than or equal to this value, ordered by updated_at descending. An unparseable value returns 400.

  • Name
    order_by
    Type
    string
    Description

    updated_at (default, descending) or name (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

GET
/api/v2/customers
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
  }
}

POST/api/v2/customers

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:

  1. external_id, exact match, when you send one.
  2. 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 (au to AU, us to US, eu to GB, nz to 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 returns 400 on mobile.

  • 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 an address_incomplete warning. The address is geocoded; a failure still saves and adds a geocode_failed warning.

  • 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

    sms or email. 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 outside A-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

POST
/api/v2/customers
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_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 POST for an email that already exists returns 409 and 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.


GET/api/v2/customers/:id

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

GET
/api/v2/customers/cust_36cfd704-5f2a-4c1e-9b7d-2e8a1c9f4b60
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.