# Edara API v3: Customers

Read the [guide](../guide.md) first: authentication, paging, errors, rate limits and the shared `PagedResult`, `BatchResult` and `*Reference` shapes.

- `GET /v3/customers`: Lists customers.
- `GET /v3/customers/{id}`: Gets a customer by id.
- `GET /v3/customers/{id}/addresses`: Gets customer addresses by id.
- `GET /v3/customers/{id}/balance`: Gets a customer balance by id.
- `GET /v3/customers/{id}/invoiceable-documents`: Lists invoiceable source documents for a customer.
- `POST /v3/customers`: Creates a customer.
- `POST /v3/customers/batch`: Creates customers in batch.
- `POST /v3/customers/find-by-ids`: Finds customers by ids.
- `POST /v3/customers/find-by-external-ids`: Finds customers by external ids.
- `PUT /v3/customers/{id}`: Updates a customer by id.
- `PUT /v3/customers/code/{code}`: Updates a customer by code.
- `PUT /v3/customers/batch/by-id`: Updates customers in batch by id.
- `PUT /v3/customers/batch/by-name`: Updates customers in batch by name.
- `PUT /v3/customers/batch/by-code`: Updates customers in batch by code.
- `DELETE /v3/customers/{id}`: Deletes a customer by id.
- `DELETE /v3/customers/code/{code}`: Deletes a customer by code.
- `PATCH /v3/customers/{id}/deactivate`: Deactivates a customer by id.
- `PATCH /v3/customers/code/{code}/deactivate`: Deactivates a customer by code.

## Endpoints

### `GET /v3/customers`: Lists customers.

Operation `GetCustomers` · permission `read:customer`

**Query string**: `GetCustomersQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `offset` | `int?` | 0 | >= 0 | Number of items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 100 | 1..1000 (out of range → 400) | Maximum number of items to return. Defaults to 100, maximum 1000. |
| `code` | `string` |  | MaximumLength(50) | CONTAINS filter on code. |
| `name` | `string` |  | MaximumLength(250) | CONTAINS filter on name. |
| `phone` | `string` |  | MaximumLength(50) | CONTAINS filter on primary phone. |
| `mobile` | `string` |  | MaximumLength(50) | CONTAINS filter on mobile. |
| `email` | `string` |  | MaximumLength(250) | CONTAINS filter on email. |
| `externalId` | `string` |  | MaximumLength(50) | CONTAINS filter on external id. |
| `updateDate` | `DateTime?` |  |  | Returns records modified on or after this date (UTC, inclusive). |
| `search` | `string` |  |  | Free-text search across name/code/phone/mobile/email. Takes precedence over individual filters. |

**Responses**: OK `PagedResult<CustomerResponse>`: Paged list of customers.

**Notes**

- When you send `search`, all other filters are ignored and `totalCount` is the number of search matches.
- `search` matches only active customers. By default it looks for the text inside the customer name and the mobile number, compared without spaces, and it does not look at the code or phone.
- Organization settings decide which fields `search` looks at (name, mobile, code, phone) and how each one is matched. The same search can behave differently between organizations, so code defensively.
- `search` returns at most 50 customers for each field it looks at, which is about 100 in total with the default fields. Further matches are left out without any warning, so do not treat a search result as a complete list.
- If the calling user is linked to a sales person, `search` returns only customers that are unassigned, assigned to that sales person, or assigned to a sales person they supervise.
- Results are ordered by customer id, with or without `search`.
- `code`, `name`, `phone` and `email` match any part of the stored value. `mobile` also matches any part: a plus sign, spaces and a leading 00 are removed from the value you send, and spaces in the stored number are ignored.
- `externalId` is an exact match, not a contains match as its description suggests.
- `updateDate` returns customers created or updated strictly after the value you send, not on or after it. A customer changed at exactly that time is not returned.
- Without `search`, when you filter by `code`, `name`, `phone`, `mobile` or `email`, only active customers whose receivable account is also active are returned. With none of these filters (no filter, `externalId` only or `updateDate` only), inactive customers are included too.
- Without `search`, when an organization setting applies data permissions to customers, the list contains only the customers the calling user is permitted to see. A user with no customer data permissions gets an empty list.
- A `name` filter longer than 100 characters or an `email` filter longer than 50 characters is cut to that length before matching.
- `balance` in list items is always 0, except when `updateDate` is the only filter. In that case it is calculated for every customer and the request is slow. To read a balance, use GET /v3/customers/{id}/balance.
- An empty result returns `200` with an empty `items` list, never `404`.

---

### `GET /v3/customers/{id}`: Gets a customer by id.

Operation `GetCustomerById` · permission `read:customer`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The customer id. |

**Responses**: OK `CustomerResponse`: The customer.

**Notes**

- An id that does not exist gives `404`. Inactive customers are returned too.
- `balance` in this response is always 0. Use GET /v3/customers/{id}/balance for the real balance.
- The response includes the customer's addresses. `salesPerson`, `relatedAccount` and `defaultForeignCurrency` are null when they are not set.

---

### `GET /v3/customers/{id}/addresses`: Gets customer addresses by id.

Operation `GetCustomerAddresses` · permission `read:customer`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The customer id. |

**Responses**: OK `IReadOnlyCollection<CustomerAddressWithLocationResponse>`: Customer addresses.

**Notes**

- A customer id that does not exist gives `404`. Inactive customers work the same as active ones.
- A customer with no addresses gives `200` with an empty array.
- Addresses come back in no guaranteed order, so do not rely on their position.
- The country, city and district references on an address are null when the referenced record is not found.

---

### `GET /v3/customers/{id}/balance`: Gets a customer balance by id.

Operation `GetCustomerBalance` · permission `read:customer`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The customer id. |

**Responses**: OK `decimal`: Customer balance.

**Notes**

- The response body is a bare decimal number, not an object. A customer that does not exist gives `404`.
- The balance covers all dates and includes both posted and unposted entries.

---

### `GET /v3/customers/{id}/invoiceable-documents`: Lists invoiceable source documents for a customer.

Operation `GetInvoiceableDocuments` · permission `read:customer`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The customer id. |

**Query string**: `GetInvoiceableDocumentsQuery`

| field | type | default | validation | description |
|---|---|---|---|---|
| `offset` | `int?` | 0 | >= 0 | Number of items to skip before returning results. Defaults to 0. |
| `limit` | `int?` | 100 | 1..1000 (out of range → 400); NotNull(); InclusiveBetween(1, 20) .When(x => x.Limit.HasValue) | Maximum number of items to return. Defaults to 100, maximum 1000. |
| `documentTypes` | `List<string>` | new List<string>() | NotEmpty(); each: Must(type => SupportedDocumentTypes.Contains(type ?? string.Empty)) | The source document types to include. Supported values: SO, SR, IO, RR. |
| _(object rule)_ | | | private static readonly HashSet<string> SupportedDocumentTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "SO", "SR", "IO", "RR" } | |

**Responses**: OK `PagedResult<InvoiceableDocumentResponse>`: Paged list of invoiceable documents.

**Notes**

- A customer that does not exist gives `404`.
- `documentTypes` must contain at least one value, and only `SO`, `SR`, `IO` and `RR` are accepted. Anything else gives `errorCode` `InvalidInvoiceableDocumentType`.
- `SO` and `SR` documents are listed only while they are not fully invoiced. They must also have passed all approvals or be flagged to create a work order automatically.
- `IO` and `RR` work orders are listed only when they are not fully invoiced, are not linked to a sales order and were not generated automatically.
- For `IO` and `RR`, `netTotal` is the sum of quantity times value over the lines, divided by the exchange rate.
- All types come back in one list, ordered by date and then document code, both descending. `totalCount` counts the whole merged list.
- When nothing matches, you get `200` with empty `items`.

---

### `POST /v3/customers`: Creates a customer.

Operation `CreateCustomer` · permission `create:customer`

**Body**: `CustomerUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | Customer id. Required for batch update; ignored on create. |
| `code` | `string` |  | MaximumLength(50) | Business code (max 50). Required on create. |
| `name` | `string` |  | NotEmpty().MaximumLength(250) | Display name (max 250). Required on create. |
| `customerType` | `CustomerType?` |  |  | Customer classification. Defaults to Consumer on create. |
| `pricingType` | `CustomerPricingType?` |  |  | Pricing strategy. |
| `priceListId` | `int?` |  | NotNull() .GreaterThan(0) .When(x => x.PricingType == CustomerPricingType.Custom) | Price list id, used when `PricingType` is PriceList. |
| `taxRegistrationId` | `string` |  | MaximumLength(20) | Tax registration id (max 20). May be required for business customers. |
| `nationalId` | `string` |  | MaximumLength(14) | National identifier (max 14). May be required for consumer customers. |
| `shippingTerm` | `string` |  |  | Shipping terms (e.g. "FOB", "CIF"). |
| `insurance` | `string` |  |  | Insurance terms or notes. |
| `guarantee` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.Guarantee.HasValue) | Optional guarantee amount. |
| `creditLimit` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.CreditLimit.HasValue) | Optional credit limit. |
| `hasDiscount` | `bool?` |  |  | Whether line discounts are allowed. |
| `discountMandatory` | `bool?` |  |  | Whether discount is mandatory. |
| `discountFrom` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountFrom.HasValue) | Minimum allowed discount percentage. |
| `discountTo` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountTo.HasValue) | Maximum allowed discount percentage. |
| `paymentType` | `CustomerPaymentType?` |  |  | Default payment type. |
| `paymentMaxDueDays` | `int?` |  | GreaterThanOrEqualTo(0) .When(x => x.PaymentMaxDueDays.HasValue) | Maximum credit days allowed. |
| `relatedAccountId` | `int?` |  |  | Existing receivable account id. Mutually exclusive with `RelatedAccountParentId`. |
| `relatedAccountParentId` | `int?` |  |  | AR parent CoA node under which a new related account is created. Only used when "create AR with new customer" is on. |
| `salesPersonId` | `int?` |  |  | Optional sales person id assigned to the customer. |
| `defaultForeignCurrencyId` | `int?` |  |  | Default foreign-currency id. |
| `externalId` | `string` |  | MaximumLength(50) | Optional external id (max 50). Must be unique across customers. |
| `active` | `bool?` |  |  | Whether the customer is active. Defaults to true on create. |
| `contactPerson` | `string` |  | MaximumLength(250) | Primary contact person name. |
| `contactCountryId` | `int?` |  |  | Primary contact country id. |
| `contactCityId` | `int?` |  |  | Primary contact city id. |
| `contactDistrictId` | `int?` |  |  | Primary contact district id. |
| `postalZipCode` | `string` |  | MaximumLength(50) | Primary contact postal / ZIP code. |
| `phone` | `string` |  | MaximumLength(50) | Primary contact phone (max 50). |
| `phone2` | `string` |  | MaximumLength(50) | Secondary contact phone (max 50). |
| `fax` | `string` |  | MaximumLength(50) | Fax number (max 50). |
| `mobile` | `string` |  | MaximumLength(50) | Primary contact mobile (max 50). May be required (CustomerMobileRequired). |
| `email` | `string` |  | MaximumLength(250) | Primary contact email (max 250). May be required (CustomerEmailRequired). |
| `tags` | `List<string>` |  |  | Customer tags. |
| `addresses` | `List<CustomerAddressUpsertRequest>` |  | each: SetValidator(new CustomerAddressUpsertRequestValidator()) | Customer addresses to create or update. |

