Customers API
Required scope: customers:read (read) · customers:write (write)
If the store's plan does not include customer profiles, every customer endpoint returns 403 with code: feature_not_enabled.
List Customers
GET /api/v1/customers
Returns a paginated list of customers, most recently updated first.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number (default: 1) |
per_page | integer | Results per page (default: 25, max: 100) |
q | string | Search by name, email, phone, customer code, or loyalty card number |
updated_since | ISO 8601 | Only customers updated after this timestamp. An unparseable value is ignored. |
Response
{
"data": [
{
"id": 1,
"name": "Jane Smith",
"email": "jane@example.com",
"email_receipt_opt_in": true,
"phone": "5551234567",
"customer_code": "C10001",
"loyalty_card_number": "LC-0001",
"date_of_birth": "1990-04-12",
"age": 35,
"birthday_today": false,
"loyalty_tier": "Gold",
"loyalty_points": 2450,
"lifetime_spend": "1250.00",
"visit_count": 34,
"store_credit_balance": "15.00",
"address": {
"line1": "123 Main St",
"city": "Saskatoon",
"province": "SK",
"postal_code": "S7K 1A1",
"country": "CA"
},
"created_at": "2025-06-10T08:00:00Z",
"updated_at": "2026-03-21T16:45:00Z"
}
],
"pagination": { ... }
}
| Field | Notes |
|---|---|
lifetime_spend, store_credit_balance | Two-decimal strings |
notes | Staff notes, when present |
address | Present only when the customer has an address line 1; also may include line2 |
Fields with no value are omitted rather than sent as null.
Get a Customer
GET /api/v1/customers/:id
Returns a single customer in the same shape as the list.
Create a Customer
POST /api/v1/customers
Required scope: customers:write
Request Body
{
"customer": {
"name": "John Doe",
"email": "john@example.com",
"phone": "5559876543",
"loyalty_card_number": "LC-0042"
}
}
Writable fields: name, email, phone, loyalty_card_number, email_receipt_opt_in, notes, date_of_birth.
Duplicate check
Before creating, the API looks for an existing customer with the same phone or email. If one is found, nothing is created and the response is 200 OK:
{
"data": {
"customer": { "id": 1, "name": "Jane Smith", ... },
"duplicate": true,
"message": "Found existing customer Jane Smith."
},
"meta": { ... }
}
If you sent an email and the existing customer has none on file, it is saved onto them. An email already on file is never replaced.
To create the record anyway, send "force_create": true at the top level of the body (next to customer).
Response
201 Created with data set to the new customer. Validation failures return 422 with { "errors": [ ... ] }.
The create, duplicate and update responses return the full internal customer record (more fields, flat address columns instead of an address object), not the shape used by list and get. If you store customers in the documented shape, follow a write with GET /api/v1/customers/:id.
Update a Customer
PATCH /api/v1/customers/:id
PUT is also accepted.
Required scope: customers:write
Request Body
{
"customer": {
"email": "newemail@example.com",
"phone": "5551111111"
}
}
The writable fields are the same as for create.
Other Customer Lookups
These GET endpoints also work with customers:read. They return the full internal customer format rather than the shape above.
| Endpoint | Description |
|---|---|
GET /api/v1/customers/lookup_by_code/:code | Find a customer by customer code (format C#####; other formats return 400) |
GET /api/v1/customers/lookup_by_card/:card_number | Find a customer by loyalty card number |
GET /api/v1/customers/:id/recent_sales | The customer's last 5 completed sales |
GET /api/v1/customers/:id/loyalty | Loyalty details (requires loyalty in the store's plan) |
Earning, redeeming or adjusting loyalty points (POST /api/v1/customers/:id/loyalty/*) is not available to API keys and returns 403.