# Edara API v3: Dynamic Properties

Read the [guide](../guide.md) first: authentication, paging, errors, rate limits and the shared `PagedResult`, `BatchResult` and `*Reference` shapes.

- `GET /v3/dynamic-properties`: Lists dynamic properties.
- `GET /v3/dynamic-properties/tree/{userId}`: Gets the dynamic property tree for a user.
- `POST /v3/dynamic-properties/batch`: Creates dynamic properties in batch.
- `PUT /v3/dynamic-properties/{id}`: Updates a dynamic property.

## Endpoints

### `GET /v3/dynamic-properties`: Lists dynamic properties.

Operation `GetDynamicProperties` · permission `read:dynamic-property`

> - `offset` must be zero or greater; omit to use the default (0).
> - `limit` must be between 1 and 1000; omit to use the default (100).

**Query string**: `GetDynamicPropertiesQuery`

| 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. |
| `names` | `List<string>` |  |  | Optional names to filter by. |

**Responses**: OK `PagedResult<DynamicPropertyResponse>`: Paged list of dynamic properties.

**Business errors** (HTTP 409, match on `errorCode`):

- `DynamicPropertiesDisabled`: The warehouse setting UseDynamicPropertiesInStockitems is disabled.

**Notes**

- Dynamic properties depend on an organization setting. When it is off, you get `409` with `errorCode` `DynamicPropertiesDisabled`, not an empty list, so handle both cases.
- `names` matches property names exactly, ignoring case. Each name is trimmed, and blank or repeated names are ignored.
- Paging counts properties, ordered by id, not values. Each property on the page comes with all its values, and `totalCount` is the number of matching properties.
- `page` in the response is calculated as `offset` divided by `limit`, plus 1.

---

### `GET /v3/dynamic-properties/tree/{userId}`: Gets the dynamic property tree for a user.

Operation `GetDynamicPropertiesTree` · permission `read:dynamic-property`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `userId` | route | `int` |  | The user id. |

**Responses**: OK `List<DynamicPropertyTreeResponse>`: Dynamic property tree.

**Notes**

- The response is a flat list ordered by id, not a nested tree. Properties have `treeLevel` 1 and their values have `treeLevel` 2.
- Unlike the list endpoint, this one does not depend on the organization's dynamic properties setting.

---

### `POST /v3/dynamic-properties/batch`: Creates dynamic properties in batch.

Operation `BatchInsertDynamicProperties` · permission `create:dynamic-property`

> Per-item errors returned in the response `failed` map:
> CodeRequiredForPropertyWhenCodingIsFree - The warehouse coding method is free coding and the property code is missing.
> CodeRequiredForPropertyWhenNoCoding - The warehouse coding method is no coding and the property code is missing.
> CodeRequiredForValueWhenCodingIsFree - The warehouse coding method is free coding and a value code is missing.
> CodeRequiredForValueWhenNoCoding - The warehouse coding method is no coding and a value code is missing.
> AlreadyExists - Another dynamic property with the same name already exists.

**Body**: `List<DynamicPropertyBatchInsertRequest>` (JSON array)

| field | type | default | validation | description |
|---|---|---|---|---|
| `name` | `string` |  | NotEmpty()  .MaximumLength(70) | Display name (e.g. "Color", "Size"). |
| `code` | `string` |  | MaximumLength(50) | Optional code. Required when coding method is `FreeCoding` or `NoCoding`. |
| `values` | `List<DynamicPropertyValueUpsertRequest>` |  | NotEmpty(); each: SetValidator(new DynamicPropertyValueUpsertRequestValidator()) | Values associated with the property. At least one required. |

**Responses**: OK `DynamicPropertyBatchInsertResponse`: Batch insert result.

**Business errors** (HTTP 409, match on `errorCode`):

- `DynamicPropertiesDisabled`: The warehouse setting UseDynamicPropertiesInStockitems is disabled.

**Notes**