**Responses**: Created `CustomerResponse`: Customer created.

**Business errors** (HTTP 409, match on `errorCode`):

- `CityNotFound`: A city with the specified identifier was not found.
- `CountryNotFound`: A country with the specified identifier was not found.
- `CustomerAddressMustContainBuildingNumber`: Address must include a building number.
- `CustomerAddressMustHaveDistrict`: Address district is required.
- `CustomerAddressRequired`: Customer addresses are required.
- `CustomerCodeExists`: Another customer already uses the same code.
- `CustomerCodeRequired`: Customer code is required.
- `CustomerEmailExists`: Another customer already uses the same email.
- `CustomerEmailRequired`: Customer email is required.
- `CustomerMobileExists`: Another customer already uses the same mobile.
- `CustomerMobileRequired`: Customer mobile is required.
- `CustomerNameExists`: Another customer already uses the same name.
- `CustomerNationalIDRequired`: Customer national identifier is required.
- `DistrictNotFound`: A district with the specified identifier was not found.
- `DuplicatedExternalId`: Another customer already uses the same external identifier.
- `DupplicatedARAccount`: Another customer already uses the same accounting name.
- `DupplicatedName`: Another customer already uses the same name.
- `NationalIDExists`: Another customer already uses the same national identifier.
- `NationalIDMustBeOfLength`: National identifier length is invalid.
- `TaxRegisterationIDExists`: Another customer already uses the same tax registration identifier.
- `TaxRegisterationIDMustBeOfLength`: Tax registration identifier length is invalid.
- `TaxRegistrationIdRequired`: Customer tax registration identifier is required.

**Notes**

- A `salesPersonId` that does not exist gives `400`.
- Send at most one default address, because more than one gives `400`. If none is marked as default, the first address becomes the default.
- Each address's country and city, and its district when you send one, must exist. Otherwise you get `409` with `errorCode` `CountryNotFound`, `CityNotFound` or `DistrictNotFound`.
- An address with a blank `name` is named automatically from its country and city, in the form "Country, City".
- When omitted, `customerType` defaults to `Consumer`, `pricingType` to `EndUser`, `paymentType` to `Cash` and `active` to true.
- How the customer's accounts receivable account is set depends on an organization setting, so handle both cases described in the next two notes.
- If the organization does not create an account for each new customer, the customer is linked to `relatedAccountId`, which must be an accounts receivable account, or to the organization's default account for customers. If neither is available, you get `400`.
- If the organization creates an account for each new customer, the new account is created under `relatedAccountParentId`, which must be an accounts receivable node with no child nodes.
- If the organization generates customer codes automatically, the `code` you send is replaced with the next generated code. Do not assume the customer keeps the `code` you sent.
- Many checks depend on organization settings: whether the name must be unique, whether mobile, email, code and tax registration id are required or unique, whether an address is required, and how many digits some numbers must have. Code defensively and handle these errors for every organization.
- `nationalId` must always be unique, whatever the organization settings.
- Special characters are removed from `code` and `name` before saving, so the stored values can differ from what you sent.
- `pricingType` `Custom` needs a price list, otherwise you get `errorCode` `CustomerCustomPricingTypeMustHavePriceList`. With any other `pricingType`, `priceListId` is cleared without an error.
- The customer, its addresses, sales person, account and tags are saved together. If anything fails, nothing is created.
- A duplicate name, accounts receivable account or `externalId` gives `errorCode` `DupplicatedName`, `DupplicatedARAccount` or `DuplicatedExternalId`. Note that the spellings differ.
- Create is not idempotent. Sending the same customer twice is stopped only by the uniqueness rules, for example a duplicate `externalId`.
- Also handle these `errorCode` values: `CustomerCustomPricingTypeMustHavePriceList`, `CustomerNameRequired`, `PriceListNotFound`, `AddressCountryRequired` and `AddressCityRequired`.

---

### `POST /v3/customers/batch`: Creates customers in batch.

Operation `CreateCustomersBatch` · permission `create:customer`

> See `CreateCustomer` for the list of business errors that can be reported per item.

