# Edara API v3: Cities

Read the [guide](../guide.md) first: authentication, paging, errors, rate limits and the shared `PagedResult`, `BatchResult` and `*Reference` shapes.

- `GET /v3/cities`: Lists cities.
- `GET /v3/cities/{id}`: Gets a city by id.
- `POST /v3/cities`: Creates a city.
- `PUT /v3/cities/{id}`: Updates a city.
- `DELETE /v3/cities/{id}`: Deletes a city.

## Endpoints

### `GET /v3/cities`: Lists cities.

Operation `GetCities` · permission `read:city`

**Query string**: `GetCitiesQuery`

| 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 city name filter. |
| `countryId` | `int?` |  |  | Optional country identifier filter. |

**Responses**: OK `PagedResult<CityResponse>`: Paged list of cities.

**Notes**

- `name` is a partial match: it finds every city whose name contains the text you send. A `name` that is only whitespace is ignored.
- `countryId` is an exact match.
- There are no hidden default filters. Omitting `name` and `countryId` does not narrow the list.
- Results are ordered by `name` ascending.
- `totalCount` is the number of cities 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.
- `page` in the response is `offset` divided by `limit`, plus 1.

---

### `GET /v3/cities/{id}`: Gets a city by id.

Operation `GetCityById` · permission `read:city`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The city id. |

**Responses**: OK `CityResponse`: The city.

**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/cities`: Creates a city.

Operation `CreateCity` · permission `create:city`

**Body**: `CityUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `name` | `string` |  | NotEmpty() .NotNull() .MaximumLength(100) | The city name. |
| `countryId` | `int` |  | GreaterThan(0) | The country identifier. |

**Responses**: Created `CityResponse`: The created city.

**Business errors** (HTTP 409, match on `errorCode`):

- `DupplicatedName`: Another city already uses the same name.

**Notes**

- `name` is trimmed before it is saved.
- Only `name` and `countryId` are read from the body. Anything else you send is ignored.
- A `countryId` that does not exist returns `400` with no `errorCode`.
- A city name must be unique within its country. A duplicate returns `409` with `errorCode` `DupplicatedName`, and the same name in a different country is allowed.

---

### `PUT /v3/cities/{id}`: Updates a city.

Operation `UpdateCity` · permission `update:city`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The city id. |

**Body**: `CityUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `name` | `string` |  | NotEmpty() .NotNull() .MaximumLength(100) | The city name. |
| `countryId` | `int` |  | GreaterThan(0) | The country identifier. |

**Responses**: OK `CityResponse`: The updated city.

**Business errors** (HTTP 409, match on `errorCode`):

- `DupplicatedName`: Another city already uses the same name.

**Notes**

- This is a full replace, not a partial update. Send both `name` and `countryId` every time; leaving out `countryId` returns `400`.
- The `id` in the path is the one used. An `id` in the body is ignored.
- An `id` that does not exist returns `404` and nothing is changed.
- A `name` already used by another city in the same country returns `409` with `errorCode` `DupplicatedName`.
- A `countryId` that does not exist returns `400` with no `errorCode`.

---

### `DELETE /v3/cities/{id}`: Deletes a city.

Operation `DeleteCity` · permission `delete:city`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The city id. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `ItemCannotDeleteItInUse`: The city is still referenced by related records and cannot be deleted.

**Notes**

- The delete is permanent. Cities have no deactivated or soft-deleted state.
- An `id` that does not exist returns `404`. Success returns `204` with an empty body.
- A city that is still in use returns `409` with `errorCode` `ItemCannotDeleteItInUse`. A city is in use when a district, customer, customer address, warehouse or account refers to it.
- Delete a city's districts before you delete the city.

## Types

### `CityResponse`

Response describing a city.

| field | type | description |
|---|---|---|
| `id` | `int` | The city identifier. |
| `name` | `string` | The city name. |
| `countryId` | `int` | The country identifier. |
