# Create or update a customer

URL: https://docs.sparklayer.io/developers/api/customers/create-or-update-a-customer
Operation: `PUT /api/v2/customers/{lookupBy}/{identifier}` (operationId `upsertCustomer`)
OpenAPI spec: https://docs.sparklayer.io/openapi/customers.yaml

Creates the customer if none matches `lookupBy` and `identifier` (`201`), or replaces the one that does (`200`).

- **The body must contain the identifier in the URL**: `external_id` for `platform`, `email` for `email`, `id` for a SparkLayer UUID or `legacy_id` for a `cu_` ID. Otherwise you get `400` (`idMismatch`). With `sparklayer` and a UUID no customer has, the customer is created with that ID.
- **Send the whole customer every time.** Fields you leave out are cleared, including `accounting_id`, `price_lists`, `discount_percentage`, `tax_exempt`, `sales_agent_groups` and the credit limit, currency and net terms in `payment_on_account`. Addresses missing from `addresses` are deleted, so leaving `addresses` out deletes them all.
- **`group` and `role` must exist**, and so must `parent_customer_id` if you set it, or you get `400`. Price list slugs aren't checked, and sales agent groups that don't exist yet are created.
- **Emails and platform IDs are unique.** `409` if another customer has the same `email` or `external_id`, or another address has the same address `external_id`.
- **`payment_on_account` records the balance.** With `balance_date`, `balance` becomes an opening balance on that date. Without it, the balance is set to `balance` (or 0) as of now. See **Adjust a customer's account balance**.

To change only some fields, use **Update a customer**.

`PUT /api/v2/customers/{lookupBy}/{identifier}`

**Base URLs:** `https://app.sparklayer.io` (Live), `https://test.app.sparklayer.io` (Test)