**Body**: `IEnumerable<CustomerUpsertRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | Customer id. Required for batch update; ignored on create. |
| `code` | `string` |  | MaximumLength(50) | Business code (max 50). Required on create. |
| `name` | `string` |  | NotEmpty().MaximumLength(250) | Display name (max 250). Required on create. |
| `customerType` | `CustomerType?` |  |  | Customer classification. Defaults to Consumer on create. |
| `pricingType` | `CustomerPricingType?` |  |  | Pricing strategy. |
| `priceListId` | `int?` |  | NotNull() .GreaterThan(0) .When(x => x.PricingType == CustomerPricingType.Custom) | Price list id, used when `PricingType` is PriceList. |
| `taxRegistrationId` | `string` |  | MaximumLength(20) | Tax registration id (max 20). May be required for business customers. |
| `nationalId` | `string` |  | MaximumLength(14) | National identifier (max 14). May be required for consumer customers. |
| `shippingTerm` | `string` |  |  | Shipping terms (e.g. "FOB", "CIF"). |
| `insurance` | `string` |  |  | Insurance terms or notes. |
| `guarantee` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.Guarantee.HasValue) | Optional guarantee amount. |
| `creditLimit` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.CreditLimit.HasValue) | Optional credit limit. |
| `hasDiscount` | `bool?` |  |  | Whether line discounts are allowed. |
| `discountMandatory` | `bool?` |  |  | Whether discount is mandatory. |
| `discountFrom` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountFrom.HasValue) | Minimum allowed discount percentage. |
| `discountTo` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountTo.HasValue) | Maximum allowed discount percentage. |
| `paymentType` | `CustomerPaymentType?` |  |  | Default payment type. |
| `paymentMaxDueDays` | `int?` |  | GreaterThanOrEqualTo(0) .When(x => x.PaymentMaxDueDays.HasValue) | Maximum credit days allowed. |
| `relatedAccountId` | `int?` |  |  | Existing receivable account id. Mutually exclusive with `RelatedAccountParentId`. |
| `relatedAccountParentId` | `int?` |  |  | AR parent CoA node under which a new related account is created. Only used when "create AR with new customer" is on. |
| `salesPersonId` | `int?` |  |  | Optional sales person id assigned to the customer. |
| `defaultForeignCurrencyId` | `int?` |  |  | Default foreign-currency id. |
| `externalId` | `string` |  | MaximumLength(50) | Optional external id (max 50). Must be unique across customers. |
| `active` | `bool?` |  |  | Whether the customer is active. Defaults to true on create. |
| `contactPerson` | `string` |  | MaximumLength(250) | Primary contact person name. |
| `contactCountryId` | `int?` |  |  | Primary contact country id. |
| `contactCityId` | `int?` |  |  | Primary contact city id. |
| `contactDistrictId` | `int?` |  |  | Primary contact district id. |
| `postalZipCode` | `string` |  | MaximumLength(50) | Primary contact postal / ZIP code. |
| `phone` | `string` |  | MaximumLength(50) | Primary contact phone (max 50). |
| `phone2` | `string` |  | MaximumLength(50) | Secondary contact phone (max 50). |
| `fax` | `string` |  | MaximumLength(50) | Fax number (max 50). |
| `mobile` | `string` |  | MaximumLength(50) | Primary contact mobile (max 50). May be required (CustomerMobileRequired). |
| `email` | `string` |  | MaximumLength(250) | Primary contact email (max 250). May be required (CustomerEmailRequired). |
| `tags` | `List<string>` |  |  | Customer tags. |
| `addresses` | `List<CustomerAddressUpsertRequest>` |  | each: SetValidator(new CustomerAddressUpsertRequestValidator()) | Customer addresses to create or update. |

**Responses**: OK `BatchResult<CustomerResponse>`: Batch insert result.

**Business errors** (HTTP 409, match on `errorCode`):

- `CityNotFound`: A city with the specified identifier was not found.
- `CountryNotFound`: A country with the specified identifier was not found.
- `CustomerAddressMustContainBuildingNumber`: Address must include a building number.
- `CustomerAddressMustHaveDistrict`: Address district is required.
- `CustomerAddressRequired`: Customer addresses are required.
- `CustomerCodeExists`: Another customer already uses the same code.
- `CustomerCodeRequired`: Customer code is required.
- `CustomerEmailExists`: Another customer already uses the same email.
- `CustomerEmailRequired`: Customer email is required.
- `CustomerMobileExists`: Another customer already uses the same mobile.
- `CustomerMobileRequired`: Customer mobile is required.
- `CustomerNameExists`: Another customer already uses the same name.
- `CustomerNationalIDRequired`: Customer national identifier is required.
- `DistrictNotFound`: A district with the specified identifier was not found.
- `DuplicatedExternalId`: Another customer already uses the same external identifier.
- `DupplicatedARAccount`: Another customer already uses the same accounting name.
- `DupplicatedName`: Another customer already uses the same name.
- `NationalIDExists`: Another customer already uses the same national identifier.
- `NationalIDMustBeOfLength`: National identifier length is invalid.
- `TaxRegisterationIDExists`: Another customer already uses the same tax registration identifier.
- `TaxRegisterationIDMustBeOfLength`: Tax registration identifier length is invalid.
- `TaxRegistrationIdRequired`: Customer tax registration identifier is required.

**Notes**

- The batch is not atomic. Each customer is created on its own, and a failed item does not stop or undo the others.
- The response is `200` even when items fail, so always check `failed`. Each entry is a string in the form "ErrorCode: message".
- `failed` is keyed by the item's name, or its code when there is no name, or its id. Two failed items with the same name overwrite each other, so you see only one of them.
- An empty body or more than 1000 items gives `400`.
- An unexpected server error stops the batch with `500`, but customers created before it stay created. Check what was created before you retry.

---

### `POST /v3/customers/find-by-ids`: Finds customers by ids.

Operation `FindCustomersByIds` · permission `read:customer`

**Body**: `IEnumerable<int>` (JSON array)

**Responses**: OK `IReadOnlyCollection<CustomerResponse>`: Matched customers.

**Notes**

- Ids of 0 or less and duplicate ids are ignored. An empty list gives `200` with an empty array, and more than 1000 distinct ids gives `400`.
- Inactive customers are returned too.
- Ids that do not exist are left out without an error, and results come back in no guaranteed order. Match results to your request by `id`.
- `balance` here is the current balance of the customer's linked accounts receivable account.

---

### `POST /v3/customers/find-by-external-ids`: Finds customers by external ids.

Operation `FindCustomersByExternalIds` · permission `read:customer`

**Body**: `IEnumerable<string>` (JSON array)

**Responses**: OK `IReadOnlyCollection<CustomerResponse>`: Matched customers.

**Notes**

- Values are trimmed, blank values are ignored and duplicates are removed without regard to letter case. An empty list gives `200` with an empty array, and more than 1000 values gives `400`.
- Matching is exact on the whole `externalId`. Whether letter case matters is not guaranteed, so send each value in the same case you stored it.
- Only active customers are returned. A deactivated customer is not found here, although POST /v3/customers/find-by-ids returns it.
- Use this endpoint to look up customers by external id. GET /v3/customers with `externalId` also matches exactly, but it can return inactive customers.
- External ids with no match are left out without an error.

---

### `PUT /v3/customers/{id}`: Updates a customer by id.

Operation `UpdateCustomerById` · permission `update:customer`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The customer id. |

**Body**: `CustomerUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | Customer id. Required for batch update; ignored on create. |
| `code` | `string` |  | MaximumLength(50) | Business code (max 50). Required on create. |
| `name` | `string` |  | NotEmpty().MaximumLength(250) | Display name (max 250). Required on create. |
| `customerType` | `CustomerType?` |  |  | Customer classification. Defaults to Consumer on create. |
| `pricingType` | `CustomerPricingType?` |  |  | Pricing strategy. |
| `priceListId` | `int?` |  | NotNull() .GreaterThan(0) .When(x => x.PricingType == CustomerPricingType.Custom) | Price list id, used when `PricingType` is PriceList. |
| `taxRegistrationId` | `string` |  | MaximumLength(20) | Tax registration id (max 20). May be required for business customers. |
| `nationalId` | `string` |  | MaximumLength(14) | National identifier (max 14). May be required for consumer customers. |
| `shippingTerm` | `string` |  |  | Shipping terms (e.g. "FOB", "CIF"). |
| `insurance` | `string` |  |  | Insurance terms or notes. |
| `guarantee` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.Guarantee.HasValue) | Optional guarantee amount. |
| `creditLimit` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.CreditLimit.HasValue) | Optional credit limit. |
| `hasDiscount` | `bool?` |  |  | Whether line discounts are allowed. |
| `discountMandatory` | `bool?` |  |  | Whether discount is mandatory. |
| `discountFrom` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountFrom.HasValue) | Minimum allowed discount percentage. |
| `discountTo` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountTo.HasValue) | Maximum allowed discount percentage. |
| `paymentType` | `CustomerPaymentType?` |  |  | Default payment type. |
| `paymentMaxDueDays` | `int?` |  | GreaterThanOrEqualTo(0) .When(x => x.PaymentMaxDueDays.HasValue) | Maximum credit days allowed. |
| `relatedAccountId` | `int?` |  |  | Existing receivable account id. Mutually exclusive with `RelatedAccountParentId`. |
| `relatedAccountParentId` | `int?` |  |  | AR parent CoA node under which a new related account is created. Only used when "create AR with new customer" is on. |
| `salesPersonId` | `int?` |  |  | Optional sales person id assigned to the customer. |
| `defaultForeignCurrencyId` | `int?` |  |  | Default foreign-currency id. |
| `externalId` | `string` |  | MaximumLength(50) | Optional external id (max 50). Must be unique across customers. |
| `active` | `bool?` |  |  | Whether the customer is active. Defaults to true on create. |
| `contactPerson` | `string` |  | MaximumLength(250) | Primary contact person name. |
| `contactCountryId` | `int?` |  |  | Primary contact country id. |
| `contactCityId` | `int?` |  |  | Primary contact city id. |
| `contactDistrictId` | `int?` |  |  | Primary contact district id. |
| `postalZipCode` | `string` |  | MaximumLength(50) | Primary contact postal / ZIP code. |
| `phone` | `string` |  | MaximumLength(50) | Primary contact phone (max 50). |
| `phone2` | `string` |  | MaximumLength(50) | Secondary contact phone (max 50). |
| `fax` | `string` |  | MaximumLength(50) | Fax number (max 50). |
| `mobile` | `string` |  | MaximumLength(50) | Primary contact mobile (max 50). May be required (CustomerMobileRequired). |
| `email` | `string` |  | MaximumLength(250) | Primary contact email (max 250). May be required (CustomerEmailRequired). |
| `tags` | `List<string>` |  |  | Customer tags. |
| `addresses` | `List<CustomerAddressUpsertRequest>` |  | each: SetValidator(new CustomerAddressUpsertRequestValidator()) | Customer addresses to create or update. |