- Validation errors return `400`. Each error is keyed by the item's position in the batch and the field name, in the form [index].field.
- When the organization setting for using dynamic properties on stock items is off, the whole batch fails with `409` and `errorCode` `DynamicPropertiesDisabled`.
- Duplicate property names inside one batch are dropped without any report. Names are compared without regard to case, the first one wins, and the others appear in neither `succeeded` nor `failed`.
- Duplicate values inside one property are removed in the same silent way.
- An organization setting for item coding decides whether you supply codes or Edara generates them. Where you supply them, a property that lacks its own code or a code for any of its values is reported in `failed`. Where Edara generates them, any codes you send are ignored. This differs between organizations, so handle both cases.
- A property whose name already exists, compared without regard to case, is reported in `failed` with a message that contains `AlreadyExists`.
- `failed` is keyed by property name. Each value is a free-text message with the error code inside it, for example "Bad Request. `CodeRequiredForValueWhenNoCoding`: value 'X'", so do not compare the value to a bare code.
- Unexpected failures also return `409`, with `errorCode` `BusinessRuleViolation` unless a more specific code applies. This endpoint does not return `500` for them.

---

### `PUT /v3/dynamic-properties/{id}`: Updates a dynamic property.

Operation `UpdateDynamicPropertyById` · permission `update:dynamic-property`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The dynamic property id. |

**Body**: `DynamicPropertyValuesUpdateRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `values` | `List<DynamicPropertyValueUpsertRequest>` |  | NotEmpty(); each: SetValidator(new DynamicPropertyValueUpsertRequestValidator()) | Full desired set of values; omitted values are deleted. |

**Responses**: OK `DynamicPropertyResponse`: The updated dynamic property.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotDeleteInUse`: One of the removed values is still referenced by stock items.
- `CodeRequiredForValueWhenCodingIsFree`: The warehouse coding method is free coding and a value code is missing.
- `CodeRequiredForValueWhenNoCoding`: The warehouse coding method is no coding and a value code is missing.
- `DynamicPropertiesDisabled`: The warehouse setting UseDynamicPropertiesInStockitems is disabled.
- `InvalidParentId`: The dynamic property identifier does not exist.

**Notes**

- An id that does not exist, or that is not a dynamic property, returns `409` with `errorCode` `InvalidParentId`. This endpoint does not return `404`.
- Other unexpected failures also return `409`, not `500`.
- When the organization setting for using dynamic properties on stock items is off, the request returns `409` with `errorCode` `DynamicPropertiesDisabled`.
- In organizations where you supply codes yourself, a value without a code returns `409`.
- `values` is the full set you want to end up with, matched to the stored values by value name without regard to case. Stored values that are not in the list are deleted, and new names are added.
- A value that matches a stored one gets its code updated. In organizations where Edara generates codes, the stored code is kept.
- If a value to be removed is used by a stock item, the request returns `409` with `errorCode` `CannotDeleteInUse`.
- Any other failure to remove a value is not reported: the update continues, and that value can remain.
- You cannot rename a property with this endpoint, because the request body has no name.

## Types

### `DynamicPropertyBatchInsertResponse`

Response describing the result of a dynamic-property batch insert.

| field | type | description |
|---|---|---|
| `failed` | `Dictionary<string,string>` | Failed items with their error messages. |
| `succeeded` | `List<DynamicPropertyResponse>` | Successfully inserted dynamic properties. |

### `DynamicPropertyResponse`

Response describing a dynamic property.

| field | type | description |
|---|---|---|
| `id` | `int` | Identifier. |
| `name` | `string` | Property name. |
| `code` | `string` | Optional property code. |
| `values` | `List<DynamicPropertyValueResponse>` | Property values. |

### `DynamicPropertyTreeResponse`

Response describing a dynamic-property tree node.

| field | type | description |
|---|---|---|
| `dynamicPropertyId` | `int` | Dynamic property identifier. |
| `parentId` | `int` | Parent identifier. |
| `description` | `string` | Description. |
| `isVisible` | `bool` | Whether the node is visible. |
| `treeLevel` | `int` | Tree level. |

### `DynamicPropertyValueResponse`

Response describing a dynamic-property value.

| field | type | description |
|---|---|---|
| `id` | `int` | Identifier. |
| `value` | `string` | Value text. |
| `code` | `string` | Optional code. |

### `DynamicPropertyValueUpsertRequest`

Request to create or update a dynamic-property value.

| field | type | validation | description |
|---|---|---|---|
| `value` | `string` | NotEmpty()  .MaximumLength(70) | Display text (e.g. "Red", "Large"). |
| `code` | `string` | MaximumLength(50) | Optional code. Required when coding method is `FreeCoding` or `NoCoding`. |
