# Edara API v3: Service Items

Read the [guide](../guide.md) first: authentication, paging, errors, rate limits and the shared `PagedResult`, `BatchResult` and `*Reference` shapes.

- `GET /v3/service-items`: Lists service items.
- `GET /v3/service-items/{id}`: Gets a service item by id.
- `POST /v3/service-items`: Creates a service item.
- `PUT /v3/service-items/{id}`: Updates a service item by id.
- `PUT /v3/service-items/code/{code}`: Updates a service item by code.
- `DELETE /v3/service-items/{id}`: Deletes a service item by id.
- `DELETE /v3/service-items/code/{code}`: Deletes a service item by code.

## Endpoints

### `GET /v3/service-items`: Lists service items.

Operation `GetServiceItems` · permission `read:service-item`

**Query string**: `GetServiceItemsQuery`

| 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` |  | Must(code => !string.IsNullOrWhiteSpace(code)) .When(x => x.Code != null) | CONTAINS filter on code. |
| `name` | `string` |  | Must(name => !string.IsNullOrWhiteSpace(name)) .When(x => x.Name != null) | CONTAINS filter on description. |
| `externalId` | `string` |  | Must(externalId => !string.IsNullOrWhiteSpace(externalId)) .When(x => x.ExternalId != null) | External id equality filter. |
| `search` | `string` |  | Must(search => !string.IsNullOrWhiteSpace(search)) .When(x => x.Search != null) | CONTAINS filter across code or description. |
| `updateDate` | `DateTime?` |  |  | Returns service items inserted or updated strictly after this value. |

**Responses**: OK `PagedResult<ServiceItemResponse>`: Paged list of service items.

**Notes**

- Filter values are trimmed, and a blank filter is treated as not sent.
- A negative `offset` is treated as 0 and a `limit` outside 1 to 1000 is treated as 100, without an error.
- An empty page returns `200` with an empty `items` and `totalCount` 0, never `404`. This also happens when `offset` is past the last item.
- `tax` and `withholdingTax` come back as reference objects. If the tax cannot be resolved, the reference contains only `id`.

---

### `GET /v3/service-items/{id}`: Gets a service item by id.

Operation `GetServiceItemById` · permission `read:service-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The service item id. |

**Responses**: OK `ServiceItemResponse`: The service item.

---

### `POST /v3/service-items`: Creates a service item.

Operation `CreateServiceItem` · permission `create:service-item`

**Body**: `ServiceItemUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `code` | `string` |  | MaximumLength(128) | Unique business code. |
| `description` | `string` |  | NotEmpty() .MaximumLength(255) | Display description. |
| `price` | `decimal` |  |  | Default selling price. |
| `taxId` | `int?` |  |  | Sales/purchase tax id. |
| `withholdingTaxId` | `int?` |  |  | Withholding tax id. |
| `cost` | `decimal` |  |  | Service cost, used in margin calculations. |
| `isShippingService` | `bool` |  |  | Whether the service item is treated as a shipping/delivery line. |
| `externalId` | `string` |  | MaximumLength(128) | External system identifier. |

**Responses**: Created `ServiceItemResponse`: Service item created.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotAssignInactiveTax`: The referenced tax is inactive.
- `DupplicatedCode`: Another service item already uses the same code.
- `DupplicatedName`: Another service item already uses the same description.
- `InvalidSpecialCharacters`: The description contains characters blocked by the system.
- `InvalidTaxScopeForItem`: The referenced tax is scoped to stock items.
- `InvalidTaxTypeForItem`: The referenced tax has no sales/purchase (or withholding) account configured.
- `MustSelectValidTaxType`: Egypt e-invoicing/e-receipt is connected and a tax must be supplied.
- `TaxNotFound`: The referenced tax does not exist.

**Notes**

- `isShippingService` is accepted but not saved.
- Special characters in the description are rejected with `InvalidSpecialCharacters`, unless an organization setting allows them. This can differ between organizations.
- If the organization is connected to electronic invoicing or electronic receipts, `taxId` is required. Without it you get `MustSelectValidTaxType`.
- A tax you send in `taxId` or `withholdingTaxId` must exist (`TaxNotFound`) and be active (`CannotAssignInactiveTax`). Its scope cannot be `StockItem` (`InvalidTaxScopeForItem`).
- The tax in `taxId` must have a sales or purchase account, and the tax in `withholdingTaxId` must have a withholding account. Otherwise you get `InvalidTaxTypeForItem`.
- `taxRate` and `withholdingTaxRate` in the response are copied from the tax, not from your request.
- A duplicate description gives `DupplicatedName`, and a duplicate `code` gives `DupplicatedCode`.
- A new service item is always active. `isActive` and `tags` cannot be set with this endpoint.
- The user that creates the service item is automatically given data permission for it.

---

### `PUT /v3/service-items/{id}`: Updates a service item by id.

Operation `UpdateServiceItemById` · permission `update:service-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The service item id. |

**Body**: `ServiceItemUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `code` | `string` |  | MaximumLength(128) | Unique business code. |
| `description` | `string` |  | NotEmpty() .MaximumLength(255) | Display description. |
| `price` | `decimal` |  |  | Default selling price. |
| `taxId` | `int?` |  |  | Sales/purchase tax id. |
| `withholdingTaxId` | `int?` |  |  | Withholding tax id. |
| `cost` | `decimal` |  |  | Service cost, used in margin calculations. |
| `isShippingService` | `bool` |  |  | Whether the service item is treated as a shipping/delivery line. |
| `externalId` | `string` |  | MaximumLength(128) | External system identifier. |