**Responses**: OK `CustomerResponse`: The updated customer.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteCustomerAddress`: An existing customer address cannot be removed because it is referenced by other records.
- `CityNotFound`: A city with the specified identifier was not found.
- `CountryNotFound`: A country with the specified identifier was not found.
- `CustomerAddressMustContainBuildingNumber`: Address must include a building number.
- `CustomerAddressMustHaveDistrict`: Address district is required.
- `CustomerAddressRequired`: Customer addresses are required.
- `CustomerCodeExists`: Another customer already uses the same code.
- `CustomerCodeRequired`: Customer code is required.
- `CustomerEmailExists`: Another customer already uses the same email.
- `CustomerEmailRequired`: Customer email is required.
- `CustomerMobileExists`: Another customer already uses the same mobile.
- `CustomerMobileRequired`: Customer mobile is required.
- `CustomerNameExists`: Another customer already uses the same name.
- `CustomerNationalIDRequired`: Customer national identifier is required.
- `CutomerMustHaveSingleDefaultAddress`: Customer cannot have more than one default address.
- `DistrictNotFound`: A district with the specified identifier was not found.
- `DuplicatedExternalId`: Another customer already uses the same external identifier.
- `DupplicatedName`: Another customer already uses the same name.
- `NationalIDExists`: Another customer already uses the same national identifier.
- `NationalIDMustBeOfLength`: National identifier length is invalid.
- `TaxRegisterationIDExists`: Another customer already uses the same tax registration identifier.
- `TaxRegisterationIDMustBeOfLength`: Tax registration identifier length is invalid.
- `TaxRegistrationIdRequired`: Customer tax registration identifier is required.

**Notes**

- An id that does not exist gives `404`. An `id` in the body that differs from the id in the URL gives `400`, and so does a `salesPersonId` that does not exist.
- This is a full replace, not a partial update. Fields you omit are cleared or set to 0, including phones, email, tags, contact ids, discounts, credit limit and currency.
- Only `code`, `customerType`, `pricingType`, `paymentType` and `externalId` keep their stored values when omitted. If the stored `pricingType` or `paymentType` is empty, it becomes `EndUser` or `Cash`.
- Because an omitted `externalId` keeps its stored value, you cannot clear it by leaving it out.
- If you omit `active`, it is set to true, so an update without `active` reactivates a deactivated customer.
- If you omit `salesPersonId`, the customer's sales person is removed.
- `relatedAccountId` and `relatedAccountParentId` are ignored on update.
- If the linked accounts receivable account is used only by this customer, its name and active state are updated to match the customer.
- Addresses are the exception to the full replace: omit them or send an empty list and they stay unchanged.
- If none of the addresses you send has an `id`, all existing addresses are deleted and the ones you send are added.
- If some addresses have an `id`, those are updated, the ones without an `id` are added and existing addresses missing from the request are deleted. An address that is in use cannot be deleted and gives `errorCode` `CannotDeleteCustomerAddress`.
- The same organization-dependent required and unique checks as POST /v3/customers apply.
- On update, any uniqueness conflict gives `errorCode` `DupplicatedName`, whichever field is duplicated.
- A customer must have a single default address. Breaking this rule gives `errorCode` `CutomerMustHaveSingleDefaultAddress`.
- The update is saved completely or not at all.
- Also handle these `errorCode` values: `CustomerHasdefaultWarehouses`, `CannotDeactivateCustomersWithNonZeroBalance`, `CustomerCustomPricingTypeMustHavePriceList` and `PriceListNotFound`.

---

### `PUT /v3/customers/code/{code}`: Updates a customer by code.

Operation `UpdateCustomerByCode` · permission `update:customer`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The customer code. |

**Body**: `CustomerUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | Customer id. Required for batch update; ignored on create. |
| `code` | `string` |  | MaximumLength(50) | Business code (max 50). Required on create. |
| `name` | `string` |  | NotEmpty().MaximumLength(250) | Display name (max 250). Required on create. |
| `customerType` | `CustomerType?` |  |  | Customer classification. Defaults to Consumer on create. |
| `pricingType` | `CustomerPricingType?` |  |  | Pricing strategy. |
| `priceListId` | `int?` |  | NotNull() .GreaterThan(0) .When(x => x.PricingType == CustomerPricingType.Custom) | Price list id, used when `PricingType` is PriceList. |
| `taxRegistrationId` | `string` |  | MaximumLength(20) | Tax registration id (max 20). May be required for business customers. |
| `nationalId` | `string` |  | MaximumLength(14) | National identifier (max 14). May be required for consumer customers. |
| `shippingTerm` | `string` |  |  | Shipping terms (e.g. "FOB", "CIF"). |
| `insurance` | `string` |  |  | Insurance terms or notes. |
| `guarantee` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.Guarantee.HasValue) | Optional guarantee amount. |
| `creditLimit` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.CreditLimit.HasValue) | Optional credit limit. |
| `hasDiscount` | `bool?` |  |  | Whether line discounts are allowed. |
| `discountMandatory` | `bool?` |  |  | Whether discount is mandatory. |
| `discountFrom` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountFrom.HasValue) | Minimum allowed discount percentage. |
| `discountTo` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountTo.HasValue) | Maximum allowed discount percentage. |
| `paymentType` | `CustomerPaymentType?` |  |  | Default payment type. |
| `paymentMaxDueDays` | `int?` |  | GreaterThanOrEqualTo(0) .When(x => x.PaymentMaxDueDays.HasValue) | Maximum credit days allowed. |
| `relatedAccountId` | `int?` |  |  | Existing receivable account id. Mutually exclusive with `RelatedAccountParentId`. |
| `relatedAccountParentId` | `int?` |  |  | AR parent CoA node under which a new related account is created. Only used when "create AR with new customer" is on. |
| `salesPersonId` | `int?` |  |  | Optional sales person id assigned to the customer. |
| `defaultForeignCurrencyId` | `int?` |  |  | Default foreign-currency id. |
| `externalId` | `string` |  | MaximumLength(50) | Optional external id (max 50). Must be unique across customers. |
| `active` | `bool?` |  |  | Whether the customer is active. Defaults to true on create. |
| `contactPerson` | `string` |  | MaximumLength(250) | Primary contact person name. |
| `contactCountryId` | `int?` |  |  | Primary contact country id. |
| `contactCityId` | `int?` |  |  | Primary contact city id. |
| `contactDistrictId` | `int?` |  |  | Primary contact district id. |
| `postalZipCode` | `string` |  | MaximumLength(50) | Primary contact postal / ZIP code. |
| `phone` | `string` |  | MaximumLength(50) | Primary contact phone (max 50). |
| `phone2` | `string` |  | MaximumLength(50) | Secondary contact phone (max 50). |
| `fax` | `string` |  | MaximumLength(50) | Fax number (max 50). |
| `mobile` | `string` |  | MaximumLength(50) | Primary contact mobile (max 50). May be required (CustomerMobileRequired). |
| `email` | `string` |  | MaximumLength(250) | Primary contact email (max 250). May be required (CustomerEmailRequired). |
| `tags` | `List<string>` |  |  | Customer tags. |
| `addresses` | `List<CustomerAddressUpsertRequest>` |  | each: SetValidator(new CustomerAddressUpsertRequestValidator()) | Customer addresses to create or update. |

