Customers API
Manage customers in your store. A customer is someone who has bought from you or been added to your list.
Shoppers can check out as guests, and most do. A shopper may also register an
optional storefront account with your store, which the customer object reports
read-only: see Storefront Accounts.
Endpoints
- List Customers -
GET /api/v1/customers/ - Create Customer -
POST /api/v1/customers/ - Get Customer -
GET /api/v1/customers/{customer_id}/ - Update Customer -
PUT /api/v1/customers/{customer_id}/
Customer Privacy covers data access and erasure
requests.
List Customers
Retrieve a paginated list of customers for your store, oldest first.
Endpoint: GET /api/v1/customers/
Scope Required: customers:read
Query Parameters
| Parameter | Type | Description |
|---|---|---|
after |
string | Cursor for pagination. Customer ID to return customers after (e.g., cus_d0b6fmv28q6vn14peun0) |
before |
string | Cursor for pagination. Customer ID to return customers before, for paging back |
limit |
integer | Number of customers to return (default 10, max 100) |
email |
string | Filter by customer email address, matched in any case (see Email Addresses) |
search |
string | Search customers by name or email |
external_source |
string | With external_id, find the one record imported from that reference; other filters and the cursor are ignored |
external_id |
string | Record's ID in external_source |
Every page comes oldest first, whether you page forward with after or back
with before, and a search pages the same way as the plain list. The
response reports has_more, which is true when another page lies beyond
this one in the direction you're paging. An invalid limit returns
INVALID_REQUEST_ERROR.
Request
curl -X GET "https://studio.easel.engineering/api/v1/customers/?search=john&after=cus_d0b6fmv28q6vn14peun0" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json"
Response
Status: 200 OK
{
"data": [
{
"id": "cus_d0b6fmv28q6vn14peun0",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+351912345678",
"created_at": "2023-01-01T10:00:00Z",
"updated_at": "2023-01-01T10:15:00Z",
"external_source": null,
"external_id": null,
"has_account": false,
"email_verified": false,
"email_verified_at": null,
"registered_at": null,
"marketing_consent": false,
"marketing_consent_at": null
},
{
"id": "cus_xk3mq7r41p8nt26zv9w0",
"first_name": "Jane",
"last_name": "Smith",
"email": "jane.smith@example.com",
"phone": null,
"created_at": "2023-01-02T09:00:00Z",
"updated_at": "2023-01-02T09:00:00Z",
"external_source": null,
"external_id": null,
"has_account": false,
"email_verified": false,
"email_verified_at": null,
"registered_at": null,
"marketing_consent": false,
"marketing_consent_at": null
}
],
"has_more": true
}
Error Responses
Invalid query parameter
Status: 400 Bad Request
A malformed after or before, both at once, or an invalid limit returns
INVALID_REQUEST_ERROR, with the offending parameter named in the error
message:
{
"error": "Invalid 'limit' parameter: cannot exceed 100",
"code": "INVALID_REQUEST_ERROR"
}
Invalid email filter
Status: 400 Bad Request
{
"error": "email must be a single valid email address",
"code": "INVALID_FIELD"
}
Internal server error
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Create Customer
Create a new customer in your store.
Endpoint: POST /api/v1/customers/
Scope Required: customers:write
Request Body
{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+351912345678"
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | Yes | Customer's first name |
last_name |
string | Yes | Customer's last name |
email |
string | Yes | Customer's email address, one bare address such as ada@example.com (see Email Addresses) |
phone |
string | No | Customer's phone number in international format, such as +351912345678 (see Phone Numbers) |
external_source |
string | No | System the record was imported from; requires external_id (see External References) |
external_id |
string | No | Record's ID in that system; requires external_source |
The storefront account fields (has_account, email_verified,
email_verified_at, registered_at, marketing_consent, and
marketing_consent_at) are read-only. Sending any of them, even as false or
null, returns INVALID_FIELD.
Request
curl -X POST "https://studio.easel.engineering/api/v1/customers/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-12345" \
-d '{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+351912345678"
}'
Optional Headers:
Idempotency-Key: Unique string to prevent duplicate customer creation (see Idempotency)
Response
Returns the created customer.
Status: 201 Created
{
"id": "cus_d0b6fmv28q6vn14peun0",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+351912345678",
"created_at": "2023-01-01T10:00:00Z",
"updated_at": "2023-01-01T10:00:00Z",
"external_source": null,
"external_id": null,
"has_account": false,
"email_verified": false,
"email_verified_at": null,
"registered_at": null,
"marketing_consent": false,
"marketing_consent_at": null
}
Error Responses
Invalid JSON body
Status: 400 Bad Request
{
"error": "Invalid JSON body",
"code": "INVALID_JSON"
}
Missing required field
Status: 400 Bad Request
{
"error": "First name is required",
"code": "MISSING_FIELD"
}
Read-only field
Status: 400 Bad Request
{
"error": "marketing_consent is read-only: only the customer can change it from their storefront account",
"code": "INVALID_FIELD"
}
Invalid email
Status: 400 Bad Request
An email that isn't one bare address, including a named one such as
Ada <ada@example.com>:
{
"error": "Email must be a single valid email address",
"code": "INVALID_FIELD"
}
Email already in use
Status: 409 Conflict
Another customer has the email, in any case:
{
"error": "Resource already exists",
"code": "DUPLICATE"
}
Internal server error
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Internal server error
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Get Customer
Retrieve a single customer by their public ID.
Endpoint: GET /api/v1/customers/{customer_id}/
Scope Required: customers:read
Path Parameters
| Parameter | Type | Description |
|---|---|---|
customer_id |
string | Customer public ID (e.g., cus_d0b6fmv28q6vn14peun0) |
Request
curl -X GET "https://studio.easel.engineering/api/v1/customers/cus_d0b6fmv28q6vn14peun0/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json"
Response
Status: 200 OK
{
"id": "cus_d0b6fmv28q6vn14peun0",
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+351912345678",
"created_at": "2023-01-01T10:00:00Z",
"updated_at": "2023-01-01T10:15:00Z",
"external_source": null,
"external_id": null,
"has_account": true,
"email_verified": true,
"email_verified_at": "2023-01-01T10:05:00Z",
"registered_at": "2023-01-01T10:04:00Z",
"marketing_consent": true,
"marketing_consent_at": "2023-01-01T10:04:00Z"
}
Error Responses
Missing customer ID
Status: 400 Bad Request
{
"error": "Customer ID is required",
"code": "MISSING_FIELD"
}
Customer not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Internal server error
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Update Customer
Update an existing customer's information.
Endpoint: PUT /api/v1/customers/{customer_id}/
Scope Required: customers:write
Path Parameters
| Parameter | Type | Description |
|---|---|---|
customer_id |
string | Customer public ID (e.g., cus_d0b6fmv28q6vn14peun0) |
Request Body
{
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com",
"phone": "+351912345679"
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
first_name |
string | Yes | Customer's first name |
last_name |
string | Yes | Customer's last name |
email |
string | Yes | Customer's email address, one bare address such as ada@example.com (see Email Addresses) |
phone |
string | No | Customer's phone number in international format, such as +351912345678 (see Phone Numbers) |
As on create, the storefront account fields are read-only and sending any of
them returns INVALID_FIELD. A customer with an account logs in with their
email, so changing it here changes the address they log in with. To keep
whoever holds the new address from taking the account over, the change
signs the customer out and they must verify the new address before they can
log in again: see Email Changes. Changing only the case of
the address changes nothing.
Request
curl -X PUT "https://studio.easel.engineering/api/v1/customers/cus_d0b6fmv28q6vn14peun0/" \
-H "Authorization: Bearer your_api_key_here" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: unique-request-id-67890" \
-d '{
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com",
"phone": "+351912345679"
}'
Optional Headers:
Idempotency-Key: Unique string to prevent duplicate customer updates (see Idempotency)
Response
Returns the updated customer.
Status: 200 OK
{
"id": "cus_d0b6fmv28q6vn14peun0",
"first_name": "John",
"last_name": "Smith",
"email": "john.smith@example.com",
"phone": "+351912345679",
"created_at": "2023-01-01T10:00:00Z",
"updated_at": "2023-01-01T11:30:00Z",
"external_source": null,
"external_id": null,
"has_account": false,
"email_verified": false,
"email_verified_at": null,
"registered_at": null,
"marketing_consent": false,
"marketing_consent_at": null
}
Error Responses
Missing customer ID
Status: 400 Bad Request
{
"error": "Customer ID is required",
"code": "MISSING_FIELD"
}
Invalid JSON body
Status: 400 Bad Request
{
"error": "Invalid JSON body",
"code": "INVALID_JSON"
}
Missing required field
Status: 400 Bad Request
{
"error": "First name is required",
"code": "MISSING_FIELD"
}
Read-only field
Status: 400 Bad Request
{
"error": "marketing_consent is read-only: only the customer can change it from their storefront account",
"code": "INVALID_FIELD"
}
Invalid email
Status: 400 Bad Request
An email that isn't one bare address, including a named one such as
Ada <ada@example.com>:
{
"error": "Email must be a single valid email address",
"code": "INVALID_FIELD"
}
Email already in use
Status: 409 Conflict
Another customer has the email, in any case:
{
"error": "Resource already exists",
"code": "DUPLICATE"
}
Customer not found
Status: 404 Not Found
{
"error": "Resource not found",
"code": "NOT_FOUND"
}
Internal server error
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Internal server error
Status: 500 Internal Server Error
{
"error": "Internal server error",
"code": "INTERNAL_ERROR"
}
Customer Object
Fields
| Field | Type | Description |
|---|---|---|
id |
string | Customer public ID |
first_name |
string | Customer's first name |
last_name |
string | Customer's last name |
email |
string | Customer's email address, lowercased (see Email Addresses) |
phone |
string|null | Customer's phone number in E.164 format, such as +351912345678 |
created_at |
string | ISO 8601 timestamp |
updated_at |
string | ISO 8601 timestamp |
external_source |
string|null | System the record was imported from (see External References) |
external_id |
string|null | Record's ID in that system |
has_account |
boolean | Whether the customer registered a storefront account. Read-only |
email_verified |
boolean | Whether they verified their email by following the emailed link. Read-only |
email_verified_at |
string|null | When they verified it, or null. Read-only |
registered_at |
string|null | When their account was created, or null without one. Read-only |
marketing_consent |
boolean | Whether you may send them marketing and sale email. Read-only |
marketing_consent_at |
string|null | When they opted in, or null when they have not. Read-only |
banned_at |
string|null | When the store banned their storefront account, or null. Read-only |
shadowbanned_at |
string|null | When the store shadowbanned them, or null. Read-only |
Email Addresses
Easel stores every email lowercased, with surrounding space trimmed, and treats
two addresses that differ only in case as the same customer. Creating or
updating a customer with an email another customer has, in any case, returns
409 DUPLICATE. A guest checkout with such an email attaches the order to the
existing customer, and the email filter matches in any case.
An email must be one bare address, such as ada@example.com. A named address
such as Ada <ada@example.com>, or anything else that isn't an address,
returns INVALID_FIELD.
Phone Numbers
- Send a phone number in international format, starting with
+and the
country code, such as+351912345678. Spaces and dashes are fine. - It's saved and returned in E.164 format, such as
+351912345678, so the
same number sent another way is the same phone, and an edit that only
changes its formatting changes nothing. - A number that isn't a valid phone number in international format returns
400withINVALID_REQUEST_ERROR, namingphone. - Phone numbers can be
nullif not provided
Storefront Accounts
Guest checkout is the default and never requires an account. A shopper who
wants one registers on your storefront, and the account only exists once they
follow the link emailed to them. It then attaches to the customer with that
email, so their guest orders and later ones share one history.
The account fields are read-only everywhere: no API endpoint, dashboard page,
or import can create an account, verify an email, or opt a customer in.
has_accountisfalseand the dates arenullfor a guest.email_verifiedistrueonce the customer verified their email, which is
what creates the account. It turnsfalseagain when you change their
email, until they verify the new one.marketing_consentistrueonly when the customer verified their email
and ticked the opt-in themselves, at registration or later on their email
preferences page. It is never pre-ticked. When they withdraw, from that page
or with the unsubscribe link in any sale alert, it returns tofalseand
marketing_consent_attonull.
Only send marketing or sale email to customers whose marketing_consent is
true, and check it again shortly before each send, since customers can
withdraw at any time.
Easel itself sends opted-in customers one kind of marketing email: a wishlist
sale alert, at most once a day, when items on their account wishlist go on
sale. See Wishlist Sale Alerts for what
triggers one and how to turn them off for a site.
Email Changes
When you change the email of a customer with an account, from the dashboard
or with Update Customer:
email_verifiedbecomesfalse, and they are signed out everywhere. Any
password reset link they had stops working.marketing_consentbecomesfalse, since the consent was given from the
old address. Only the customer can opt in again.- The new address is emailed a verification link. Following it asks for the
account's password, so the link alone is not enough to log in. The usual
limits on verification emails apply, and the customer can ask for a new
link from the storefront. - The old address is emailed a notice that the email was changed, so the
customer notices if someone else asked you to change it.
Until they verify the new address, the customer can't log in.
Bans and Shadowbans
From a customer's page in the dashboard, or from one of their reviews, you
can ban or shadowban their storefront account. Both are read-only here:
sending banned_at or shadowbanned_at to a create or update endpoint is
rejected with 400 Bad Request, like the other account fields.
- A ban stops the customer logging in and ends their sessions. Their
reviews leave the storefront and their products'
ratings. Their orders are unaffected. - A shadowban hides their reviews from everyone but them and leaves the
reviews out of their products' ratings. They still see their reviews as
usual, and can still shop and log in. It exists only on servers with
reviews on.
Lifting either restores the customer's reviews. Neither deletes anything.
Customer Privacy
Customers have the right to a copy of their data and to have it erased
(GDPR, CCPA). Both work the same whether the customer asks you or does it
themselves.
Data Exports
On the dashboard, a customer's page has an export that downloads their data
as JSON: their details, saved addresses, and every order. For a customer with
a storefront account, it also has the account's registration, verification,
and marketing consent dates, and their account wishlist. It lists the
reviews they wrote, with whether and why the store hid each.
Customers with an account can download the same file themselves from the
privacy page of their storefront account, up to three times a day.
Erasure
Deleting a customer on the dashboard erases their personal data. Customers
with an account can also delete it themselves from the privacy page of their
storefront account, after entering their password and confirming. Both go
through the same erasure and remove:
- their name, email, and phone number,
- their saved addresses,
- their storefront account, its sessions, and any pending verification or
password reset link, - their account wishlist, their wishlist sale alert history, and the
unsubscribe links already emailed to them, - their reviews, with the ratings of the products they reviewed updated to
match. - the gift notes on their orders and the messages on gift cards they bought,
which keep working, - their restock waitlist signups,
- the name, email, phone number, and address on any failed checkout still
holding their payment. It keeps the payment and amount so you can refund
it, but it can no longer be converted into an order.
Their orders are kept, because EU VAT law requires you to keep records of
your sales, and the GDPR allows that (Article 17(3)(b)). An invoice must name
the buyer and their address (VAT Directive, Article 226), and the billing
postcode shows where the sale was taxed, so the order's own addresses, items,
and totals stay as they were. The order no longer links to their email.
After erasure the customer no longer appears in the API:
Get Customer returns 404 Not Found and
List Customers leaves them out. A customer who deleted
their own account is treated exactly like one you deleted. Their email is free again,
so they can register later, as a new customer with no order history.
A customer who deleted their account is emailed a short confirmation. Erasure
requests from guests, or that reach you by email, are still yours to handle
on the dashboard. That includes banned customers, who can't log in to delete
their own account.
The API has no endpoint to export or erase a customer.