**Responses**: OK `ServiceItemResponse`: The updated service item.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotAssignInactiveTax`: The referenced tax is inactive.
- `DescriptionContainsInvalidSpecialCharacters`: The description contains characters blocked by the system.
- `DupplicatedCode`: Another service item already uses the same code.
- `DupplicatedName`: Another service item already uses the same description.
- `InvalidTaxScopeForItem`: The referenced tax is scoped to stock items.
- `InvalidTaxTypeForItem`: The referenced tax has no sales/purchase (or withholding) account configured.
- `MustSelectValidTaxType`: Egypt e-invoicing/e-receipt is connected and a tax must be supplied.
- `TaxNotFound`: The referenced tax does not exist.

**Notes**

- This is a full replace. An omitted `externalId` is cleared, an omitted `price` or `cost` becomes 0, and an omitted `taxId` or `withholdingTaxId` removes that tax.
- `code` is the exception: if you omit it or send it empty, the stored code is kept.
- `isActive` and `tags` are not changed by this endpoint, and `isShippingService` is ignored.
- The description, tax and duplicate rules are the same as on create. The one difference is that special characters in the description give `DescriptionContainsInvalidSpecialCharacters`.
- `taxRate` and `withholdingTaxRate` are recalculated from the tax.

---

### `PUT /v3/service-items/code/{code}`: Updates a service item by code.

Operation `UpdateServiceItemByCode` · permission `update:service-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The service item code. |

**Body**: `ServiceItemUpsertRequest`

| field | type | default | validation | description |
|---|---|---|---|---|
| `code` | `string` |  | MaximumLength(128) | Unique business code. |
| `description` | `string` |  | NotEmpty() .MaximumLength(255) | Display description. |
| `price` | `decimal` |  |  | Default selling price. |
| `taxId` | `int?` |  |  | Sales/purchase tax id. |
| `withholdingTaxId` | `int?` |  |  | Withholding tax id. |
| `cost` | `decimal` |  |  | Service cost, used in margin calculations. |
| `isShippingService` | `bool` |  |  | Whether the service item is treated as a shipping/delivery line. |
| `externalId` | `string` |  | MaximumLength(128) | External system identifier. |

**Responses**: OK `ServiceItemResponse`: The updated service item.

**Business errors** (HTTP 409, match on `errorCode`):

- `CannotAssignInactiveTax`: The referenced tax is inactive.
- `DescriptionContainsInvalidSpecialCharacters`: The description contains characters blocked by the system.
- `DupplicatedCode`: Another service item already uses the same code.
- `DupplicatedName`: Another service item already uses the same description.
- `InvalidTaxScopeForItem`: The referenced tax is scoped to stock items.
- `InvalidTaxTypeForItem`: The referenced tax has no sales/purchase (or withholding) account configured.
- `MustSelectValidTaxType`: Egypt e-invoicing/e-receipt is connected and a tax must be supplied.
- `TaxNotFound`: The referenced tax does not exist.

**Notes**

- The `code` in the path is trimmed and matched exactly. No match returns `404`.
- Apart from the lookup, this behaves like the update by id, including the full replace. A body `code` that is not empty renames the service item.

---

### `DELETE /v3/service-items/{id}`: Deletes a service item by id.

Operation `DeleteServiceItemById` · permission `delete:service-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `id` | route | `int` |  | The service item id. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `ItemCannotDeleteItInUse`: The service item is referenced by existing documents or related records.

**Notes**

- A service item that is still used, for example on documents or sales order lines, cannot be deleted. The request fails with `ItemCannotDeleteItInUse`.

---

### `DELETE /v3/service-items/code/{code}`: Deletes a service item by code.

Operation `DeleteServiceItemByCode` · permission `delete:service-item`

**Parameters**

| name | in | type | default | description |
|---|---|---|---|---|
| `code` | route | `string` |  | The service item code. |

**Responses**: NoContent: (not declared; read from the method body)

**Business errors** (HTTP 409, match on `errorCode`):

- `ItemCannotDeleteItInUse`: The service item is referenced by existing documents or related records.

**Notes**

- The `code` in the path is trimmed and matched exactly. No match returns `404`.
- A service item that is still used cannot be deleted and fails with `ItemCannotDeleteItInUse`, as in the delete by id.

## Types

### `ServiceItemResponse`

Response describing a service item.

| field | type | description |
|---|---|---|
| `id` | `int` | Service-item identifier. |
| `code` | `string` | Unique business code. |
| `description` | `string` | Display description. |
| `price` | `decimal` | Default selling price. |
| `tax` | `TaxReference` | Sales/purchase tax reference. |
| `taxRate` | `decimal` | Effective tax rate percentage. |
| `withholdingTax` | `TaxReference` | Withholding tax reference. |
| `cost` | `decimal` | Service cost, used in margin calculations. |
| `isShippingService` | `bool` | Whether the service item is treated as a shipping/delivery line. |
| `externalId` | `string` | External system identifier. |

### `TaxReference`

Lightweight tax reference (id + name + rate).

| field | type | description |
|---|---|---|
| `id` | `int` | The tax id. |
| `name` | `string` | The tax name. |
| `rate` | `decimal` | The tax rate as a percentage. |