**Responses**: OK `CustomerResponse`: The updated customer.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteCustomerAddress`: An existing customer address cannot be removed because it is referenced by other records.
- `CityNotFound`: A city with the specified identifier was not found.
- `CountryNotFound`: A country with the specified identifier was not found.
- `CustomerAddressMustContainBuildingNumber`: Address must include a building number.
- `CustomerAddressMustHaveDistrict`: Address district is required.
- `CustomerAddressRequired`: Customer addresses are required.
- `CustomerCodeExists`: Another customer already uses the same code.
- `CustomerCodeRequired`: Customer code is required.
- `CustomerEmailExists`: Another customer already uses the same email.
- `CustomerEmailRequired`: Customer email is required.
- `CustomerMobileExists`: Another customer already uses the same mobile.
- `CustomerMobileRequired`: Customer mobile is required.
- `CustomerNameExists`: Another customer already uses the same name.
- `CustomerNationalIDRequired`: Customer national identifier is required.
- `CutomerMustHaveSingleDefaultAddress`: Customer cannot have more than one default address.
- `DistrictNotFound`: A district with the specified identifier was not found.
- `DuplicatedExternalId`: Another customer already uses the same external identifier.
- `DupplicatedName`: Another customer already uses the same name.
- `NationalIDExists`: Another customer already uses the same national identifier.
- `NationalIDMustBeOfLength`: National identifier length is invalid.
- `TaxRegisterationIDExists`: Another customer already uses the same tax registration identifier.
- `TaxRegisterationIDMustBeOfLength`: Tax registration identifier length is invalid.
- `TaxRegistrationIdRequired`: Customer tax registration identifier is required.

**Notes**

- The `code` in the URL is trimmed and always replaces any `code` in the body, so you cannot change a customer's code with this endpoint.
- The `code` must match exactly, and inactive customers are matched too. Whether letter case matters is not guaranteed, and a code that matches no customer gives `404`.
- If several customers share the same code, only one of them is updated and you cannot choose which. Use PUT /v3/customers/{id} when codes are not unique.
- Everything else works like PUT /v3/customers/{id}: it is a full replace, an omitted `active` becomes true and an omitted `salesPersonId` removes the sales person.

---

### `PUT /v3/customers/batch/by-id`: Updates customers in batch by id.

Operation `BatchUpdateCustomersById` · permission `update:customer`

**Body**: `IEnumerable<CustomerUpsertRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | Customer id. Required for batch update; ignored on create. |
| `code` | `string` |  | MaximumLength(50) | Business code (max 50). Required on create. |
| `name` | `string` |  | NotEmpty().MaximumLength(250) | Display name (max 250). Required on create. |
| `customerType` | `CustomerType?` |  |  | Customer classification. Defaults to Consumer on create. |
| `pricingType` | `CustomerPricingType?` |  |  | Pricing strategy. |
| `priceListId` | `int?` |  | NotNull() .GreaterThan(0) .When(x => x.PricingType == CustomerPricingType.Custom) | Price list id, used when `PricingType` is PriceList. |
| `taxRegistrationId` | `string` |  | MaximumLength(20) | Tax registration id (max 20). May be required for business customers. |
| `nationalId` | `string` |  | MaximumLength(14) | National identifier (max 14). May be required for consumer customers. |
| `shippingTerm` | `string` |  |  | Shipping terms (e.g. "FOB", "CIF"). |
| `insurance` | `string` |  |  | Insurance terms or notes. |
| `guarantee` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.Guarantee.HasValue) | Optional guarantee amount. |
| `creditLimit` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.CreditLimit.HasValue) | Optional credit limit. |
| `hasDiscount` | `bool?` |  |  | Whether line discounts are allowed. |
| `discountMandatory` | `bool?` |  |  | Whether discount is mandatory. |
| `discountFrom` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountFrom.HasValue) | Minimum allowed discount percentage. |
| `discountTo` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountTo.HasValue) | Maximum allowed discount percentage. |
| `paymentType` | `CustomerPaymentType?` |  |  | Default payment type. |
| `paymentMaxDueDays` | `int?` |  | GreaterThanOrEqualTo(0) .When(x => x.PaymentMaxDueDays.HasValue) | Maximum credit days allowed. |
| `relatedAccountId` | `int?` |  |  | Existing receivable account id. Mutually exclusive with `RelatedAccountParentId`. |
| `relatedAccountParentId` | `int?` |  |  | AR parent CoA node under which a new related account is created. Only used when "create AR with new customer" is on. |
| `salesPersonId` | `int?` |  |  | Optional sales person id assigned to the customer. |
| `defaultForeignCurrencyId` | `int?` |  |  | Default foreign-currency id. |
| `externalId` | `string` |  | MaximumLength(50) | Optional external id (max 50). Must be unique across customers. |
| `active` | `bool?` |  |  | Whether the customer is active. Defaults to true on create. |
| `contactPerson` | `string` |  | MaximumLength(250) | Primary contact person name. |
| `contactCountryId` | `int?` |  |  | Primary contact country id. |
| `contactCityId` | `int?` |  |  | Primary contact city id. |
| `contactDistrictId` | `int?` |  |  | Primary contact district id. |
| `postalZipCode` | `string` |  | MaximumLength(50) | Primary contact postal / ZIP code. |
| `phone` | `string` |  | MaximumLength(50) | Primary contact phone (max 50). |
| `phone2` | `string` |  | MaximumLength(50) | Secondary contact phone (max 50). |
| `fax` | `string` |  | MaximumLength(50) | Fax number (max 50). |
| `mobile` | `string` |  | MaximumLength(50) | Primary contact mobile (max 50). May be required (CustomerMobileRequired). |
| `email` | `string` |  | MaximumLength(250) | Primary contact email (max 250). May be required (CustomerEmailRequired). |
| `tags` | `List<string>` |  |  | Customer tags. |
| `addresses` | `List<CustomerAddressUpsertRequest>` |  | each: SetValidator(new CustomerAddressUpsertRequestValidator()) | Customer addresses to create or update. |

