# Qisutu REST API reference

This document describes the external HTTP interface implemented by the Qisutu core. The API is local to each Qisutu installation, versioned independently from the Qisutu release, and uses JSON unless an attachment is downloaded.

## Base URL and discovery

For an installation with the web path `/qisutu`, the base URL is:

`https://HOST/qisutu/api.pl/v1`

`GET /ping` and `GET /openapi.json` are public. The latter returns the OpenAPI 3.0.3 route description generated by the running installation, including routes supplied by installed add-ons. All other core routes require an API token.

## Authentication and request rules

An administrator creates API tokens under `Administration > API`. Send a token as `Authorization: Bearer TOKEN`. The legacy `X-Qisutu-API-Token: TOKEN` header is also accepted. Token scopes, account permissions, optional source-IP restrictions, expiry, activation state, and per-minute rate limits are enforced together.

Requests with a JSON body must use `Content-Type: application/json`. The body must be a JSON object and may not exceed 50 MiB. List query values are URL encoded. Responses are UTF-8. Successful JSON responses use `{"data": ...}` and may add `"meta"`. Error responses use:

`{"error":{"code":"CODE","message":"MESSAGE","request_id":"REQUEST-ID"}}`

Every response includes `X-Request-ID`. Common statuses are 200, 201, 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, and 503. A 429 response means that the token rate limit was exceeded. A 503 response can indicate installation, maintenance, configuration, or database availability problems.

## Scopes

- `tickets.read`: read tickets and articles visible to the token user.
- `tickets.create`: create tickets.
- `tickets.status`: change ticket state and pending time.
- `tickets.properties`: change queue, priority, service, customer user, owner, or responsible agent.
- `tickets.articles`: add ticket articles.
- `tickets.internal_notes`: create agent-only notes.
- `tickets.attachments`: add or download attachments.
- `master_data.read`: read queues, states, priorities, services, SLAs, and dynamic fields.
- `customers.read`: list customers and customer users.
- `customers.write`: create or change customers and customer users; the associated Qisutu user must also be an administrator.

Add-ons may register additional routes and scopes. They appear in the running installation's OpenAPI document.

## Public endpoints

### `GET /ping`

Returns `data.status` with the value `ok` and `data.version` with the installed Qisutu version.

### `GET /openapi.json`

Returns the generated OpenAPI 3.0.3 document for core and installed add-on routes.

## Identity

### `GET /me`

Returns the authenticated API identity: `id`, `login`, `email`, `firstname`, `lastname`, `account_type`, `token_label`, and the effective `scopes` array.

## Tickets

### `GET /tickets`

Requires `tickets.read`. Optional query parameters are `page` (default 1), `per_page` (default 50, maximum 100), `query`, `queue_id`, `state_id`, `priority_id`, `customer_id`, `customer_user_id`, `owner_user_id`, `service_id`, `sort_by` (default `changed`), and `sort_direction` (default `desc`).

`data` is an array of ticket objects. `meta` contains `page`, `per_page`, and `total`. A ticket object can contain `id`, `ticket_number`, `title`, queue, state, priority, customer, customer-user, owner, responsible-agent, service and SLA identifiers and names, `pending_until`, escalation fields, due timestamps, `created_at`, and `changed_at`.

### `GET /tickets/{ticket_id}`

Requires `tickets.read` and permission to view the ticket. Returns the ticket object described above plus `dynamic_fields`, an array whose entries contain `field_id`, `name`, `label`, `field_type`, and `value`.

### `POST /tickets`

Requires `tickets.create`. Required fields for every account are `queue_id`, `title`, and `body`. Agent accounts must also provide `customer_user_id`. Optional fields are `content_type` (default `text/plain`), `service_id`, `owner_user_id`, `responsible_user_id`, `state_id`, `priority_id`, `pending_until`, `dynamic_fields`, and `attachments`.

`dynamic_fields` is an object whose keys are numeric dynamic-field IDs. `attachments` is an array of objects containing `filename`, `content_base64`, and optional `content_type` (default `application/octet-stream`). Customer accounts create the ticket for their own customer identity; agent-only assignment and state fields are not applied to customer-created tickets.

Returns status 201 and the created ticket object.

### `PATCH /tickets/{ticket_id}`

Requires permission to edit the ticket and at least one supported field. `state_id` and `pending_until` require `tickets.status`; `pending_until` is only accepted together with `state_id`. The fields `priority_id`, `queue_id`, `service_id`, `customer_user_id`, `owner_user_id`, and `responsible_user_id` require `tickets.properties`. Unsupported fields cause status 422. Returns the updated ticket object.

## Articles and attachments

### `GET /tickets/{ticket_id}/articles`

Requires `tickets.read` and ticket visibility. Returns `data` as an array and `meta.total`. Each article contains `id`, `ticket_id`, `article_number`, `channel`, `sender_type`, `visibility`, sender and recipient names and addresses, `cc`, `subject`, `body`, `content_type`, `created_at`, and `attachments`. Attachment metadata contains `id`, `filename`, `content_type`, and `content_size`.

### `POST /tickets/{ticket_id}/articles`

Requires `tickets.articles`. The field `body` is required. Optional fields are `subject`, `content_type` (default `text/plain`), `internal`, and `attachments` in the same format used for ticket creation. Internal notes additionally require `tickets.internal_notes` and are never created as internal by a customer account. Adding attachments requires `tickets.attachments`.

Returns status 201 with `data.id` and `data.ticket_id`.

### `GET /attachments/{attachment_id}`

Requires `tickets.attachments` and visibility of the associated ticket. Returns the original bytes rather than a JSON envelope. `Content-Type` and `Content-Disposition` describe the download; `Cache-Control` is `private, no-store`.

## Master data

### `GET /master-data/{type}`

Requires `master_data.read`. Supported types are `queues`, `states`, `priorities`, `services`, `slas`, and `dynamic-fields`. Returns `data` as an array and `meta.total`. Queue results are restricted to queues the user can view. Dynamic-field results include their available options.

## Customers and customer users

These routes require the named scope and an administrator account.

### `GET /customers`

Requires `customers.read`. Returns the customer list.

### `POST /customers`

Requires `customers.write`. Input fields are `customer_number`, `name`, and optional `dynamic_fields`, whose keys are numeric field IDs. Returns status 201 with `id`, `customer_number`, and `name`. The underlying validation rejects missing, invalid, or duplicate values with status 422.

### `PATCH /customers/{customer_id}`

Requires `customers.write`. Accepted fields are `customer_number`, `name`, `active`, and `dynamic_fields`. Omitted standard fields retain their existing values. Returns the updated customer object.

### `GET /customer-users`

Requires `customers.read`. Returns the customer-user list.

### `POST /customer-users`

Requires `customers.write`. Required fields are `customer_id`, `login`, `email`, and `password`; the password must contain 8 to 128 characters. Optional fields are `firstname`, `lastname`, and `dynamic_fields`. Returns status 201 with `id`, `login`, and `email`.

### `PATCH /customer-users/{customer_user_id}`

Requires `customers.write`. Accepted fields are `customer_id`, `login`, `email`, `password`, `firstname`, `lastname`, `active`, and `dynamic_fields`. An omitted or empty password leaves the password unchanged. Other omitted standard fields retain their existing values. Returns the updated customer-user object without returning a password.

## Compatibility

Clients should use only documented fields, accept additional response fields, and treat identifiers as opaque integers. Breaking API changes require a new URL version. The current core URL version is `v1`.
