Public Alpha. Easel is in early development, so expect rough edges.

Easel

← API Documentation

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

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
    400 with INVALID_REQUEST_ERROR, naming phone.
  • Phone numbers can be null if 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_account is false and the dates are null for a guest.
  • email_verified is true once the customer verified their email, which is
    what creates the account. It turns false again when you change their
    email, until they verify the new one.
  • marketing_consent is true only 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 to false and
    marketing_consent_at to null.

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_verified becomes false, and they are signed out everywhere. Any
    password reset link they had stops working.
  • marketing_consent becomes false, 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.