**Responses**: OK `BatchResult<CustomerResponse>`: Batch update result.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteCustomerAddress`: An existing customer address cannot be removed because it is referenced by other records.
- `CityNotFound`: A city with the specified identifier was not found.
- `CountryNotFound`: A country with the specified identifier was not found.
- `CustomerAddressMustContainBuildingNumber`: Address must include a building number.
- `CustomerAddressMustHaveDistrict`: Address district is required.
- `CustomerAddressRequired`: Customer addresses are required.
- `CustomerCodeExists`: Another customer already uses the same code.
- `CustomerCodeRequired`: Customer code is required.
- `CustomerEmailExists`: Another customer already uses the same email.
- `CustomerEmailRequired`: Customer email is required.
- `CustomerMobileExists`: Another customer already uses the same mobile.
- `CustomerMobileRequired`: Customer mobile is required.
- `CustomerNameExists`: Another customer already uses the same name.
- `CustomerNationalIDRequired`: Customer national identifier is required.
- `CutomerMustHaveSingleDefaultAddress`: Customer cannot have more than one default address.
- `DistrictNotFound`: A district with the specified identifier was not found.
- `DuplicatedExternalId`: Another customer already uses the same external identifier.
- `DupplicatedName`: Another customer already uses the same name.
- `NationalIDExists`: Another customer already uses the same national identifier.
- `NationalIDMustBeOfLength`: National identifier length is invalid.
- `TaxRegisterationIDExists`: Another customer already uses the same tax registration identifier.
- `TaxRegisterationIDMustBeOfLength`: Tax registration identifier length is invalid.
- `TaxRegistrationIdRequired`: Customer tax registration identifier is required.

**Notes**

- Each item needs an `id` greater than 0, otherwise that item fails with "Customer Id is required." Each item is then a full replace, exactly like PUT /v3/customers/{id}.
- The batch is not atomic. Each item is saved on its own, and failed items are reported in `failed`, keyed by id or name, while the rest continue.
- The response is `200` even when items fail, so always check `failed`.
- An empty body or more than 1000 items gives `400`.
- An unexpected server error stops the batch with `500`, but items updated before it stay updated.

---

### `PUT /v3/customers/batch/by-name`: Updates customers in batch by name.

Operation `BatchUpdateCustomersByName` · permission `update:customer`

**Body**: `IEnumerable<CustomerUpsertRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | Customer id. Required for batch update; ignored on create. |
| `code` | `string` |  | MaximumLength(50) | Business code (max 50). Required on create. |
| `name` | `string` |  | NotEmpty().MaximumLength(250) | Display name (max 250). Required on create. |
| `customerType` | `CustomerType?` |  |  | Customer classification. Defaults to Consumer on create. |
| `pricingType` | `CustomerPricingType?` |  |  | Pricing strategy. |
| `priceListId` | `int?` |  | NotNull() .GreaterThan(0) .When(x => x.PricingType == CustomerPricingType.Custom) | Price list id, used when `PricingType` is PriceList. |
| `taxRegistrationId` | `string` |  | MaximumLength(20) | Tax registration id (max 20). May be required for business customers. |
| `nationalId` | `string` |  | MaximumLength(14) | National identifier (max 14). May be required for consumer customers. |
| `shippingTerm` | `string` |  |  | Shipping terms (e.g. "FOB", "CIF"). |
| `insurance` | `string` |  |  | Insurance terms or notes. |
| `guarantee` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.Guarantee.HasValue) | Optional guarantee amount. |
| `creditLimit` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.CreditLimit.HasValue) | Optional credit limit. |
| `hasDiscount` | `bool?` |  |  | Whether line discounts are allowed. |
| `discountMandatory` | `bool?` |  |  | Whether discount is mandatory. |
| `discountFrom` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountFrom.HasValue) | Minimum allowed discount percentage. |
| `discountTo` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountTo.HasValue) | Maximum allowed discount percentage. |
| `paymentType` | `CustomerPaymentType?` |  |  | Default payment type. |
| `paymentMaxDueDays` | `int?` |  | GreaterThanOrEqualTo(0) .When(x => x.PaymentMaxDueDays.HasValue) | Maximum credit days allowed. |
| `relatedAccountId` | `int?` |  |  | Existing receivable account id. Mutually exclusive with `RelatedAccountParentId`. |
| `relatedAccountParentId` | `int?` |  |  | AR parent CoA node under which a new related account is created. Only used when "create AR with new customer" is on. |
| `salesPersonId` | `int?` |  |  | Optional sales person id assigned to the customer. |
| `defaultForeignCurrencyId` | `int?` |  |  | Default foreign-currency id. |
| `externalId` | `string` |  | MaximumLength(50) | Optional external id (max 50). Must be unique across customers. |
| `active` | `bool?` |  |  | Whether the customer is active. Defaults to true on create. |
| `contactPerson` | `string` |  | MaximumLength(250) | Primary contact person name. |
| `contactCountryId` | `int?` |  |  | Primary contact country id. |
| `contactCityId` | `int?` |  |  | Primary contact city id. |
| `contactDistrictId` | `int?` |  |  | Primary contact district id. |
| `postalZipCode` | `string` |  | MaximumLength(50) | Primary contact postal / ZIP code. |
| `phone` | `string` |  | MaximumLength(50) | Primary contact phone (max 50). |
| `phone2` | `string` |  | MaximumLength(50) | Secondary contact phone (max 50). |
| `fax` | `string` |  | MaximumLength(50) | Fax number (max 50). |
| `mobile` | `string` |  | MaximumLength(50) | Primary contact mobile (max 50). May be required (CustomerMobileRequired). |
| `email` | `string` |  | MaximumLength(250) | Primary contact email (max 250). May be required (CustomerEmailRequired). |
| `tags` | `List<string>` |  |  | Customer tags. |
| `addresses` | `List<CustomerAddressUpsertRequest>` |  | each: SetValidator(new CustomerAddressUpsertRequestValidator()) | Customer addresses to create or update. |

