# Edara API v3: Districts

Read the [guide](../guide.md) first: authentication, paging, errors, rate limits and the shared `PagedResult`, `BatchResult` and `*Reference` shapes.

- `GET /v3/districts`: Lists districts.
- `GET /v3/districts/{id}`: Gets a district by id.
- `POST /v3/districts`: Creates a district.
- `PUT /v3/districts/{id}`: Updates a district.
- `DELETE /v3/districts/{id}`: Deletes a district.

## Endpoints

### `GET /v3/districts`: Lists districts.

Operation `GetDistricts` · permission `read:district`

**Query string**: `GetDistrictsQuery`

| 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. |
| `name` | `string` |  |  | Optional district name filter. |
| `cityId` | `int?` |  | GreaterThan(0) .When(x => x.CityId.HasValue) | Optional city identifier filter. |

**Responses**: OK `PagedResult<DistrictResponse>`: Paged list of districts.

**Notes**

- `name` is a partial match: it finds every district whose name contains the text you send. A `name` that is only whitespace is ignored.
- `cityId` is an exact match. There are no other filters.
- Results are ordered by `name`, then by `id`.
- `totalCount` is the number of districts that match the filters, not the number on the page.
- When nothing matches you get `200` with an empty `items` list and `totalCount` 0.
- Tab characters are removed from `name` in this list. The get-by-id endpoint returns `name` as stored, so the two can differ.

---

### `GET /v3/districts/{id}`: Gets a district by id.

Operation `GetDistrictById` · permission `read:district`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The district id. |

**Responses**: OK `DistrictResponse`: The district.

**Notes**

- An `id` that does not exist returns `404`.
- `name` is returned exactly as stored. The list endpoint removes tab characters from `name`, so the two can differ.

---

### `POST /v3/districts`: Creates a district.

Operation `CreateDistrict` · permission `create:district`

**Body**: `DistrictUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `name` | `string` |  | NotEmpty() .NotNull() .MaximumLength(100) | The district name. |
| `cityId` | `int` |  | GreaterThan(0) | The parent city identifier. |

**Responses**: Created `DistrictResponse`: The created district.

**Business errors** (HTTP 409, match on `errorCode`):

- `DupplicatedName`: Another district already uses the same name in the same city.

**Notes**

- Only `name` and `cityId` are read from the body. `name` is trimmed before it is saved.
- A `cityId` that does not exist returns `400` with no `errorCode`.
- A district name must be unique within its city. A duplicate returns `409` with `errorCode` `DupplicatedName`, and the same name under a different city is allowed.

---

### `PUT /v3/districts/{id}`: Updates a district.

Operation `UpdateDistrict` · permission `update:district`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The district id. |

**Body**: `DistrictUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `name` | `string` |  | NotEmpty() .NotNull() .MaximumLength(100) | The district name. |
| `cityId` | `int` |  | GreaterThan(0) | The parent city identifier. |

**Responses**: OK `DistrictResponse`: The updated district.

**Business errors** (HTTP 409, match on `errorCode`):

- `DupplicatedName`: Another district already uses the same name in the same city.

**Notes**

- This is a full replace, not a partial update. Send both `name` and `cityId` every time; leaving out `cityId` returns `400`.
- An `id` that does not exist returns `404` before anything is changed.
- A `name` already used by another district in the same city returns `409` with `errorCode` `DupplicatedName`.
- A `cityId` that does not exist returns `400` with no `errorCode`.

---

### `DELETE /v3/districts/{id}`: Deletes a district.

Operation `DeleteDistrict` · permission `delete:district`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The district id. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `ItemCannotDeleteItInUse`: The district is still referenced by related records and cannot be deleted.

**Notes**

- The delete is permanent.
- An `id` that does not exist returns `404`. Success returns `204`.
- A district that is still in use returns `409` with `errorCode` `ItemCannotDeleteItInUse`. A district is in use when a street, customer, customer address, warehouse or account refers to it.

## Types

### `DistrictResponse`

Response describing a district.

| field | type | description |
|---|---|---|
| `id` | `int` | The district identifier. |
| `name` | `string` | The district name. |
| `cityId` | `int` | The city identifier. |