**Authentication:** send `Authorization: Bearer <access_token>` and `Site-Id: <site id>` with every request. Get the token from [Get an access token](https://docs.sparklayer.io/developers/api/core/get-an-access-token.md); see [Authentication](https://docs.sparklayer.io/developers/authentication.md).

### Example request

```bash
curl -X PUT "https://app.sparklayer.io/api/v2/customers/platform/7843024560291" \
  -H "Authorization: Bearer $SPARKLAYER_TOKEN" \
  -H "Site-Id: $SPARKLAYER_SITE_ID" \
  -H "Content-Type: application/json" \
  -d '{
  "title": "Mr",
  "first_name": "Tom",
  "last_name": "Jones",
  "company_name": "Tom Jones Climbing Ltd",
  "parent_customer_id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "status": "active",
  "group": "base",
  "role": "limited",
  "default_billing_address_id": "<default_billing_address_id>",
  "default_shipping_address_id": "<default_shipping_address_id>",
  "available_shipping_addresses": [
    "<available_shipping_addresses>"
  ],
  "sales_agent_groups": [
    "south-west-uk"
  ],
  "price_lists": [
    "b2b_gb_gbp_123"
  ],
  "discount_percentage": 0,
  "addresses": [
    {
      "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
      "legacy_id": "ca_47",
      "title": "Mr",
      "first_name": "Bob",
      "last_name": "Jones",
      "company": "Tom Jones Climbing Ltd",
      "address_line1": "Example Industrial Estate",
      "address_line2": "North Country",
      "city": "Cityland",
      "region_code": "CA",
      "postal_code": "12345",
      "country_code": "US",
      "phone": "+44 (0) 123456789",
      "external_id": "123456789",
      "is_default_shipping": true,
      "is_default_billing": true,
      "valid_for_purchase_id": "3f2b8c1e-5d4a-4e6f-9b7c-1a2d3e4f5a6b"
    }
  ],
  "cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "previous_cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "cart_loaded_from_purchase": false,
  "tax_exempt": true,
  "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "legacy_id": "cu_123456789",
  "external_id": "123456789",
  "accounting_id": "<accounting_id>",
  "email": "Tom.jones@climbing.com",
  "payment_on_account": null
}'
```

Set `SPARKLAYER_TOKEN` to an access token and `SPARKLAYER_SITE_ID` to your Site ID. For the test environment, use `https://test.app.sparklayer.io`.

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `lookupBy` | "sparklayer" \| "platform" \| "email" | Yes | What `identifier` is: `sparklayer` (the customer's SparkLayer ID), `platform` (their ID on your eCommerce platform) or `email` (their email address). Example: `platform` |
| `identifier` | string | Yes | The customer's ID or email address, of the kind set by `lookupBy`. SparkLayer IDs are UUIDs; legacy `cu_` IDs work too, except for shopping lists and favourites. Email lookups are exact and case-sensitive: URL-encode the address, with `+` as `%2B`. Example: `7843024560291` |

### Header parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Site-Id` | string | Yes | Your SparkLayer Site ID, from Settings > API in the SparkLayer Dashboard. Length: min 1. Example: `jones-climbing` |

### Request body (`application/json`) (required)

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string \| null | | Title. Length: max 30. |
| `first_name` | string | Yes | First Name. Length: max 128. |
| `last_name` | string | Yes | Last Name. Length: max 128. |
| `company_name` | string \| null | | Company Name. Length: max 255. |
| `parent_customer_id` | string \| null | | SparkLayer or Platform Customer ID. Default: `null`. |
| `status` | "active" \| "inactive" \| "archived" | Yes | `active` customers can sign in. `inactive` customers are kept but can't sign in. `archived` marks a former B2B customer, such as one whose `b2b` tag was removed in Shopify: they're kept, but left out of searches and sales reps' customer lists. Default: `active`. |
| `group` | string | Yes | Customer Group Slug. Length: min 1, max 128. |
| `role` | string | Yes | A role that represents the users permissions. Length: max 64. |
| `default_billing_address_id` | string \| null | | Default Billing Address ID |
| `default_shipping_address_id` | string \| null | | Default Shipping Address ID |
| `available_shipping_addresses` | string[] \| null | | Limit the shipping addresses to be selected |
| `sales_agent_groups` | string[] | | Sales Agent Groups The customer's own membership, and the only sales agent group list that is writable. Access decisions should read effective_sales_agent_groups, which also includes the groups the customer inherits from their section. Never write that field back here: it would turn a section's groups into the customer's own, and they would keep them after leaving the section. If a sales agent/customer, access is only given to other sales agents/customers with one of the groups specified. If a customer, access is only given to other sales agents with one of the groups specified. If empty for a customer, only sales agents with an empty list can access the customer. If empty for a sales agent, any customer is accessible. |
| `price_lists` | string[] | | Price list slugs for this customer, which replace their customer group's. SparkLayer doesn't check the slugs: make sure each price list exists. |
| `discount_percentage` | number (float) \| null | | Customer specific percentage discount. Min 0, max 100. |
| `addresses` | any[] | | |
| `addresses[].id` | string (uuid) | | SparkLayer Customer Address ID |
| `addresses[].legacy_id` | string \| null | | Legacy SparkLayer Customer Address ID |
| `addresses[].title` | string \| null | | Title. Length: max 30. |
| `addresses[].first_name` | string | Yes | First Name. Length: max 50. |
| `addresses[].last_name` | string | Yes | Last Name. Length: max 50. |
| `addresses[].company` | string \| null | | Company. Length: max 128. |
| `addresses[].address_line1` | string | Yes | Address Line 1. Length: min 1, max 255. |
| `addresses[].address_line2` | string \| null | | Address Line 2. Length: max 255. |
| `addresses[].city` | string | Yes | City. Length: min 1, max 128. |
| `addresses[].region_code` | string \| null | Yes | The region within the country, such as `CA` for California. Optional, but if you send it, it must be a region of `country_code`. Length: max 10. |
| `addresses[].postal_code` | string | | Postal Code. Length: max 20. |
| `addresses[].country_code` | string | Yes | The 2-letter ISO 3166-1 country code, such as `GB`. Length: max 2. |
| `addresses[].phone` | string \| null | | Phone. Length: max 20. |
| `addresses[].external_id` | string \| null | | The identifier for the address on the e-commerce platform. Length: min 1, max 124. |
| `addresses[].is_default_shipping` | boolean | | |
| `addresses[].is_default_billing` | boolean | | |
| `addresses[].valid_for_purchase_id` | string (uuid) \| null | | A temporary address (typically used for drop shipping) only valid for a purchase ID |
| `cart_uuid` | string (uuid) \| null | | |
| `previous_cart_uuid` | string (uuid) \| null | | |
| `cart_loaded_from_purchase` | boolean \| null | | Default: `false`. |
| `tax_exempt` | boolean \| null | | |
| `id` | string (uuid) | | SparkLayer Customer ID |
| `legacy_id` | string \| null | | Legacy SparkLayer Customer ID. Length: min 4. |
| `external_id` | string \| null | Yes | The customer's ID on your eCommerce platform. Look the customer up by it with `lookupBy` set to `platform`. Length: min 1, max 124. |
| `accounting_id` | string \| null | | The identifier for the customer on the merchants accountancy or ERP. Length: min 1, max 124. |
| `email` | string | Yes | The customer's email address. Lookups by email are exact and case-sensitive. Length: min 1, max 255. |
| `payment_on_account` | object \| null | | Allow payments on account when the order total meets available credit (overrides customer group settings). Default: `null`. |
| `payment_on_account.credit_limit` | number (float) \| null | | Max 999999999.999. |
| `payment_on_account.balance` | number (float) \| null | | What the customer owes. Sending it adds an entry to the customer's account credit ledger: an opening balance with `balance_date`, otherwise the balance as of now. Max 999999999.999. |
| `payment_on_account.currency_code` | string \| null | | If set allows overriding the currency for the customer, otherwise the base currency of the store is to be used. Pattern: `^[A-Za-z]{3}$`. |
| `payment_on_account.net_terms` | "7_days" \| "15_days" \| "30_days" \| "45_days" \| "60_days" \| "90_days" \| null | | |
| `payment_on_account.balance_date` | string (date-time) \| null | | Makes `balance` an opening balance on this date: from then on, only orders and payments dated on or after it count towards the balance. Leave it out to set the balance as of now. |

```json
{
  "title": "Mr",
  "first_name": "Tom",
  "last_name": "Jones",
  "company_name": "Tom Jones Climbing Ltd",
  "parent_customer_id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "status": "active",
  "group": "base",
  "role": "limited",
  "default_billing_address_id": "<default_billing_address_id>",
  "default_shipping_address_id": "<default_shipping_address_id>",
  "available_shipping_addresses": [
    "<available_shipping_addresses>"
  ],
  "sales_agent_groups": [
    "south-west-uk"
  ],
  "price_lists": [
    "b2b_gb_gbp_123"
  ],
  "discount_percentage": 0,
  "addresses": [
    {
      "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
      "legacy_id": "ca_47",
      "title": "Mr",
      "first_name": "Bob",
      "last_name": "Jones",
      "company": "Tom Jones Climbing Ltd",
      "address_line1": "Example Industrial Estate",
      "address_line2": "North Country",
      "city": "Cityland",
      "region_code": "CA",
      "postal_code": "12345",
      "country_code": "US",
      "phone": "+44 (0) 123456789",
      "external_id": "123456789",
      "is_default_shipping": true,
      "is_default_billing": true,
      "valid_for_purchase_id": "3f2b8c1e-5d4a-4e6f-9b7c-1a2d3e4f5a6b"
    }
  ],
  "cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "previous_cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "cart_loaded_from_purchase": false,
  "tax_exempt": true,
  "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "legacy_id": "cu_123456789",
  "external_id": "123456789",
  "accounting_id": "<accounting_id>",
  "email": "Tom.jones@climbing.com",
  "payment_on_account": null
}
```

### Responses

#### 200: Successful response, customer updated

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string \| null | | Title. Length: max 30. |
| `first_name` | string | | First Name. Length: max 128. |
| `last_name` | string | | Last Name. Length: max 128. |
| `company_name` | string \| null | | Company Name. Length: max 255. |
| `parent_customer_id` | string (uuid) \| null | | SparkLayer Customer ID |
| `status` | "active" \| "inactive" \| "archived" | | `active` customers can sign in. `inactive` customers are kept but can't sign in. `archived` marks a former B2B customer, such as one whose `b2b` tag was removed in Shopify: they're kept, but left out of searches and sales reps' customer lists. Default: `active`. |
| `group` | string | | Customer Group Slug. Length: min 1, max 128. |
| `role` | string | | Role slug (e.g. main, limited-customer, sales-agent, sales-admin, or custom slug). Length: max 64. |
| `default_billing_address_id` | string \| null | | Default Billing Address ID |
| `default_shipping_address_id` | string \| null | | Default Shipping Address ID |
| `available_shipping_addresses` | string[] \| null | | Limit the shipping addresses to be selected |
| `sales_agent_groups` | string[] | | Sales Agent Groups The customer's own membership, and the only sales agent group list that is writable. Access decisions should read effective_sales_agent_groups, which also includes the groups the customer inherits from their section. Never write that field back here: it would turn a section's groups into the customer's own, and they would keep them after leaving the section. If a sales agent/customer, access is only given to other sales agents/customers with one of the groups specified. If a customer, access is only given to other sales agents with one of the groups specified. If empty for a customer, only sales agents with an empty list can access the customer. If empty for a sales agent, any customer is accessible. |
| `price_lists` | string[] | | Price list slugs for this customer, which replace their customer group's. SparkLayer doesn't check the slugs: make sure each price list exists. |
| `discount_percentage` | number (float) \| null | | Customer specific percentage discount. Min 0, max 100. |
| `addresses` | any[] | | |
| `addresses[].id` | string (uuid) | | SparkLayer Customer Address ID |
| `addresses[].legacy_id` | string \| null | | Legacy SparkLayer Customer Address ID |
| `addresses[].created_at` | string (date-time) | | Created At |
| `addresses[].updated_at` | string (date-time) | | Updated At |
| `addresses[].title` | string \| null | | Title. Length: max 30. |
| `addresses[].first_name` | string | Yes | First Name. Length: max 50. |
| `addresses[].last_name` | string | Yes | Last Name. Length: max 50. |
| `addresses[].company` | string \| null | | Company. Length: max 128. |
| `addresses[].address_line1` | string | Yes | Address Line 1. Length: min 1, max 255. |
| `addresses[].address_line2` | string \| null | | Address Line 2. Length: max 255. |
| `addresses[].city` | string | Yes | City. Length: min 1, max 128. |
| `addresses[].region_name` | string \| null | | Region Name (readonly). Length: max 128. |
| `addresses[].region_code` | string \| null | Yes | The region within the country, such as `CA` for California. Optional, but if you send it, it must be a region of `country_code`. Length: max 10. |
| `addresses[].postal_code` | string | | Postal Code. Length: max 20. |
| `addresses[].country_name` | string | | Country Name (readonly). Length: max 3. |
| `addresses[].country_code` | string | Yes | The 2-letter ISO 3166-1 country code, such as `GB`. Length: max 2. |
| `addresses[].phone` | string \| null | | Phone. Length: max 20. |
| `addresses[].external_id` | string \| null | | The identifier for the address on the e-commerce platform. Length: min 1, max 124. |
| `addresses[].is_default_shipping` | boolean | | |
| `addresses[].is_default_billing` | boolean | | |
| `addresses[].valid_for_purchase_id` | string (uuid) \| null | | A temporary address (typically used for drop shipping) only valid for a purchase ID |
| `cart_uuid` | string (uuid) \| null | | |
| `previous_cart_uuid` | string (uuid) \| null | | |
| `cart_loaded_from_purchase` | boolean \| null | | Default: `false`. |
| `tax_exempt` | boolean \| null | | |
| `id` | string (uuid) | | SparkLayer Customer ID |
| `legacy_id` | string \| null | | Legacy SparkLayer Customer ID. Length: min 4. |
| `external_id` | string \| null | | The customer's ID on your eCommerce platform. Look the customer up by it with `lookupBy` set to `platform`. Length: min 1, max 124. |
| `accounting_id` | string \| null | | The identifier for the customer on the merchants accountancy or ERP. Length: min 1, max 124. |
| `email` | string | | The customer's email address. Lookups by email are exact and case-sensitive. Length: min 1, max 255. |
| `effective_sales_agent_groups` | string[] | | sales_agent_groups unioned with the groups of every section from the customer's section up to the company root. A company admin user, who has no assigned section, resolves the company root's groups only. Purely additive and derived on read: a section's groups never remove a customer's own, and this is never persisted. |
| `role_object` | object | | Full role details (id, name, slug, type, permissions, is_default). Present in responses only. |
| `role_object.id` | string (uuid) | Yes | |
| `role_object.name` | string | Yes | Length: max 50. |
| `role_object.slug` | string | Yes | Length: max 64. |
| `role_object.type` | "customer" \| "sales" | Yes | |
| `role_object.permissions` | object \| null | | Map of permission key (see PermissionKey enum) to boolean. Only keys from PermissionKey are accepted; invalid keys return 400. |
| `role_object.is_default` | boolean | | True when this role is a built-in default (cannot be updated or deleted). Derived from IsDefaultRoleID. |
| `created_at` | string (date-time) | | Created At |
| `updated_at` | string (date-time) | | Updated At |
| `parent_customer` | object | | |
| `parent_customer.title` | string \| null | | Title. Length: max 30. |
| `parent_customer.first_name` | string | | First Name. Length: max 128. |
| `parent_customer.last_name` | string | | Last Name. Length: max 128. |
| `parent_customer.company_name` | string \| null | | Company Name. Length: max 255. |
| `parent_customer.parent_customer_id` | string \| null | | SparkLayer or Platform Customer ID. Default: `null`. |
| `parent_customer.status` | "active" \| "inactive" \| "archived" | | `active` customers can sign in. `inactive` customers are kept but can't sign in. `archived` marks a former B2B customer, such as one whose `b2b` tag was removed in Shopify: they're kept, but left out of searches and sales reps' customer lists. Default: `active`. |
| `parent_customer.group` | string | | Customer Group Slug. Length: min 1, max 128. |
| `parent_customer.role` | string | | A role that represents the users permissions. Length: max 64. |
| `parent_customer.default_billing_address_id` | string \| null | | Default Billing Address ID |
| `parent_customer.default_shipping_address_id` | string \| null | | Default Shipping Address ID |
| `parent_customer.available_shipping_addresses` | string[] \| null | | Limit the shipping addresses to be selected |
| `parent_customer.sales_agent_groups` | string[] | | Sales Agent Groups The customer's own membership, and the only sales agent group list that is writable. Access decisions should read effective_sales_agent_groups, which also includes the groups the customer inherits from their section. Never write that field back here: it would turn a section's groups into the customer's own, and they would keep them after leaving the section. If a sales agent/customer, access is only given to other sales agents/customers with one of the groups specified. If a customer, access is only given to other sales agents with one of the groups specified. If empty for a customer, only sales agents with an empty list can access the customer. If empty for a sales agent, any customer is accessible. |
| `parent_customer.price_lists` | string[] | | Price list slugs for this customer, which replace their customer group's. SparkLayer doesn't check the slugs: make sure each price list exists. |
| `parent_customer.discount_percentage` | number (float) \| null | | Customer specific percentage discount. Min 0, max 100. |
| `parent_customer.addresses` | any[] | | |
| `parent_customer.addresses[].id` | string (uuid) | | SparkLayer Customer Address ID |
| `parent_customer.addresses[].legacy_id` | string \| null | | Legacy SparkLayer Customer Address ID |
| `parent_customer.addresses[].created_at` | string (date-time) | | Created At |
| `parent_customer.addresses[].updated_at` | string (date-time) | | Updated At |
| `parent_customer.addresses[].title` | string \| null | | Title. Length: max 30. |
| `parent_customer.addresses[].first_name` | string | Yes | First Name. Length: max 50. |
| `parent_customer.addresses[].last_name` | string | Yes | Last Name. Length: max 50. |
| `parent_customer.addresses[].company` | string \| null | | Company. Length: max 128. |
| `parent_customer.addresses[].address_line1` | string | Yes | Address Line 1. Length: min 1, max 255. |
| `parent_customer.addresses[].address_line2` | string \| null | | Address Line 2. Length: max 255. |
| `parent_customer.addresses[].city` | string | Yes | City. Length: min 1, max 128. |
| `parent_customer.addresses[].region_name` | string \| null | | Region Name (readonly). Length: max 128. |
| `parent_customer.addresses[].region_code` | string \| null | Yes | The region within the country, such as `CA` for California. Optional, but if you send it, it must be a region of `country_code`. Length: max 10. |
| `parent_customer.addresses[].postal_code` | string | | Postal Code. Length: max 20. |
| `parent_customer.addresses[].country_name` | string | | Country Name (readonly). Length: max 3. |
| `parent_customer.addresses[].country_code` | string | Yes | The 2-letter ISO 3166-1 country code, such as `GB`. Length: max 2. |
| `parent_customer.addresses[].phone` | string \| null | | Phone. Length: max 20. |
| `parent_customer.addresses[].external_id` | string \| null | | The identifier for the address on the e-commerce platform. Length: min 1, max 124. |
| `parent_customer.addresses[].is_default_shipping` | boolean | | |
| `parent_customer.addresses[].is_default_billing` | boolean | | |
| `parent_customer.addresses[].valid_for_purchase_id` | string (uuid) \| null | | A temporary address (typically used for drop shipping) only valid for a purchase ID |
| `parent_customer.cart_uuid` | string (uuid) \| null | | |
| `parent_customer.previous_cart_uuid` | string (uuid) \| null | | |
| `parent_customer.cart_loaded_from_purchase` | boolean \| null | | Default: `false`. |
| `parent_customer.tax_exempt` | boolean \| null | | |
| `payment_on_account` | object \| null | | Allow payments on account when the order total meets available credit (overrides customer group settings). Default: `null`. |
| `payment_on_account.credit_limit` | number (float) \| null | | Max 999999999.999. |
| `payment_on_account.balance` | number (float) \| null | | What the customer owes. Sending it adds an entry to the customer's account credit ledger: an opening balance with `balance_date`, otherwise the balance as of now. Max 999999999.999. |
| `payment_on_account.currency_code` | string \| null | | If set allows overriding the currency for the customer, otherwise the base currency of the store is to be used. Pattern: `^[A-Za-z]{3}$`. |
| `payment_on_account.net_terms` | "7_days" \| "15_days" \| "30_days" \| "45_days" \| "60_days" \| "90_days" \| null | | |
| `payment_on_account.balance_date` | string (date-time) \| null | | Makes `balance` an opening balance on this date: from then on, only orders and payments dated on or after it count towards the balance. Leave it out to set the balance as of now. |

```json
{
  "title": "Mr",
  "first_name": "Tom",
  "last_name": "Jones",
  "company_name": "Tom Jones Climbing Ltd",
  "parent_customer_id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "status": "active",
  "group": "base",
  "role": "limited",
  "default_billing_address_id": "<default_billing_address_id>",
  "default_shipping_address_id": "<default_shipping_address_id>",
  "available_shipping_addresses": [
    "<available_shipping_addresses>"
  ],
  "sales_agent_groups": [
    "south-west-uk"
  ],
  "price_lists": [
    "b2b_gb_gbp_123"
  ],
  "discount_percentage": 0,
  "addresses": [
    {
      "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
      "legacy_id": "ca_47",
      "created_at": "2026-01-31T09:00:00Z",
      "updated_at": "2026-01-31T09:00:00Z",
      "title": "Mr",
      "first_name": "Bob",
      "last_name": "Jones",
      "company": "Tom Jones Climbing Ltd",
      "address_line1": "Example Industrial Estate",
      "address_line2": "North Country",
      "city": "Cityland",
      "region_name": "California",
      "region_code": "CA",
      "postal_code": "12345",
      "country_name": "USA",
      "country_code": "US",
      "phone": "+44 (0) 123456789",
      "external_id": "123456789",
      "is_default_shipping": true,
      "is_default_billing": true,
      "valid_for_purchase_id": "3f2b8c1e-5d4a-4e6f-9b7c-1a2d3e4f5a6b"
    }
  ],
  "cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "previous_cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "cart_loaded_from_purchase": false,
  "tax_exempt": true,
  "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "legacy_id": "cu_123456789",
  "external_id": "123456789",
  "accounting_id": "<accounting_id>",
  "email": "Tom.jones@climbing.com",
  "effective_sales_agent_groups": [
    "south-west-uk"
  ],
  "role_object": {
    "id": "019bbd11-4e64-735f-9205-5b7bc84181f6",
    "name": "My Custom Role",
    "slug": "my-custom-role",
    "type": "customer",
    "permissions": {
      "purchase-edit-prices": true,
      "customers-create-new": false
    },
    "is_default": true
  },
  "created_at": "2026-01-31T09:00:00Z",
  "updated_at": "2026-01-31T09:00:00Z",
  "parent_customer": {
    "title": "Mr",
    "first_name": "Tom",
    "last_name": "Jones",
    "company_name": "Tom Jones Climbing Ltd",
    "parent_customer_id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
    "status": "active",
    "group": "base",
    "role": "limited",
    "default_billing_address_id": "<default_billing_address_id>",
    "default_shipping_address_id": "<default_shipping_address_id>",
    "available_shipping_addresses": [
      "<available_shipping_addresses>"
    ],
    "sales_agent_groups": [
      "south-west-uk"
    ],
    "price_lists": [
      "b2b_gb_gbp_123"
    ],
    "discount_percentage": 0,
    "addresses": [
      {
        "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
        "legacy_id": "ca_47",
        "created_at": "2026-01-31T09:00:00Z",
        "updated_at": "2026-01-31T09:00:00Z",
        "title": "Mr",
        "first_name": "Bob",
        "last_name": "Jones",
        "company": "Tom Jones Climbing Ltd",
        "address_line1": "Example Industrial Estate",
        "address_line2": "North Country",
        "city": "Cityland",
        "region_name": "California",
        "region_code": "CA",
        "postal_code": "12345",
        "country_name": "USA",
        "country_code": "US",
        "phone": "+44 (0) 123456789",
        "external_id": "123456789",
        "is_default_shipping": true,
        "is_default_billing": true,
        "valid_for_purchase_id": "3f2b8c1e-5d4a-4e6f-9b7c-1a2d3e4f5a6b"
      }
    ],
    "cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
    "previous_cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
    "cart_loaded_from_purchase": false,
    "tax_exempt": true
  },
  "payment_on_account": null
}
```

#### 201: Successful response, customer created

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string \| null | | Title. Length: max 30. |
| `first_name` | string | | First Name. Length: max 128. |
| `last_name` | string | | Last Name. Length: max 128. |
| `company_name` | string \| null | | Company Name. Length: max 255. |
| `parent_customer_id` | string (uuid) \| null | | SparkLayer Customer ID |
| `status` | "active" \| "inactive" \| "archived" | | `active` customers can sign in. `inactive` customers are kept but can't sign in. `archived` marks a former B2B customer, such as one whose `b2b` tag was removed in Shopify: they're kept, but left out of searches and sales reps' customer lists. Default: `active`. |
| `group` | string | | Customer Group Slug. Length: min 1, max 128. |
| `role` | string | | Role slug (e.g. main, limited-customer, sales-agent, sales-admin, or custom slug). Length: max 64. |
| `default_billing_address_id` | string \| null | | Default Billing Address ID |
| `default_shipping_address_id` | string \| null | | Default Shipping Address ID |
| `available_shipping_addresses` | string[] \| null | | Limit the shipping addresses to be selected |
| `sales_agent_groups` | string[] | | Sales Agent Groups The customer's own membership, and the only sales agent group list that is writable. Access decisions should read effective_sales_agent_groups, which also includes the groups the customer inherits from their section. Never write that field back here: it would turn a section's groups into the customer's own, and they would keep them after leaving the section. If a sales agent/customer, access is only given to other sales agents/customers with one of the groups specified. If a customer, access is only given to other sales agents with one of the groups specified. If empty for a customer, only sales agents with an empty list can access the customer. If empty for a sales agent, any customer is accessible. |
| `price_lists` | string[] | | Price list slugs for this customer, which replace their customer group's. SparkLayer doesn't check the slugs: make sure each price list exists. |
| `discount_percentage` | number (float) \| null | | Customer specific percentage discount. Min 0, max 100. |
| `addresses` | any[] | | |
| `addresses[].id` | string (uuid) | | SparkLayer Customer Address ID |
| `addresses[].legacy_id` | string \| null | | Legacy SparkLayer Customer Address ID |
| `addresses[].created_at` | string (date-time) | | Created At |
| `addresses[].updated_at` | string (date-time) | | Updated At |
| `addresses[].title` | string \| null | | Title. Length: max 30. |
| `addresses[].first_name` | string | Yes | First Name. Length: max 50. |
| `addresses[].last_name` | string | Yes | Last Name. Length: max 50. |
| `addresses[].company` | string \| null | | Company. Length: max 128. |
| `addresses[].address_line1` | string | Yes | Address Line 1. Length: min 1, max 255. |
| `addresses[].address_line2` | string \| null | | Address Line 2. Length: max 255. |
| `addresses[].city` | string | Yes | City. Length: min 1, max 128. |
| `addresses[].region_name` | string \| null | | Region Name (readonly). Length: max 128. |
| `addresses[].region_code` | string \| null | Yes | The region within the country, such as `CA` for California. Optional, but if you send it, it must be a region of `country_code`. Length: max 10. |
| `addresses[].postal_code` | string | | Postal Code. Length: max 20. |
| `addresses[].country_name` | string | | Country Name (readonly). Length: max 3. |
| `addresses[].country_code` | string | Yes | The 2-letter ISO 3166-1 country code, such as `GB`. Length: max 2. |
| `addresses[].phone` | string \| null | | Phone. Length: max 20. |
| `addresses[].external_id` | string \| null | | The identifier for the address on the e-commerce platform. Length: min 1, max 124. |
| `addresses[].is_default_shipping` | boolean | | |
| `addresses[].is_default_billing` | boolean | | |
| `addresses[].valid_for_purchase_id` | string (uuid) \| null | | A temporary address (typically used for drop shipping) only valid for a purchase ID |
| `cart_uuid` | string (uuid) \| null | | |
| `previous_cart_uuid` | string (uuid) \| null | | |
| `cart_loaded_from_purchase` | boolean \| null | | Default: `false`. |
| `tax_exempt` | boolean \| null | | |
| `id` | string (uuid) | | SparkLayer Customer ID |
| `legacy_id` | string \| null | | Legacy SparkLayer Customer ID. Length: min 4. |
| `external_id` | string \| null | | The customer's ID on your eCommerce platform. Look the customer up by it with `lookupBy` set to `platform`. Length: min 1, max 124. |
| `accounting_id` | string \| null | | The identifier for the customer on the merchants accountancy or ERP. Length: min 1, max 124. |
| `email` | string | | The customer's email address. Lookups by email are exact and case-sensitive. Length: min 1, max 255. |
| `effective_sales_agent_groups` | string[] | | sales_agent_groups unioned with the groups of every section from the customer's section up to the company root. A company admin user, who has no assigned section, resolves the company root's groups only. Purely additive and derived on read: a section's groups never remove a customer's own, and this is never persisted. |
| `role_object` | object | | Full role details (id, name, slug, type, permissions, is_default). Present in responses only. |
| `role_object.id` | string (uuid) | Yes | |
| `role_object.name` | string | Yes | Length: max 50. |
| `role_object.slug` | string | Yes | Length: max 64. |
| `role_object.type` | "customer" \| "sales" | Yes | |
| `role_object.permissions` | object \| null | | Map of permission key (see PermissionKey enum) to boolean. Only keys from PermissionKey are accepted; invalid keys return 400. |
| `role_object.is_default` | boolean | | True when this role is a built-in default (cannot be updated or deleted). Derived from IsDefaultRoleID. |
| `created_at` | string (date-time) | | Created At |
| `updated_at` | string (date-time) | | Updated At |
| `parent_customer` | object | | |
| `parent_customer.title` | string \| null | | Title. Length: max 30. |
| `parent_customer.first_name` | string | | First Name. Length: max 128. |
| `parent_customer.last_name` | string | | Last Name. Length: max 128. |
| `parent_customer.company_name` | string \| null | | Company Name. Length: max 255. |
| `parent_customer.parent_customer_id` | string \| null | | SparkLayer or Platform Customer ID. Default: `null`. |
| `parent_customer.status` | "active" \| "inactive" \| "archived" | | `active` customers can sign in. `inactive` customers are kept but can't sign in. `archived` marks a former B2B customer, such as one whose `b2b` tag was removed in Shopify: they're kept, but left out of searches and sales reps' customer lists. Default: `active`. |
| `parent_customer.group` | string | | Customer Group Slug. Length: min 1, max 128. |
| `parent_customer.role` | string | | A role that represents the users permissions. Length: max 64. |
| `parent_customer.default_billing_address_id` | string \| null | | Default Billing Address ID |
| `parent_customer.default_shipping_address_id` | string \| null | | Default Shipping Address ID |
| `parent_customer.available_shipping_addresses` | string[] \| null | | Limit the shipping addresses to be selected |
| `parent_customer.sales_agent_groups` | string[] | | Sales Agent Groups The customer's own membership, and the only sales agent group list that is writable. Access decisions should read effective_sales_agent_groups, which also includes the groups the customer inherits from their section. Never write that field back here: it would turn a section's groups into the customer's own, and they would keep them after leaving the section. If a sales agent/customer, access is only given to other sales agents/customers with one of the groups specified. If a customer, access is only given to other sales agents with one of the groups specified. If empty for a customer, only sales agents with an empty list can access the customer. If empty for a sales agent, any customer is accessible. |
| `parent_customer.price_lists` | string[] | | Price list slugs for this customer, which replace their customer group's. SparkLayer doesn't check the slugs: make sure each price list exists. |
| `parent_customer.discount_percentage` | number (float) \| null | | Customer specific percentage discount. Min 0, max 100. |
| `parent_customer.addresses` | any[] | | |
| `parent_customer.addresses[].id` | string (uuid) | | SparkLayer Customer Address ID |
| `parent_customer.addresses[].legacy_id` | string \| null | | Legacy SparkLayer Customer Address ID |
| `parent_customer.addresses[].created_at` | string (date-time) | | Created At |
| `parent_customer.addresses[].updated_at` | string (date-time) | | Updated At |
| `parent_customer.addresses[].title` | string \| null | | Title. Length: max 30. |
| `parent_customer.addresses[].first_name` | string | Yes | First Name. Length: max 50. |
| `parent_customer.addresses[].last_name` | string | Yes | Last Name. Length: max 50. |
| `parent_customer.addresses[].company` | string \| null | | Company. Length: max 128. |
| `parent_customer.addresses[].address_line1` | string | Yes | Address Line 1. Length: min 1, max 255. |
| `parent_customer.addresses[].address_line2` | string \| null | | Address Line 2. Length: max 255. |
| `parent_customer.addresses[].city` | string | Yes | City. Length: min 1, max 128. |
| `parent_customer.addresses[].region_name` | string \| null | | Region Name (readonly). Length: max 128. |
| `parent_customer.addresses[].region_code` | string \| null | Yes | The region within the country, such as `CA` for California. Optional, but if you send it, it must be a region of `country_code`. Length: max 10. |
| `parent_customer.addresses[].postal_code` | string | | Postal Code. Length: max 20. |
| `parent_customer.addresses[].country_name` | string | | Country Name (readonly). Length: max 3. |
| `parent_customer.addresses[].country_code` | string | Yes | The 2-letter ISO 3166-1 country code, such as `GB`. Length: max 2. |
| `parent_customer.addresses[].phone` | string \| null | | Phone. Length: max 20. |
| `parent_customer.addresses[].external_id` | string \| null | | The identifier for the address on the e-commerce platform. Length: min 1, max 124. |
| `parent_customer.addresses[].is_default_shipping` | boolean | | |
| `parent_customer.addresses[].is_default_billing` | boolean | | |
| `parent_customer.addresses[].valid_for_purchase_id` | string (uuid) \| null | | A temporary address (typically used for drop shipping) only valid for a purchase ID |
| `parent_customer.cart_uuid` | string (uuid) \| null | | |
| `parent_customer.previous_cart_uuid` | string (uuid) \| null | | |
| `parent_customer.cart_loaded_from_purchase` | boolean \| null | | Default: `false`. |
| `parent_customer.tax_exempt` | boolean \| null | | |
| `payment_on_account` | object \| null | | Allow payments on account when the order total meets available credit (overrides customer group settings). Default: `null`. |
| `payment_on_account.credit_limit` | number (float) \| null | | Max 999999999.999. |
| `payment_on_account.balance` | number (float) \| null | | What the customer owes. Sending it adds an entry to the customer's account credit ledger: an opening balance with `balance_date`, otherwise the balance as of now. Max 999999999.999. |
| `payment_on_account.currency_code` | string \| null | | If set allows overriding the currency for the customer, otherwise the base currency of the store is to be used. Pattern: `^[A-Za-z]{3}$`. |
| `payment_on_account.net_terms` | "7_days" \| "15_days" \| "30_days" \| "45_days" \| "60_days" \| "90_days" \| null | | |
| `payment_on_account.balance_date` | string (date-time) \| null | | Makes `balance` an opening balance on this date: from then on, only orders and payments dated on or after it count towards the balance. Leave it out to set the balance as of now. |

```json
{
  "title": "Mr",
  "first_name": "Tom",
  "last_name": "Jones",
  "company_name": "Tom Jones Climbing Ltd",
  "parent_customer_id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "status": "active",
  "group": "base",
  "role": "limited",
  "default_billing_address_id": "<default_billing_address_id>",
  "default_shipping_address_id": "<default_shipping_address_id>",
  "available_shipping_addresses": [
    "<available_shipping_addresses>"
  ],
  "sales_agent_groups": [
    "south-west-uk"
  ],
  "price_lists": [
    "b2b_gb_gbp_123"
  ],
  "discount_percentage": 0,
  "addresses": [
    {
      "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
      "legacy_id": "ca_47",
      "created_at": "2026-01-31T09:00:00Z",
      "updated_at": "2026-01-31T09:00:00Z",
      "title": "Mr",
      "first_name": "Bob",
      "last_name": "Jones",
      "company": "Tom Jones Climbing Ltd",
      "address_line1": "Example Industrial Estate",
      "address_line2": "North Country",
      "city": "Cityland",
      "region_name": "California",
      "region_code": "CA",
      "postal_code": "12345",
      "country_name": "USA",
      "country_code": "US",
      "phone": "+44 (0) 123456789",
      "external_id": "123456789",
      "is_default_shipping": true,
      "is_default_billing": true,
      "valid_for_purchase_id": "3f2b8c1e-5d4a-4e6f-9b7c-1a2d3e4f5a6b"
    }
  ],
  "cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "previous_cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "cart_loaded_from_purchase": false,
  "tax_exempt": true,
  "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
  "legacy_id": "cu_123456789",
  "external_id": "123456789",
  "accounting_id": "<accounting_id>",
  "email": "Tom.jones@climbing.com",
  "effective_sales_agent_groups": [
    "south-west-uk"
  ],
  "role_object": {
    "id": "019bbd11-4e64-735f-9205-5b7bc84181f6",
    "name": "My Custom Role",
    "slug": "my-custom-role",
    "type": "customer",
    "permissions": {
      "purchase-edit-prices": true,
      "customers-create-new": false
    },
    "is_default": true
  },
  "created_at": "2026-01-31T09:00:00Z",
  "updated_at": "2026-01-31T09:00:00Z",
  "parent_customer": {
    "title": "Mr",
    "first_name": "Tom",
    "last_name": "Jones",
    "company_name": "Tom Jones Climbing Ltd",
    "parent_customer_id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
    "status": "active",
    "group": "base",
    "role": "limited",
    "default_billing_address_id": "<default_billing_address_id>",
    "default_shipping_address_id": "<default_shipping_address_id>",
    "available_shipping_addresses": [
      "<available_shipping_addresses>"
    ],
    "sales_agent_groups": [
      "south-west-uk"
    ],
    "price_lists": [
      "b2b_gb_gbp_123"
    ],
    "discount_percentage": 0,
    "addresses": [
      {
        "id": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
        "legacy_id": "ca_47",
        "created_at": "2026-01-31T09:00:00Z",
        "updated_at": "2026-01-31T09:00:00Z",
        "title": "Mr",
        "first_name": "Bob",
        "last_name": "Jones",
        "company": "Tom Jones Climbing Ltd",
        "address_line1": "Example Industrial Estate",
        "address_line2": "North Country",
        "city": "Cityland",
        "region_name": "California",
        "region_code": "CA",
        "postal_code": "12345",
        "country_name": "USA",
        "country_code": "US",
        "phone": "+44 (0) 123456789",
        "external_id": "123456789",
        "is_default_shipping": true,
        "is_default_billing": true,
        "valid_for_purchase_id": "3f2b8c1e-5d4a-4e6f-9b7c-1a2d3e4f5a6b"
      }
    ],
    "cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
    "previous_cart_uuid": "9151f21f-43ae-43b4-92f3-f4af67cdf544",
    "cart_loaded_from_purchase": false,
    "tax_exempt": true
  },
  "payment_on_account": null
}
```

#### Other statuses: An error. The body describes the problem: see [Errors](https://docs.sparklayer.io/developers/errors.md).

`application/problem+json`: the standard error body (below).

### Error body

Error responses with a body use this RFC 7807 problem details object. See [Errors](https://docs.sparklayer.io/developers/errors.md) for every status code and which errors to retry.

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `detail` | string | | Human-readable summary of the error |
| `status` | integer | | HTTP Status code returned from API |
| `title` | string | | Machine-readable error code |
| `type` | string | | |
| `errors` | object[] | | |
| `errors[].code` | string | | A machine-readable error message |
| `errors[].property` | string | | The offending property, with the full path |
| `errors[].field` | string | | The offending property, stripped of the path |
| `errors[].message` | string | | A human-readable summary of the error |

```json
{
  "detail": "Data Validation Failed",
  "status": 400,
  "title": "invalid-api-request-contents",
  "type": "https://hub.sparklayer.io/tech-docs",
  "errors": [
    {
      "code": "unique-constraint-violation",
      "property": "purchase_identifiers.sparklayer",
      "field": "sparklayer",
      "message": "Each stock level must have a unique stock location and sku combination"
    }
  ]
}
```