**Responses**: OK `BatchResult<CustomerResponse>`: Batch update result.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteCustomerAddress`: An existing customer address cannot be removed because it is referenced by other records.
- `CityNotFound`: A city with the specified identifier was not found.
- `CountryNotFound`: A country with the specified identifier was not found.
- `CustomerAddressMustContainBuildingNumber`: Address must include a building number.
- `CustomerAddressMustHaveDistrict`: Address district is required.
- `CustomerAddressRequired`: Customer addresses are required.
- `CustomerCodeExists`: Another customer already uses the same code.
- `CustomerCodeRequired`: Customer code is required.
- `CustomerEmailExists`: Another customer already uses the same email.
- `CustomerEmailRequired`: Customer email is required.
- `CustomerMobileExists`: Another customer already uses the same mobile.
- `CustomerMobileRequired`: Customer mobile is required.
- `CustomerNameExists`: Another customer already uses the same name.
- `CustomerNationalIDRequired`: Customer national identifier is required.
- `CutomerMustHaveSingleDefaultAddress`: Customer cannot have more than one default address.
- `DistrictNotFound`: A district with the specified identifier was not found.
- `DuplicatedExternalId`: Another customer already uses the same external identifier.
- `DupplicatedName`: Another customer already uses the same name.
- `NationalIDExists`: Another customer already uses the same national identifier.
- `NationalIDMustBeOfLength`: National identifier length is invalid.
- `TaxRegisterationIDExists`: Another customer already uses the same tax registration identifier.
- `TaxRegisterationIDMustBeOfLength`: Tax registration identifier length is invalid.
- `TaxRegistrationIdRequired`: Customer tax registration identifier is required.

**Notes**

- Each item is matched by `name`, which is trimmed and compared exactly. Inactive customers are matched too.
- Because `name` is the key, you cannot rename a customer with this endpoint.
- If several customers share the same name, only one of them is updated and you cannot choose which.
- Each matched item is a full replace, exactly like PUT /v3/customers/{id}.
- The batch is not atomic and takes at most 1000 items. Failed items are reported in `failed`, keyed by name, and the response is `200` even when items fail.

---

### `PUT /v3/customers/batch/by-code`: Updates customers in batch by code.

Operation `BatchUpdateCustomersByCode` · permission `update:customer`

**Body**: `IEnumerable<CustomerUpsertRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `id` | `int?` |  |  | Customer id. Required for batch update; ignored on create. |
| `code` | `string` |  | MaximumLength(50) | Business code (max 50). Required on create. |
| `name` | `string` |  | NotEmpty().MaximumLength(250) | Display name (max 250). Required on create. |
| `customerType` | `CustomerType?` |  |  | Customer classification. Defaults to Consumer on create. |
| `pricingType` | `CustomerPricingType?` |  |  | Pricing strategy. |
| `priceListId` | `int?` |  | NotNull() .GreaterThan(0) .When(x => x.PricingType == CustomerPricingType.Custom) | Price list id, used when `PricingType` is PriceList. |
| `taxRegistrationId` | `string` |  | MaximumLength(20) | Tax registration id (max 20). May be required for business customers. |
| `nationalId` | `string` |  | MaximumLength(14) | National identifier (max 14). May be required for consumer customers. |
| `shippingTerm` | `string` |  |  | Shipping terms (e.g. "FOB", "CIF"). |
| `insurance` | `string` |  |  | Insurance terms or notes. |
| `guarantee` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.Guarantee.HasValue) | Optional guarantee amount. |
| `creditLimit` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.CreditLimit.HasValue) | Optional credit limit. |
| `hasDiscount` | `bool?` |  |  | Whether line discounts are allowed. |
| `discountMandatory` | `bool?` |  |  | Whether discount is mandatory. |
| `discountFrom` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountFrom.HasValue) | Minimum allowed discount percentage. |
| `discountTo` | `decimal?` |  | GreaterThanOrEqualTo(0) .When(x => x.DiscountTo.HasValue) | Maximum allowed discount percentage. |
| `paymentType` | `CustomerPaymentType?` |  |  | Default payment type. |
| `paymentMaxDueDays` | `int?` |  | GreaterThanOrEqualTo(0) .When(x => x.PaymentMaxDueDays.HasValue) | Maximum credit days allowed. |
| `relatedAccountId` | `int?` |  |  | Existing receivable account id. Mutually exclusive with `RelatedAccountParentId`. |
| `relatedAccountParentId` | `int?` |  |  | AR parent CoA node under which a new related account is created. Only used when "create AR with new customer" is on. |
| `salesPersonId` | `int?` |  |  | Optional sales person id assigned to the customer. |
| `defaultForeignCurrencyId` | `int?` |  |  | Default foreign-currency id. |
| `externalId` | `string` |  | MaximumLength(50) | Optional external id (max 50). Must be unique across customers. |
| `active` | `bool?` |  |  | Whether the customer is active. Defaults to true on create. |
| `contactPerson` | `string` |  | MaximumLength(250) | Primary contact person name. |
| `contactCountryId` | `int?` |  |  | Primary contact country id. |
| `contactCityId` | `int?` |  |  | Primary contact city id. |
| `contactDistrictId` | `int?` |  |  | Primary contact district id. |
| `postalZipCode` | `string` |  | MaximumLength(50) | Primary contact postal / ZIP code. |
| `phone` | `string` |  | MaximumLength(50) | Primary contact phone (max 50). |
| `phone2` | `string` |  | MaximumLength(50) | Secondary contact phone (max 50). |
| `fax` | `string` |  | MaximumLength(50) | Fax number (max 50). |
| `mobile` | `string` |  | MaximumLength(50) | Primary contact mobile (max 50). May be required (CustomerMobileRequired). |
| `email` | `string` |  | MaximumLength(250) | Primary contact email (max 250). May be required (CustomerEmailRequired). |
| `tags` | `List<string>` |  |  | Customer tags. |
| `addresses` | `List<CustomerAddressUpsertRequest>` |  | each: SetValidator(new CustomerAddressUpsertRequestValidator()) | Customer addresses to create or update. |

