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 JWT — Authorization: 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.

The body uses the shared address vocabulary (the same column names as cart and order addresses). Which fields are required depends on the country — see Address Validation below.

Request Schema (application/json)
Field NameField TypeRequired
recipientstring or nullNo
given_namestring or nullNo
family_namestring or nullNo
organizationstring or nullNo
address_line_1string or nullNo
address_line_2string or nullNo
address_line_3string or nullNo
dependent_localitystring or nullNo
localitystring or nullNo
administrative_areastring or nullNo
postal_codestring or nullNo
sorting_codestring or nullNo
countrystringYes
telephonestring or nullNo
extraobject or nullNo
is_defaultboolean or number or string or nullNo
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,
"recipient": "John Smith",
"given_name": null,
"family_name": null,
"organization": null,
"address_line_1": "123 Main St",
"address_line_2": null,
"address_line_3": null,
"dependent_locality": null,
"locality": "New York",
"administrative_area": "US-NY",
"postal_code": "10001",
"sorting_code": null,
"country": "US",
"telephone": "+15550100",
"extra": {},
"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 against the country's schema, so sending only locality does not trip a required-field rule. Country-scoped extra fields of the stored row survive a patch that omits country. Setting is_default to true here also clears the flag on the customer's other addresses.

Request Schema (application/json)
Field NameField TypeRequired
recipientstring or nullNo
given_namestring or nullNo
family_namestring or nullNo
organizationstring or nullNo
address_line_1string or nullNo
address_line_2string or nullNo
address_line_3string or nullNo
dependent_localitystring or nullNo
localitystring or nullNo
administrative_areastring or nullNo
postal_codestring or nullNo
sorting_codestring or nullNo
countrystringNo
telephonestring or nullNo
extraobject or nullNo
is_defaultboolean or number or string or nullNo
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,
"recipient": "John A. Smith",
"given_name": null,
"family_name": null,
"organization": null,
"address_line_1": "456 Oak Ave",
"address_line_2": null,
"address_line_3": null,
"dependent_locality": null,
"locality": "New York",
"administrative_area": "US-NY",
"postal_code": "10001",
"sorting_code": null,
"country": "US",
"telephone": "+15550100",
"extra": {},
"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,
"recipient": "John A. Smith",
"given_name": null,
"family_name": null,
"organization": null,
"address_line_1": "456 Oak Ave",
"address_line_2": null,
"address_line_3": null,
"dependent_locality": null,
"locality": "New York",
"administrative_area": "US-NY",
"postal_code": "10001",
"sorting_code": null,
"country": "US",
"telephone": "+15550100",
"extra": {},
"is_default": true,
"created_at": "2025-02-07T14:18:05.000Z",
"updated_at": "2025-02-07T15:02:19.000Z"
}
}

Address Validation​

The payload schemas above only check the shape of the keys and require country. Everything else is validated by the createCustomerAddress / updateCustomerAddress services against the country's address schema — the same addressSchema(country) the storefront form renders from — after the input has been normalized (trimmed, country upper-cased, telephone converted to E.164 with the country's dial code).

CheckcodeExample message
A key that is neither a shared column nor a registered extra fieldunknown_fieldprovince is not a known address field
A field the country requires (or the store requires through the address settings) is emptyrequiredZIP code is required
A value does not match the country's pattern (postal code, telephone, an extra field's pattern)patternZIP code is not valid
A region key the country's provider does not list as active, at any enumerated levelregion_invalidState is not a valid region
A value of the wrong JSON typetypeCompany has the wrong type
A country outside the merchant's sell-to listcountry_not_allowedWe do not sell to Narnia
A rule added by an extension with addAddressValidationRulethe rule's own codethe rule's own message

Messages are translated into the request locale with the field's label interpolated, so a storefront can show them next to the input.

Failures answer 400 with every failing field at once:

{
"error": {
"status": 400,
"message": "Invalid address",
"errors": [
{ "field": "postal_code", "code": "pattern", "message": "ZIP code is not valid" },
{ "field": "telephone", "code": "required", "message": "Telephone is required" }
]
}
}

Extensions add rules from bootstrap.ts with addAddressValidationRule; the rules run after the schema checks, with the resolved schema as their second argument. The field vocabulary, region keys and the eight store settings that change what is required are explained in the Address formats guide.

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.