Skip to main content

Customer Self Service API

Overview

Every endpoint on this page acts on the customer making the request. There is no customer id anywhere in the URL — the account is resolved from the credential attached to the request and nothing else. That is the single difference that matters between this page and the admin endpoints in the Customer API, which name their target in the path (/api/customers/:customer_id/...) and require an admin token.

Self service (this page)Admin (customer.md)
Path/api/customers/me/.../api/customers/:customer_id/...
route.json accesspublicprivate
CredentialCustomer JWT or storefront session cookieAdmin JWT or admin session cookie
Who is editedOnly the callerAny customer named in the path
Protected fieldsStripped from the payload before the writeWritable
access: "public" does not mean unauthenticated

The public flag in route.json only tells the global admin auth middleware to stand down. Each handler then calls request.getCurrentCustomer() itself and answers 401 when there is no customer. A request with no credential is rejected — it is just rejected by the handler rather than by the router.

Authentication

Two credentials are accepted, checked in this order by the customer module's global middleware:

  1. Customer JWTAuthorization: Bearer <token>, where the token was issued by POST /api/customer/tokens (see the Authentication API). The middleware decodes the token first and ignores it unless its tokenType is customer, so an admin token falls through and leaves the request anonymous here.
  2. Storefront session cookie — the signed session cookie written when a customer logs in through the storefront. Its name comes from system.session.cookieName and defaults to sid. The session row is read straight from the database and the customer must still have status = 1.

Whichever path matched, the resolved account is what getCurrentCustomer() returns, and the handlers never read a customer id from the URL or the request body.

curl -X PATCH \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <customer JWT>" \
--data-raw '{"full_name":"John A. Smith"}' \
https://<your domain>/api/customers/me

Endpoints

Update My Profile

Updates the calling customer's own record. Send only the fields you want to change.

The payload schema declares full_name and email but sets additionalProperties: true, so extension columns flow through untouched. Three fields are deleted from the body before the write no matter what you send:

Stripped fieldWhy
passwordChanging it goes through POST /api/customers/password, which verifies a reset token.
group_idSelf-promotion into another customer or pricing group.
statusRe-activating a disabled account.

If those are the only fields you sent, the request fails with 400 and There is nothing to update. Changing email to an address already owned by another customer fails with 400 and Email is already used.

Request Schema (application/json)
Field NameField TypeRequired
full_namestringNo
emailstringNo
PATCH/api/customers/me
curl
-H "Accept: application/json"
--data-raw "<JSON DATA>"
https://<your domain>/api/customers/me
Sample Of Response
{
"data": {
"customer_id": 21,
"uuid": "433ba97f-8be7-4be9-be3f-a9f341f2b89f",
"status": 1,
"group_id": 1,
"email": "john.smith@example.com",
"full_name": "John A. Smith",
"created_at": "2025-02-07T14:18:05.000Z",
"updated_at": "2025-02-07T14:22:41.000Z",
"links": [
{
"rel": "self",
"href": "/api/customers/me",
"action": "PATCH",
"types": [
"application/json"
]
}
]
}
}

The password column is removed from the returned row.


Create My Address

Adds an address to the calling customer's address book. customer_id is taken from the authenticated context; customer_id, customer_address_id, uuid and address_id are deleted from the body before the insert, so an attacker cannot graft an address onto another account.

Setting is_default to true clears the flag on every other address belonging to the same customer, in the same transaction.

Request Schema (application/json)
Field NameField TypeRequired
full_namestringYes
telephonestringNo
address_1stringYes
address_2stringNo
citystringNo
provincestringYes
countrystringYes
postcodestringYes
is_defaultbooleanNo
POST/api/customers/me/addresses
curl
-H "Accept: application/json"
--data-raw "<JSON DATA>"
https://<your domain>/api/customers/me/addresses
Sample Of Response
{
"data": {
"customer_address_id": 42,
"uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"customer_id": 21,
"full_name": "John Smith",
"telephone": "+1 555 0100",
"address_1": "123 Main St",
"address_2": null,
"postcode": "10001",
"city": "New York",
"province": "US-NY",
"country": "US",
"is_default": true,
"created_at": "2025-02-07T14:18:05.000Z",
"updated_at": "2025-02-07T14:18:05.000Z",
"links": [
{
"rel": "edit",
"href": "/api/customers/me/addresses/a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"action": "UPDATE",
"types": [
"application/json"
]
},
{
"rel": "delete",
"href": "/api/customers/me/addresses/a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"action": "DELETE",
"types": [
"application/json"
]
}
]
}
}
action: "UPDATE" is not a typo you should copy