**Responses**: OK `BatchResult<CustomerResponse>`: Batch update result.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteCustomerAddress`: An existing customer address cannot be removed because it is referenced by other records.
- `CityNotFound`: A city with the specified identifier was not found.
- `CountryNotFound`: A country with the specified identifier was not found.
- `CustomerAddressMustContainBuildingNumber`: Address must include a building number.
- `CustomerAddressMustHaveDistrict`: Address district is required.
- `CustomerAddressRequired`: Customer addresses are required.
- `CustomerCodeExists`: Another customer already uses the same code.
- `CustomerCodeRequired`: Customer code is required.
- `CustomerEmailExists`: Another customer already uses the same email.
- `CustomerEmailRequired`: Customer email is required.
- `CustomerMobileExists`: Another customer already uses the same mobile.
- `CustomerMobileRequired`: Customer mobile is required.
- `CustomerNameExists`: Another customer already uses the same name.
- `CustomerNationalIDRequired`: Customer national identifier is required.
- `CutomerMustHaveSingleDefaultAddress`: Customer cannot have more than one default address.
- `DistrictNotFound`: A district with the specified identifier was not found.
- `DuplicatedExternalId`: Another customer already uses the same external identifier.
- `DupplicatedName`: Another customer already uses the same name.
- `NationalIDExists`: Another customer already uses the same national identifier.
- `NationalIDMustBeOfLength`: National identifier length is invalid.
- `TaxRegisterationIDExists`: Another customer already uses the same tax registration identifier.
- `TaxRegisterationIDMustBeOfLength`: Tax registration identifier length is invalid.
- `TaxRegistrationIdRequired`: Customer tax registration identifier is required.

**Notes**

- Each item is matched by `code`, which is trimmed and compared exactly. Inactive customers are matched too.
- An item without `code` fails with "Customer Code is required."
- Each matched item is a full replace, exactly like PUT /v3/customers/{id}.
- The batch is not atomic and takes at most 1000 items. Failed items are reported in `failed`, keyed by code or name, and the response is `200` even when items fail.

---

### `DELETE /v3/customers/{id}`: Deletes a customer by id.

Operation `DeleteCustomerById` · permission `delete:customer`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The customer id. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `ItemCannotDeleteItInUse`: The customer is referenced by existing documents or related records.

**Notes**

- This permanently deletes the customer and all its addresses. An id that does not exist gives `404`.
- The customer's linked accounts receivable account is deleted too when no other customer uses it.
- A customer that is used elsewhere, for example on documents, cannot be deleted. You get `409` with `errorCode` `ItemCannotDeleteItInUse` and nothing is deleted, so deactivate customers that have history instead.

---

### `DELETE /v3/customers/code/{code}`: Deletes a customer by code.

Operation `DeleteCustomerByCode` · permission `delete:customer`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The customer code. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `ItemCannotDeleteItInUse`: The customer is referenced by existing documents or related records.

**Notes**

- The `code` is trimmed and must match exactly, and inactive customers are matched too. A code that matches no customer gives `404`.
- After the lookup, this behaves exactly like DELETE /v3/customers/{id}: the customer is permanently deleted.

---

### `PATCH /v3/customers/{id}/deactivate`: Deactivates a customer by id.

Operation `DeactivateCustomerById` · permission `update:customer-status`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The customer id. |

**Responses**: OK: Customer deactivated.

**Notes**

- An `id` of 0 or less gives `400`, and an id that does not exist gives `404`. Success returns `200` with the updated customer.
- Deactivation is refused with `409` and `errorCode` `CannotDeactivateCustomersWithNonZeroBalance` only when the balance is greater than 0. A customer with a negative (credit) balance can be deactivated.
- Deactivating does not change the customer's update date, so GET /v3/customers filtered by `updateDate` does not pick it up.
- The customer's linked accounts receivable account stays active.
- The balance check runs on every call, so repeating the call on an inactive customer succeeds only while the balance is 0 or less.
- There is no reactivate endpoint. To reactivate a customer, update it with PUT, where an omitted `active` becomes true.

---

### `PATCH /v3/customers/code/{code}/deactivate`: Deactivates a customer by code.

Operation `DeactivateCustomerByCode` · permission `update:customer-status`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The customer code. |

**Responses**: OK: Customer deactivated.

**Notes**

- The `code` is trimmed and must match exactly, and inactive customers are matched too. A code that matches no customer gives `404`.
- After the lookup, this behaves exactly like PATCH /v3/customers/{id}/deactivate: a balance greater than 0 blocks it, only the active state changes.

## Types

### `AccountReference`

Lightweight account reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The account id. |
| `code` | `string` | The account code. |
| `name` | `string` | The account name. |

### `CityReference`

Lightweight city reference (id + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The city id. |
| `name` | `string` | The city name. |

### `CountryReference`

Lightweight country reference (id + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The country id. |
| `name` | `string` | The country name. |

### `CurrencyReference`

Lightweight currency reference (id + ISO code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The currency id. |
| `code` | `string` | The ISO 4217 currency code. |
| `name` | `string` | The currency name. |

### `CustomerAddressResponse`

Response describing a customer address.

| field | type | description |
|---|---|---|
| `id` | `int` | Address identifier. |
| `customerId` | `int` | Owning customer identifier. |
| `name` | `string` | Address label. |
| `countryId` | `int` | Country identifier. |
| `cityId` | `int` | City identifier. |
| `districtId` | `int?` | District identifier, if any. |
| `streetDescription` | `string` | Street description (building number, floor, etc.). |
| `description` | `string` | Full printable address description. |
| `isDefault` | `bool` | Indicates whether this is the default address. |
| `addressPhone` | `string` | Address phone number. |
| `externalId` | `string` | External identifier for the address. |

### `CustomerAddressUpsertRequest`

Request to create or update a customer address.

| field | type | validation | description |
|---|---|---|---|
| `id` | `int?` |  | Existing address id. Null on create; required on update. Addresses absent from the request are deleted. |
| `name` | `string` | MaximumLength(250) | Address label. Defaults to "{Country}, {City}" when empty. |
| `countryId` | `int?` | NotNull() | Country identifier. Required. |
| `cityId` | `int?` | NotNull() | City identifier. Required. |
| `districtId` | `int?` |  | Optional district identifier. |
| `streetDescription` | `string` | MaximumLength(500) | Free-form street description. |
| `isDefault` | `bool` |  | Whether this is the customer's default address. Only one per customer; first is promoted if none set. |
| `addressPhone` | `string` | MaximumLength(50) | Optional address phone number. |
| `externalId` | `string` | MaximumLength(50) | Optional external identifier. |

### `CustomerAddressWithLocationResponse`

Response describing a customer address, including resolved location names.

| field | type | description |
|---|---|---|
| `id` | `int` | Address identifier. |
| `customerId` | `int` | Owning customer identifier. |
| `name` | `string` | Address label. |
| `country` | `CountryReference` | Country reference. |
| `city` | `CityReference` | City reference. |
| `district` | `DistrictReference` | District reference, if any. |
| `streetDescription` | `string` | Street description (building number, floor, etc.). |
| `description` | `string` | Full printable address description. |
| `isDefault` | `bool` | Indicates whether this is the default address. |
| `addressPhone` | `string` | Address phone number. |
| `externalId` | `string` | External identifier for the address. |

### `CustomerPaymentType`

Customer default payment method.

Values (sent/returned as the name): `Cash`=0, `Credit`=1

### `CustomerPricingType`

Customer pricing tier.

Values (sent/returned as the name): `EndUser`=0, `Dealer`=1, `SuperDealer`=2, `Custom`=3

### `CustomerResponse`

Response describing a customer.

| field | type | description |
|---|---|---|
| `id` | `int` | Customer identifier. |
| `code` | `string` | Customer business code. |
| `name` | `string` | Customer display name. |
| `customerType` | `CustomerType?` | Customer classification (Business / Consumer). |
| `pricingType` | `CustomerPricingType?` | Pricing strategy. |
| `priceListId` | `int?` | Identifier of the price list linked to this customer. |
| `nationalId` | `string` | National identifier. |
| `taxRegistrationId` | `string` | Tax registration identifier. |
| `shippingTerm` | `string` | Shipping terms applicable to the customer. |
| `insurance` | `string` | Insurance terms or notes. |
| `guarantee` | `decimal?` | Guarantee amount. |
| `creditLimit` | `decimal?` | Credit limit value. |
| `hasDiscount` | `bool?` | Indicates whether discounts are allowed. |
| `discountMandatory` | `bool?` | Indicates whether a discount is mandatory. |
| `discountFrom` | `decimal?` | Minimum discount percentage. |
| `discountTo` | `decimal?` | Maximum discount percentage. |
| `paymentType` | `CustomerPaymentType?` | Default payment type. |
| `paymentMaxDueDays` | `int?` | Maximum number of credit days allowed. |
| `relatedAccount` | `AccountReference` | Reference to the linked receivable account. |
| `salesPerson` | `SalesPersonReference` | Reference to the assigned sales person, if any. |
| `defaultForeignCurrency` | `CurrencyReference` | Default foreign currency reference, if any. |
| `balance` | `decimal?` | Current customer balance (read-only). |
| `active` | `bool?` | Indicates whether the customer is active. |
| `externalId` | `string` | External identifier. |
| `contactPerson` | `string` | Name of the primary contact person at the customer. |
| `contactCountryId` | `int?` | Country identifier of the primary contact address. |
| `contactCityId` | `int?` | City identifier of the primary contact address. |
| `contactDistrictId` | `int?` | District identifier of the primary contact address. |
| `postalZipCode` | `string` | Postal / ZIP code for the primary contact address. |
| `phone` | `string` | Primary contact phone number. |
| `phone2` | `string` | Secondary contact phone number. |
| `fax` | `string` | Fax number. |
| `mobile` | `string` | Primary contact mobile number. |
| `email` | `string` | Primary contact email. |
| `tags` | `List<string>` | Tags associated with the customer. |
| `addresses` | `List<CustomerAddressResponse>` | Customer addresses. |

### `CustomerType`

Customer classification type.

Values (sent/returned as the name): `Consumer`=0, `Business`=1

### `DistrictReference`

Lightweight district reference (id + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The district id. |
| `name` | `string` | The district name. |

### `InvoiceableDocumentResponse`

Response describing an invoiceable source document header (SO/SR/IO/RR).

| field | type | description |
|---|---|---|
| `code` | `string` | The source document code. |
| `type` | `string` | The source document type. |
| `date` | `DateTime` | The document date. |
| `customer` | `string` | The customer name. |
| `currency` | `string` | The document currency code. |
| `netTotal` | `decimal` | The document net total. For IO/RR this is computed from Warehouse.WHWorkOrderDetails (SUM(Quantity * Value)). |

### `SalesPersonReference`

Lightweight sales-person reference (id + code + name).

| field | type | description |
|---|---|---|
| `id` | `int` | The sales-person id. |
| `code` | `string` | The sales-person code. |
| `name` | `string` | The sales-person name. |
