Maintain customers
Read a customer and keep addresses, notes, labels and the customer class up to date, using your own customer ID.
Which customers you can reach
The customer endpoints work with customers that were created or matched through your access, see How customers are matched. You address them with your customer ID, the customer.external_id from your orders:
GET /api/v1/customers/u-55871
Any other ID, a customer linked to a different access, and a deleted customer all answer 404 not_found. The response does not reveal whether such a customer exists.
The ID is part of the path, so URL-encode it if it contains special characters. Do not use / in customer IDs you want to address here.
Scopes:
| Scope | Allows |
|---|---|
customers.read | read the customer, the address book, the list of labels and the list of customer classes |
customers.write | create, change and delete addresses, create notes, set labels and the customer class |
See the scope reference. Each shop has its own address; the examples use https://shop.example.com/api/v1.
Read a customer
GET /customers/{external_id} returns the complete customer record and the address book. Login data such as passwords and security codes is never included.
curl -s https://shop.example.com/api/v1/customers/u-55871 \
-H "Authorization: Bearer $OMNEYA_API_KEY" \
-H "Accept: application/json"
The response is shortened here. It contains all stored fields of the customer, and the set of fields can grow:
{
"data": {
"customer_id": 9021,
"external_id": "u-55871",
"email": "jane.doe@example.com",
"first_name": "Jane",
"last_name": "Doe",
"country": "DE",
"billing": {
"salutation": "Ms",
"title": null,
"first_name": "Jane",
"last_name": "Doe",
"company": null,
"street": "Example Street",
"house": "5",
"additional": "2nd floor",
"postcode": "10115",
"city": "Berlin",
"country": "DE"
},
"addresses": [
{
"id": 1,
"first_name": "Jane",
"last_name": "Doe",
"street": "Packstation",
"house_number": "123",
"postcode": "10117",
"city": "Berlin",
"country": "DE",
"packstation": 1,
"postnumber": "12345678",
"is_default_shipping": true
}
]
}
}
billing uses the same field names as orders. Entries in addresses contain the stored fields of each address, plus country as a two-letter code and is_default_shipping.
Addresses
| Action | Request | Success |
|---|---|---|
| List addresses | GET /customers/{external_id}/addresses | 200 |
| Create an address | POST /customers/{external_id}/addresses | 201, or 200 if the same address already exists |
| Update an address | PATCH /customers/{external_id}/addresses/{id} | 200 |
| Delete an address | DELETE /customers/{external_id}/addresses/{id} | 204 |
The request body uses the same format as customer.shipping in an order:
curl -s -X POST https://shop.example.com/api/v1/customers/u-55871/addresses \
-H "Authorization: Bearer $OMNEYA_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
--data '{
"first_name": "Jane",
"last_name": "Doe",
"street": "Packstation",
"house": "123",
"postcode": "10117",
"city": "Berlin",
"country": "DE",
"telephone": "+49 30 1234567",
"packstation": true,
"postnumber": "12345678",
"is_default_shipping": true,
"is_default_billing": false
}'
How the address endpoints behave:
- Creating an address that already exists for the customer creates no second one. You receive the existing address with
200. is_default_shippingandis_default_billingmake the address the default delivery or billing address. The previous default loses its flag, so there is always at most one of each.- When updating, send only the fields that change.
street,postcode,cityandcountrycannot be emptied. - The response contains the stored address with its
id, which you need for updates and deletes. - An
idthat does not belong to this customer answers404.
All fields and limits are in the address reference.
Notes
POST /customers/{external_id}/notes adds a note to the customer, for staff in the shop:
{ "text": "Customer prefers delivery in the afternoon.", "notification": 0 }
notification controls whether staff see the note as a notice: 0 no notice (default), 1 notice when opening the customer or an order and when creating an order, 2 notice only in the customer view.
text may contain plain text or simple HTML such as paragraphs, lists and bold text. Scripts and event attributes are removed. Special characters come back HTML-encoded, for example & as &.
The response answers 201:
{
"data": {
"id": 501,
"customer_id": 9021,
"text": "Customer prefers delivery in the afternoon.",
"notification": 0,
"created_at": "2026-10-13T14:02:11+02:00"
}
}
Each request creates a new note.
Labels
Labels are defined in the shop. Each label has values, and you assign values, not labels. GET /customer-labels lists all labels with their values:
{
"data": [
{
"id": 4,
"name": "Interests",
"active": true,
"values": [
{ "id": 31, "value": "Running", "color": "#2e7d32", "active": true },
{ "id": 32, "value": "Yoga", "color": null, "active": true }
]
}
]
}
PUT /customers/{external_id}/labels sets values:
{ "mode": "add", "value_ids": [31, 32] }
add(default) adds the values; values already set stay.replacesets exactly these values and removes all others."value_ids": []removes all.
A value that is already set is never added twice. The response lists all values set afterwards:
{ "data": { "customer_id": 9021, "value_ids": [31, 32] } }
DELETE /customers/{external_id}/labels/{value_id} removes one value and answers 204. If the value was not set, it answers 404.
Customer class
GET /customer-classes lists all customer classes with their settings, for example discount and price display, and under payment_methods the payment methods assigned to each class.
PUT /customers/{external_id}/class assigns a class:
{ "customer_class_id": 3 }
{ "data": { "customer_id": 9021, "customer_class_id": 3 } }
The new class applies from the customer’s next order on, including its discount.
Errors
Invalid input answers 422 validation_failed with all problems per field, in the same format as orders:
{
"error": {
"code": "validation_failed",
"message": "The request payload is invalid.",
"details": {
"postcode": [{ "code": "required", "message": "The postcode field is required." }]
}
}
}
Typical field codes here: required, unknown_country for an unknown country code, unknown_value for an unknown label value or customer class, invalid_value for an unknown mode or notification, duplicate_value for a value listed twice in value_ids, and out_of_range for more than 200 entries in value_ids. See Errors.