The edit link reports "action": "UPDATE". The endpoint is a PATCH. Follow the href, not the action.


Update My Address

Updates one address that belongs to the calling customer. {address_id} is the address uuid.

The handler loads the row with uuid = {address_id} AND customer_id = <current customer> before writing anything. An address that exists but belongs to someone else is indistinguishable from one that does not exist: both answer 400 with Invalid address. The same four ownership columns (customer_id, customer_address_id, uuid, address_id) are stripped from the body.

A partial patch is safe: the service merges your fields over the stored row and validates the merged address, so sending only city does not trip the "Full name is required" rule. Setting is_default to true here also clears the flag on the customer's other addresses.

Request Schema (application/json)
Field NameField TypeRequired
full_namestringNo
telephonestringNo
address_1stringNo
address_2stringNo
citystringNo
provincestringNo
countrystringNo
postcodestringNo
is_defaultbooleanNo
PATCH/api/customers/me/addresses/a1b2c3d4-e5f6-4890-abcd-ef1234567890
curl
-H "Accept: application/json"
--data-raw "<JSON DATA>"
https://<your domain>/api/customers/me/addresses/a1b2c3d4-e5f6-4890-abcd-ef1234567890
Sample Of Response
{
"data": {
"customer_address_id": 42,
"uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"customer_id": 21,
"full_name": "John A. Smith",
"telephone": "+1 555 0100",
"address_1": "456 Oak Ave",
"address_2": null,
"postcode": "10001",
"city": "New York",
"province": "US-NY",
"country": "US",
"is_default": true,
"created_at": "2025-02-07T14:18:05.000Z",
"updated_at": "2025-02-07T15:02:19.000Z",
"links": [
{
"rel": "edit",
"href": "/api/customers/me/addresses/a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"action": "UPDATE",
"types": [
"application/json"
]
},
{
"rel": "delete",
"href": "/api/customers/me/addresses/a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"action": "DELETE",
"types": [
"application/json"
]
}
]
}
}

Delete My Address

Permanently removes one address that belongs to the calling customer. {address_id} is the address uuid, and the same ownership check applies. The deleted row is echoed back — with no links array, unlike the create and update responses.

Request Schema (application/json)
No request body required
DELETE/api/customers/me/addresses/a1b2c3d4-e5f6-4890-abcd-ef1234567890
curl
-H "Accept: application/json"
https://<your domain>/api/customers/me/addresses/a1b2c3d4-e5f6-4890-abcd-ef1234567890
Sample Of Response
{
"data": {
"customer_address_id": 42,
"uuid": "a1b2c3d4-e5f6-4890-abcd-ef1234567890",
"customer_id": 21,
"full_name": "John A. Smith",
"telephone": "+1 555 0100",
"address_1": "456 Oak Ave",
"address_2": null,
"postcode": "10001",
"city": "New York",
"province": "US-NY",
"country": "US",
"is_default": true,
"created_at": "2025-02-07T14:18:05.000Z",
"updated_at": "2025-02-07T15:02:19.000Z"
}
}

Address Validation

The three address endpoints have no payloadSchema.json. Nothing is validated by AJV at the router level — validation happens inside the createCustomerAddress / updateCustomerAddress services, driven by the shared addressValidator rule set:

RuleMessage
full_name present and not blankFull name is required
address_1 present and not blankAddress is required
province present and not blankProvince is required
country present and not blankCountry is required
postcode present and not blankPostcode is required

Extensions add to this list from bootstrap.ts with addAddressValidationRule(...), so a live store may enforce more than the five rules above.

Validation failures come back as 500, not 400

A failed address rule is thrown by the service and caught by the generic catch in the handler, which answers 500 with the joined rule messages as the error text (for example Full name is required, Postcode is required). Only the ownership and authentication checks produce 400 / 401. Treat a 500 from these endpoints as "possibly your payload" rather than "the server is broken".

Error Responses

StatusWhen
401No customer JWT and no valid storefront session. Message: You must be logged in to update your profile or You must be logged in to manage your addresses.
400Nothing left to update, duplicate email, or an address uuid that is not owned by the caller.
500Address validation failure, or an unexpected error. The service message is passed through verbatim.

All errors use the standard envelope:

{
"error": {
"status": 400,
"message": "Invalid address"
}
}

Reading Your Own Data

There is no GET /api/customers/me. Read the current customer, their address book, and their orders through GraphQL — the storefront schema exposes them on the authenticated currentCustomer field. See the data fetching documentation.