# jeCheck API & MCP

> REST API: https://app.jecheck.io/api/ext/v1 · MCP: https://app.jecheck.io/mcp · OpenAPI: https://jecheck.io/developers/openapi.json

Authenticate with `Authorization: Bearer <token>`. Responses: `{"data": …, "meta": …}`; errors: `{"error": {"code", "message", "fields"}}`.


Make your first jeCheck API call in under five minutes. You need a jeCheck company on the **Enterprise** plan and a user who may manage API keys (administrators can).

## 1. Create an API key

1. Sign in to jeCheck and open **API & MCP** (the user menu or *Management → API & MCP*).
2. Click **New key**, name it after the system that will use it (e.g. *Data warehouse*).
3. Choose what the key may do:
   - **Roles** — the key gets exactly the permissions of the selected roles (you can only grant what you have yourself);
   - **Locations** — all locations or a selected subset;
   - **Access** — *Read* or *Read & write*;
   - optional **expiry date**.
4. Copy the token. It starts with `jck_live_` and is shown **only once**.

> **Tip** Developers who just want to experiment can create a **personal token** in *API & MCP → My tokens*. It acts with your own permissions.

## 2. Check the key

```bash
export JECHECK_TOKEN=jck_live_xxxxxxxxxxxxxxxx

curl {{API_BASE}}/me \
  -H "Authorization: Bearer $JECHECK_TOKEN"
```

The response tells you who the key acts as, the company, the token scopes and every permission with the locations where it applies:

```json
{
  "data": {
    "user": { "id": 1042, "name": "API: Data warehouse", "is_service": true },
    "company": { "id": 761, "name": "Coffee Lab", "plan": "enterprise" },
    "token": { "kind": "key", "scopes": ["read", "write"] },
    "permissions": { "audits.view": null, "tasks.view_all": [12, 14] }
  }
}
```

`null` means *all locations*; a list means only those location ids.

## 3. Read data

Latest failed audits:

```bash
curl "{{API_BASE}}/audits?status=failed&period=7d" \
  -H "Authorization: Bearer $JECHECK_TOKEN"
```

Overdue corrective tasks, most urgent first:

```bash
curl "{{API_BASE}}/tasks?overdue=true&sort=due_asc" \
  -H "Authorization: Bearer $JECHECK_TOKEN"
```

## 4. Write data

Create a task (requires a key with **Read & write**):

```bash
curl -X POST {{API_BASE}}/tasks \
  -H "Authorization: Bearer $JECHECK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Replace the broken fridge seal", "priority": "high", "location_id": 12, "due_date": "2026-11-10"}'
```

Assignees get notified exactly as if the task was created in the app.

## 5. Next steps

- [Authentication](/developers/authentication) — key types, scopes, rotation.
- [Pagination & sync](/developers/pagination) — fetch everything and keep it in sync with `updated_since`.
- [API reference](/developers/reference) — every endpoint with examples.
- [MCP server](/developers/mcp) — connect Claude, ChatGPT or Cursor.
- [OpenAPI spec](/developers/openapi.json) — generate a typed client (openapi-generator, orval, NSwag…).


Every request is authenticated with a bearer token in the `Authorization` header:

```http
GET /api/ext/v1/me HTTP/1.1
Host: app.jecheck.io
Authorization: Bearer jck_live_xxxxxxxxxxxxxxxx
```

Requests without a valid token get `401 unauthorized`. Tokens must never be sent in query strings.

## Token types

| Prefix | Type | Acts as | Created in | Best for |
|---|---|---|---|---|
| `jck_live_` | Company key | A service user with the roles and locations chosen for the key | API & MCP → Company keys (permission *Company API keys*) | Server-to-server integrations: ERP, BI, payroll, bots |
| `jck_pat_` | Personal token | You, with your own permissions | API & MCP → My tokens (permission *Personal API tokens & AI apps*) | Scripts, prototypes, MCP in Cursor / Claude Desktop |
| `jck_oat_` | OAuth token | The user who approved the connection | Issued automatically to AI apps | claude.ai, ChatGPT and other MCP clients — see [OAuth](/developers/oauth) |

### Company keys and permissions

A company key is backed by a **service user** that cannot sign in to the web or mobile app. Its permissions are exactly the permissions of the roles you pick, on the locations you pick — the same permission model the jeCheck UI uses. So a key with the role *Auditor* on two locations sees audits of those two locations only, and it is impossible to grant a key more than you have yourself.

Things the key creates or changes are attributed to the service user (shown as *API: &lt;key name&gt;* in history), so you always know what came from an integration.

> **Note** "Full access" keys act as an administrator. Only administrators can create them; use them sparingly.

## Scopes

| Scope | Allows |
|---|---|
| `read` | All `GET` endpoints the key's permissions allow |
| `write` | Creating, changing and deleting (includes `read`) |

A read-only token calling a write endpoint gets `403 insufficient_scope`. MCP clients with a read-only token simply don't see write tools.

## Plan

API and MCP access is part of the **Enterprise** plan. On other plans every call returns `403 plan_required`. Some endpoints additionally need a paid feature of the plan (HACCP, testing, mystery shopping…) — the reference marks them with *plan feature*.

## Lifetime and rotation

- Company keys and personal tokens live until revoked or until the optional expiry date.
- To rotate, create a new key, switch your integration, then revoke the old one. Revocation is immediate.
- OAuth access tokens live 1 hour and are refreshed with a refresh token (30 days, rotated on every use).
- Each key shows *last used* time, IP and request count; the call log keeps 30 days of requests.

## Checking what a token can do

`GET /me` returns the acting user, the token kind and scopes, and every permission with its locations. Call it at start-up and fail fast if something is missing.

## Security checklist

- Store tokens in a secrets manager or environment variables, never in code or front-end bundles.
- Give each integration its own key with the narrowest roles, locations and the `read` scope where possible.
- Set an expiry date for temporary integrations.
- Revoke keys you no longer use — *API & MCP → Company keys* lists them all.


jeCheck uses conventional HTTP status codes and returns every error in the same JSON shape:

```json
{
  "error": {
    "code": "validation_failed",
    "message": "The given data was invalid",
    "fields": {
      "title": ["The title field is required."],
      "priority": ["The selected priority is invalid."]
    }
  }
}
```

- `code` is stable and machine-readable — branch on it, not on the message.
- `message` is a human-readable English explanation.
- `fields` is present only for `422` and lists problems per parameter.

Every response carries an `X-Request-Id` header. Include it when contacting support.

## Error codes

| HTTP | `code` | Meaning | What to do |
|---|---|---|---|
| 400 | `bad_request`, `empty_update` | Malformed request, nothing to update | Fix the request |
| 401 | `unauthorized` | Token missing, invalid, revoked, expired, or its user was deactivated | Check the token; for OAuth refresh it |
| 403 | `forbidden` | The token's user lacks the permission (message names it) | Grant the role/permission or use another key |
| 403 | `insufficient_scope` | Read-only token tried to change data | Use a token with the `write` scope |
| 403 | `plan_required` | The plan has no API access or lacks a paid feature | Upgrade the plan |
| 404 | `not_found` | The record doesn't exist **or isn't visible** to the token | Check the id and the key's locations |
| 404 | `route_not_found` | No such endpoint | Check method and path |
| 409 | `conflict` | The record's state doesn't allow the action (already closed, locked period…) | Re-read the record |
| 422 | `validation_failed` | Invalid parameters, see `fields` | Fix the listed fields |
| 429 | `rate_limited` | Too many requests | Wait `Retry-After` seconds — see [Rate limits](/developers/rate-limits) |
| 500 | `internal_error` | Something broke on our side; it's logged | Retry with backoff; contact support with `X-Request-Id` |

> **Note** For privacy, a record that exists but belongs to another location or company returns `404`, not `403` — the API never confirms the existence of data you can't see.

## Retrying

- Retry `429` and `5xx` with exponential backoff (e.g. 1s, 2s, 4s… up to a minute).
- `GET`, `PUT`, `PATCH` and `DELETE` are safe to retry. Retrying `POST` may create duplicates — check first (for example list with `updated_since`) or use endpoints that accept a client-generated UUID (HACCP entries, routine marks).

## Errors in MCP

Over MCP a failed tool call returns a normal result with `isError: true` and the same error object in `structuredContent`, so the assistant can read it and react. Protocol problems (unknown tool, bad JSON-RPC) use JSON-RPC errors such as `-32602`.


List endpoints are paginated and return a `meta` object next to `data`:

```json
{
  "data": [ { "uuid": "…" }, { "uuid": "…" } ],
  "meta": { "page": 1, "per_page": 25, "total": 312, "has_more": true }
}
```

| Parameter | Default | Max | |
|---|---|---|---|
| `page` | 1 | — | Page number, from 1 |
| `per_page` | 25 | 100 | Items per page |

## Fetch everything

```python
import os, requests

def fetch_all(path, **params):
    page = 1
    while True:
        r = requests.get(
            f"{{API_BASE}}{path}",
            headers={"Authorization": f"Bearer {os.environ['JECHECK_TOKEN']}"},
            params={**params, "page": page, "per_page": 100},
        )
        r.raise_for_status()
        body = r.json()
        yield from body["data"]
        if not body["meta"]["has_more"]:
            break
        page += 1

tasks = list(fetch_all("/tasks", status="in_progress"))
```

## Incremental sync with `updated_since`

Most lists accept `updated_since` (ISO 8601 timestamp). Store the time of your last successful sync and ask only for what changed:

```bash
curl "{{API_BASE}}/tasks?updated_since=2026-11-03T09:00:00%2B05:00&sort=updated_desc&per_page=100" \
  -H "Authorization: Bearer $JECHECK_TOKEN"
```

Recommended loop:

1. Remember `sync_started = now()` before the request.
2. Page through `updated_since=<last_sync>`.
3. Upsert every record by its id / uuid.
4. Save `last_sync = sync_started` (not the time you finished — that avoids gaps).

Deleted records disappear from lists. To detect deletions run a periodic full sync of ids.

## Filters

Filters are query parameters. Arrays can be passed either repeated or comma-separated — these are equivalent:

```
/tasks?status[]=new&status[]=in_progress
/tasks?status=new,in_progress
```

Dates are `YYYY-MM-DD`; moments are ISO 8601 with a time-zone offset. Analytics endpoints take `period` (`7d`, `30d`, `90d`, `12m` or `custom` with `from`/`to`).


Each key may make **600 requests per minute**. Every response tells you where you stand:

| Header | |
|---|---|
| `X-RateLimit-Limit` | Requests allowed per minute for this key |
| `X-RateLimit-Remaining` | Requests left in the current window |
| `Retry-After` | Only on `429`: seconds to wait |

When the limit is exceeded the API answers `429 rate_limited`:

```json
{ "error": { "code": "rate_limited", "message": "Too many requests" } }
```

## Staying within limits

- Use `per_page=100` and `updated_since` instead of re-reading everything.
- Cache reference data (locations, users, checklists) and refresh it a few times a day.
- Run heavy exports at night in your company's time zone.
- Use separate keys for separate integrations — limits are per key.
- On `429` wait `Retry-After` seconds, then continue; don't retry immediately in a loop.

Need a higher limit for a large network? Write to support@jecheck.io with the key prefix and the expected volume.


Endpoints that accept files (task attachments, HACCP photos, routine media, request forms…) take **multipart/form-data**. File fields are marked *file* in the reference; arrays of files use the `[]` suffix.

```bash
curl -X POST {{API_BASE}}/tasks/3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b/files \
  -H "Authorization: Bearer $JECHECK_TOKEN" \
  -F "files[]=@before.jpg" \
  -F "files[]=@after.jpg"
```

Other fields of a multipart request are sent as form fields; arrays and objects can be sent as JSON strings.

- Maximum size: **25 MB** per file.
- Images (JPEG, PNG, WebP, HEIC), PDF and office documents are accepted where the app accepts them.

## Downloading files

Responses never contain the file bytes. They contain a `url` — a **signed link** valid for a limited time (usually one hour) that opens without the token. Request the record again to get a fresh link.

## Files over MCP

MCP tools accept files as objects with the file name and base64 content:

```json
{
  "name": "tasks_files_upload",
  "arguments": {
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "files": [{ "name": "after.jpg", "content_base64": "/9j/4AAQSkZJRgABAQ…" }]
  }
}
```

## Reports (PDF, Excel)

Report endpoints return a JSON object with a signed `url` and `expires_at` rather than the binary itself, so they work the same way over REST and MCP.


jeCheck runs a remote **Model Context Protocol** server. Connect it to an AI assistant and the assistant can read and act on your jeCheck data — audits, tasks, routines, HACCP, schedules, the knowledge base, analytics — with exactly your permissions.

| | |
|---|---|
| Server URL | `{{MCP_URL}}` |
| Transport | Streamable HTTP (stateless, JSON responses) |
| Auth | OAuth 2.1 (automatic sign-in) or `Authorization: Bearer <token>` |
| Protocol versions | 2025-06-18 (default), 2025-03-26, 2025-11-25, 2024-11-05 |

> **Note** MCP is part of the Enterprise plan. Each user needs the permission *Personal API tokens & AI apps* (administrators have it).

## Connect your assistant

### claude.ai and Claude Desktop

1. Open **Settings → Connectors → Add custom connector**.
2. Name it *jeCheck*, paste `{{MCP_URL}}` and click **Add**.
3. Click **Connect**, sign in to jeCheck and approve access. Untick *Allow changes* if the assistant should only read.

### ChatGPT

In ChatGPT open **Settings → Apps & Connectors**, enable developer mode under *Advanced* if your workspace requires it, then **Create** a connector with the URL `{{MCP_URL}}` and OAuth authentication. Sign in to jeCheck when prompted.

### Claude Code

```bash
claude mcp add --transport http jecheck {{MCP_URL}}
```

Run `/mcp` inside Claude Code and choose *Authenticate*. To use a personal token instead:

```bash
claude mcp add --transport http jecheck {{MCP_URL}} \
  --header "Authorization: Bearer $JECHECK_TOKEN"
```

### Cursor

`~/.cursor/mcp.json` (or `.cursor/mcp.json` in a project):

```json
{
  "mcpServers": {
    "jecheck": {
      "url": "{{MCP_URL}}",
      "headers": { "Authorization": "Bearer jck_pat_xxxxxxxx" }
    }
  }
}
```

### VS Code (GitHub Copilot agent mode)

`.vscode/mcp.json`:

```json
{
  "servers": {
    "jecheck": {
      "type": "http",
      "url": "{{MCP_URL}}",
      "headers": { "Authorization": "Bearer ${input:jecheck-token}" }
    }
  },
  "inputs": [
    { "id": "jecheck-token", "type": "promptString", "description": "jeCheck personal token", "password": true }
  ]
}
```

### Windsurf and other clients

Any client that supports remote MCP over HTTP works: use the URL above and either OAuth or the `Authorization` header with a personal token from *API & MCP → My tokens*. Clients that only support local (stdio) servers can use a bridge such as `npx mcp-remote {{MCP_URL}}`.

### Your own agent

OpenAI Responses API:

```python
from openai import OpenAI

client = OpenAI()
resp = client.responses.create(
    model="gpt-5",
    tools=[{
        "type": "mcp",
        "server_label": "jecheck",
        "server_url": "{{MCP_URL}}",
        "headers": {"Authorization": "Bearer jck_live_xxxxxxxx"},
        "require_approval": "never",
    }],
    input="Which locations had the lowest audit scores this week?",
)
print(resp.output_text)
```

Any MCP SDK (TypeScript, Python, C#, Java…) can connect with the Streamable HTTP client transport.

## What the assistant gets

### Tools

Every endpoint of the [API reference](/developers/reference) is a tool — its name is shown on the endpoint card (e.g. `tasks_list`, `haccp_deviations_get`, `audits_get`). The tool list is **personal**: a user sees only tools allowed by their permissions, the company plan and the token scope. Tools are annotated: read-only tools have `readOnlyHint`, deleting tools `destructiveHint`, so clients can ask for confirmation.

Narrow the tool set with query parameters on the server URL — useful for clients with a tool limit:

| URL | Effect |
|---|---|
| `{{MCP_URL}}?toolsets=tasks,audits,analytics` | Only these sections (section keys from the reference URLs) |
| `{{MCP_URL}}?readonly=1` | Only read-only tools |

### Resources

| URI | Content |
|---|---|
| `jecheck://me`, `jecheck://company` | Who you are connected as, company and plan |
| `jecheck://tasks/{uuid}`, `jecheck://audits/{id}`, `jecheck://checklists/{id}` | Records as JSON |
| `jecheck://instructions/{uuid}` | Knowledge-base documents as Markdown |
| `jecheck://help/{section}/{slug}` | jeCheck help-center articles |

### Prompts

Ready-made scenarios the user can pick in the client: *Weekly operations review*, *Location deep dive*, *Overdue tasks digest*, *Audit follow-up*, *Draft a checklist*, *HACCP status today*.

## Example questions

- "Show the five worst locations by audit score this month and the most frequent violations there."
- "Which corrective tasks are overdue at Dostyk Plaza? Group them by owner."
- "Create a task for Aigerim to replace the fridge seal at Abay Ave by Friday."
- "Are any HACCP temperature logs missing today?"
- "Who is on shift right now at the central kitchen?"
- "Summarise the new hygiene instruction and list who hasn't acknowledged it."

## Safety

- The assistant acts **as you** — it can never see or change more than you can in jeCheck.
- Changes made through MCP are recorded in history like any other change by you.
- Approve write access only for assistants you trust; you can connect read-only.
- Disconnect an app at any time in **API & MCP → AI apps**; access stops immediately.

## Protocol details

- `POST {{MCP_URL}}` with JSON-RPC 2.0 messages (single or batch); notifications get `202 Accepted`. `GET` returns `405` (no server-initiated stream). No session id is needed.
- Supported methods: `initialize`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/templates/list`, `resources/read`, `prompts/list`, `prompts/get`, `completion/complete`, `logging/setLevel`.
- Tool results contain both a text block and `structuredContent` with the same JSON as the REST API (`{"data": …, "meta": …}`); failures set `isError: true`.
- Unauthenticated requests get `401` with `WWW-Authenticate: Bearer resource_metadata="…"` pointing to the [OAuth](/developers/oauth) metadata.

```bash
curl -X POST {{MCP_URL}} \
  -H "Authorization: Bearer $JECHECK_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"tasks_list","arguments":{"overdue":true}}}'
```


AI apps such as claude.ai and ChatGPT connect to the jeCheck MCP server with **OAuth 2.1** — the user signs in to jeCheck and approves access, no token copying needed. jeCheck implements the MCP authorization spec: protected-resource metadata (RFC 9728), authorization-server metadata (RFC 8414), dynamic client registration (RFC 7591), authorization code with **PKCE S256** and rotating refresh tokens.

Most developers never touch this — MCP clients do it automatically. This page is for those building their own client.

## Endpoints

| | URL |
|---|---|
| Protected resource metadata | `{{APP_URL}}/.well-known/oauth-protected-resource` |
| Authorization server metadata | `{{APP_URL}}/.well-known/oauth-authorization-server` |
| Registration | `POST {{APP_URL}}/oauth/register` |
| Authorization | `GET {{APP_URL}}/oauth/authorize` |
| Token | `POST {{APP_URL}}/oauth/token` |
| Revocation | `POST {{APP_URL}}/oauth/revoke` |

Scopes: `read`, `write`. The user can deny `write` on the consent screen — check the `scope` field of the token response.

## Flow

### 1. Discover

An unauthenticated call to `{{MCP_URL}}` returns:

```http
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="{{APP_URL}}/.well-known/oauth-protected-resource"
```

The resource metadata names the authorization server; its metadata lists all endpoints.

### 2. Register the client

```bash
curl -X POST {{APP_URL}}/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name": "My agent", "redirect_uris": ["https://agent.example.com/callback"], "token_endpoint_auth_method": "none"}'
```

```json
{ "client_id": "jck_app_9f3k…", "redirect_uris": ["https://agent.example.com/callback"], "token_endpoint_auth_method": "none" }
```

Redirect URIs must be `https`, loopback `http` (`http://127.0.0.1:PORT/…` for desktop apps) or a custom app scheme. Confidential clients may request `client_secret_post` or `client_secret_basic` and receive a secret.

### 3. Send the user to authorize

```
{{APP_URL}}/oauth/authorize?response_type=code
  &client_id=jck_app_9f3k…
  &redirect_uri=https://agent.example.com/callback
  &scope=read%20write
  &state=af0ifjsldkj
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
```

The user signs in to jeCheck (if needed) and sees what the app asks for. After approval jeCheck redirects to `redirect_uri?code=…&state=…&iss=…`; on refusal — `error=access_denied`.

### 4. Exchange the code

```bash
curl -X POST {{APP_URL}}/oauth/token \
  -d grant_type=authorization_code \
  -d code=SplxlOBeZQQYbYS6WxSbIA \
  -d client_id=jck_app_9f3k… \
  -d redirect_uri=https://agent.example.com/callback \
  -d code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
```

```json
{
  "access_token": "jck_oat_…",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "jck_ort_…",
  "scope": "read write"
}
```

Codes are single-use and expire after 10 minutes.

### 5. Refresh

```bash
curl -X POST {{APP_URL}}/oauth/token \
  -d grant_type=refresh_token \
  -d refresh_token=jck_ort_… \
  -d client_id=jck_app_9f3k…
```

Each refresh returns a **new** refresh token and invalidates the old one together with its access token. Refresh tokens expire after 30 days of inactivity.

### 6. Revoke

```bash
curl -X POST {{APP_URL}}/oauth/revoke -d token=jck_oat_…
```

Users can also disconnect apps in *API & MCP → AI apps*.

## Using the token

OAuth access tokens work for both the MCP server and the REST API, with the permissions of the user who approved the connection.

## Errors

Token-endpoint errors follow RFC 6749: `{"error": "invalid_grant", "error_description": "…"}` with `400` (or `401 invalid_client`).


# API reference

## Account

Identify the token: the acting user, the company, the plan and the effective permissions. Call `GET /me` first to check that a key works.

### GET /me — Get the current identity

Returns the user the token acts as (for a company key — its service user), the company, the token kind and scopes, and the list of permissions with the locations where each one applies (`null` — all locations).

MCP tool: `account_me`

Example response `data`:

```json
{
    "user": {
        "id": 1042,
        "uuid": "9b7c3f8e-2d1a-4b6e-9f00-1c2d3e4f5a6b",
        "name": "API: Data warehouse",
        "email": "api-7f3k@api.jecheck.io",
        "is_admin": false,
        "is_service": true,
        "locale": "en"
    },
    "company": {
        "id": 761,
        "name": "Coffee Lab",
        "country": "KZ",
        "locale": "ru",
        "timezone": "Asia/Almaty",
        "plan": "enterprise"
    },
    "token": {
        "kind": "key",
        "name": "Data warehouse",
        "prefix": "jck_live_a1B2c3",
        "scopes": [
            "read",
            "write"
        ],
        "expires_at": null
    },
    "permissions": {
        "analytics.view": null,
        "audits.view": [
            12,
            14
        ],
        "tasks.view_all": null
    },
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        },
        {
            "id": 14,
            "name": "Dostyk Plaza"
        }
    ]
}
```

### GET /company — Get the company profile

Company settings that matter for integrations: name, country, language of reports, time zone, plan and its paid features, number of locations.

MCP tool: `account_company`

Example response `data`:

```json
{
    "id": 761,
    "name": "Coffee Lab",
    "country": "KZ",
    "locale": "ru",
    "timezone": "Asia/Almaty",
    "plan": {
        "key": "enterprise",
        "until": null,
        "features": [
            "automation",
            "ai",
            "testing",
            "haccp",
            "api"
        ]
    },
    "locations_count": 14,
    "created_at": "2024-03-02T09:15:00+05:00"
}
```

## Locations

Locations (venues, stores, branches) and regions: the list the token can see, location cards, staff and managers. Location ids are used as filters across the whole API. Creating and editing locations needs `locations.manage`; staff and managers — `locations.staff`.

### GET /locations — List locations

Locations the token can see, sorted by name. Administrators and holders of `locations.manage` / `locations.staff` see every location; other users see their own locations plus the locations where any of their permissions applies.

MCP tool: `locations_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `region_id` | query | integer | no | Only locations of this region |
| `search` | query | string | no | Search in name and address |

Example response `data`:

```json
[
    {
        "id": 12,
        "name": "Abay Ave",
        "address": "Almaty, Abay Ave 52",
        "email": "abay@coffeelab.kz",
        "timezone": "Asia/Almaty",
        "timezone_inherited": true,
        "region": {
            "id": 3,
            "name": "Almaty"
        },
        "managers": [
            {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            }
        ],
        "created_at": "2024-03-02T09:15:00+05:00",
        "url": "https://app.jecheck.io/ui/locations"
    }
]
```

### GET /locations/{id} — Retrieve a location

Location card: address, time zone, region, managers of the location and of its region, teams serving it. With `locations.manage` or `locations.staff` it also includes the staff count and who is on shift right now.

MCP tool: `locations_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Location id |

Example response `data`:

```json
{
    "id": 12,
    "name": "Abay Ave",
    "address": "Almaty, Abay Ave 52",
    "email": "abay@coffeelab.kz",
    "timezone": "Asia/Almaty",
    "timezone_inherited": true,
    "region": {
        "id": 3,
        "name": "Almaty"
    },
    "managers": [
        {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        }
    ],
    "created_at": "2024-03-02T09:15:00+05:00",
    "url": "https://app.jecheck.io/ui/locations",
    "region_managers": [
        {
            "id": 98,
            "uuid": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
            "name": "Asel Nurlanova"
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Maintenance"
        }
    ],
    "staff_count": 14,
    "on_shift_now": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ]
}
```

### POST /locations — Create a location

Adds a location to the company. On the free plan the number of locations is limited — the call fails with `validation_failed` when the limit is reached.

MCP tool: `locations_create` · Permission: `locations.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Location name |
| `address` | body | string | no | Street address |
| `email` | body | string | no | Contact email of the location |
| `region_id` | body | integer | no | Region id; `null` removes the location from its region |
| `timezone` | body | string | no | IANA time zone, e.g. `Asia/Almaty`; `null` — same as the company |

Example response `data`:

```json
{
    "id": 12,
    "name": "Abay Ave",
    "address": "Almaty, Abay Ave 52",
    "email": "abay@coffeelab.kz",
    "timezone": "Asia/Almaty",
    "timezone_inherited": true,
    "region": {
        "id": 3,
        "name": "Almaty"
    },
    "managers": [
        {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        }
    ],
    "created_at": "2024-03-02T09:15:00+05:00",
    "url": "https://app.jecheck.io/ui/locations",
    "region_managers": [
        {
            "id": 98,
            "uuid": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
            "name": "Asel Nurlanova"
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Maintenance"
        }
    ],
    "staff_count": 14,
    "on_shift_now": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ]
}
```

### PATCH /locations/{id} — Update a location

Partial update: send only the fields to change.

MCP tool: `locations_update` · Permission: `locations.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Location id |
| `name` | body | string | no | Location name |
| `address` | body | string | no | Street address |
| `email` | body | string | no | Contact email of the location |
| `region_id` | body | integer | no | Region id; `null` removes the location from its region |
| `timezone` | body | string | no | IANA time zone, e.g. `Asia/Almaty`; `null` — same as the company |

Example response `data`:

```json
{
    "id": 12,
    "name": "Abay Ave",
    "address": "Almaty, Abay Ave 52",
    "email": "abay@coffeelab.kz",
    "timezone": "Asia/Almaty",
    "timezone_inherited": true,
    "region": {
        "id": 3,
        "name": "Almaty"
    },
    "managers": [
        {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        }
    ],
    "created_at": "2024-03-02T09:15:00+05:00",
    "url": "https://app.jecheck.io/ui/locations",
    "region_managers": [
        {
            "id": 98,
            "uuid": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
            "name": "Asel Nurlanova"
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Maintenance"
        }
    ],
    "staff_count": 14,
    "on_shift_now": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ]
}
```

### DELETE /locations/{id} — Delete a location

Marks the location as deleted. Its history (audits, tasks) stays; it disappears from lists and filters.

MCP tool: `locations_delete` · Permission: `locations.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Location id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /locations/{id}/staff — List location staff

Employees attached to the location: `kind` is `home` (primary location) or `extra` (also works here); managers that are not attached have `kind: null`. `on_shift_now` — a published shift is running or the employee has clocked in.

MCP tool: `locations_staff_list` · Permission: `locations.manage` or `locations.staff`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Location id |

Example response `data`:

```json
[
    {
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "kind": "home",
        "is_manager": false,
        "position": {
            "id": 5,
            "name": "Barista"
        },
        "on_shift_now": true
    }
]
```

### POST /locations/{id}/staff — Attach or detach employees

`kind: home` makes the location the primary one for these employees (their previous primary location becomes additional); `kind: extra` adds it as an additional location; `kind: null` removes them from the location (the manager flag is kept). Returns the updated staff list.

MCP tool: `locations_staff_set` · Permission: `locations.staff`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Location id |
| `user_ids` | body | array<integer> | yes | Employee ids |
| `kind` | body | string (home, extra) | yes | `home`, `extra` or `null` to detach |

Example response `data`:

```json
[
    {
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "kind": "home",
        "is_manager": false,
        "position": {
            "id": 5,
            "name": "Barista"
        },
        "on_shift_now": false
    }
]
```

### PUT /locations/{id}/managers — Set location managers

Replaces the list of managers of the location. Managers get the permissions of the system role "Location manager" for this location. The change is written to the access log.

MCP tool: `locations_managers_set` · Permission: `locations.staff`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Location id |
| `user_ids` | body | array<integer> | yes | Employee ids; empty array removes all managers |

Example response `data`:

```json
{
    "id": 12,
    "name": "Abay Ave",
    "address": "Almaty, Abay Ave 52",
    "email": "abay@coffeelab.kz",
    "timezone": "Asia/Almaty",
    "timezone_inherited": true,
    "region": {
        "id": 3,
        "name": "Almaty"
    },
    "managers": [
        {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        }
    ],
    "created_at": "2024-03-02T09:15:00+05:00",
    "url": "https://app.jecheck.io/ui/locations",
    "region_managers": [
        {
            "id": 98,
            "uuid": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
            "name": "Asel Nurlanova"
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Maintenance"
        }
    ],
    "staff_count": 14,
    "on_shift_now": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ]
}
```

### PUT /locations/people/{user_id}/anywhere — Allow an employee to work at any location

"Can work at all locations": the employee may be scheduled and audit at any location without being attached to it. It does not widen what data the employee sees. Written to the access log.

MCP tool: `locations_anywhere_set` · Permission: `locations.staff`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `user_id` | path | integer | yes | Employee id |
| `anywhere` | body | boolean | yes |  |

Example response `data`:

```json
{
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "anywhere": true
}
```

### GET /locations/regions — List regions

Regions (groups of locations) with their managers. `locations` lists only the locations visible to the token.

MCP tool: `locations_regions_list`

Example response `data`:

```json
[
    {
        "id": 3,
        "name": "Almaty",
        "color": "#2f6fed",
        "managers": [
            {
                "id": 98,
                "uuid": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
                "name": "Asel Nurlanova"
            }
        ],
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave"
            },
            {
                "id": 14,
                "name": "Dostyk Plaza"
            }
        ]
    }
]
```

### POST /locations/regions — Create a region

Creates a region. Setting `manager_ids` additionally requires `locations.staff`.

MCP tool: `locations_regions_create` · Permission: `locations.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Region name |
| `color` | body | string | no | Color, hex like `#2f6fed` |
| `location_ids` | body | array<integer> | no | Locations of the region |
| `manager_ids` | body | array<integer> | no | Region managers (user ids); requires `locations.staff` |

Example response `data`:

```json
{
    "id": 3,
    "name": "Almaty",
    "color": "#2f6fed",
    "managers": [
        {
            "id": 98,
            "uuid": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
            "name": "Asel Nurlanova"
        }
    ],
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        },
        {
            "id": 14,
            "name": "Dostyk Plaza"
        }
    ]
}
```

### PATCH /locations/regions/{id} — Update a region

Partial update. `location_ids` replaces the set of locations in the region; `manager_ids` replaces its managers (requires `locations.staff`).

MCP tool: `locations_regions_update` · Permission: `locations.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Region id |
| `name` | body | string | no | Region name |
| `color` | body | string | no | Color, hex like `#2f6fed` |
| `location_ids` | body | array<integer> | no | Locations of the region |
| `manager_ids` | body | array<integer> | no | Region managers (user ids); requires `locations.staff` |

Example response `data`:

```json
{
    "id": 3,
    "name": "Almaty",
    "color": "#2f6fed",
    "managers": [
        {
            "id": 98,
            "uuid": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
            "name": "Asel Nurlanova"
        }
    ],
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        },
        {
            "id": 14,
            "name": "Dostyk Plaza"
        }
    ]
}
```

### DELETE /locations/regions/{id} — Delete a region

Deletes the region; its locations stay without a region.

MCP tool: `locations_regions_delete` · Permission: `locations.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Region id |

Example response `data`:

```json
{
    "deleted": true
}
```

## Employees

Employees of the company: list with filters, employee cards, adding and editing employees, deactivation, passwords and invitations, the positions directory. Visibility follows `users.view` (employees of the locations where it applies); email and phone are returned only with `users.contacts`.

### GET /users — List employees

Employees visible to the token (`users.view`: employees of the locations where the permission applies; employees without a location are visible only with the permission for all locations). Active employees by default; use `status` for deleted ones. `email` and `phone` are `null` without `users.contacts` for that employee.

MCP tool: `users_list` · Permission: `users.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `status` | query | string (active, deleted, all) | no | Employment status |
| `location_ids` | query | array<integer> | no | Only employees attached to these locations (primary or additional) |
| `position_id` | query | integer | no | Only employees with this position |
| `team_id` | query | integer | no | Only members of this team |
| `is_admin` | query | boolean | no | Only administrators (`true`) or only non-administrators (`false`) |
| `search` | query | string | no | Search in first name, last name and email |
| `updated_since` | query | string | no | Only employees changed after this moment — use for incremental sync |
| `sort` | query | string (name, created_desc, updated_desc) | no |  |

Example response `data`:

```json
[
    {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "first_name": "Aigerim",
        "last_name": "Sadykova",
        "name": "Aigerim Sadykova",
        "email": "aigerim@coffeelab.kz",
        "phone": "+77011234567",
        "position": {
            "id": 5,
            "name": "Barista"
        },
        "status": "active",
        "is_admin": false,
        "is_owner": false,
        "web_access": true,
        "works_anywhere": false,
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home",
                "is_manager": false
            }
        ],
        "teams": [
            {
                "id": 4,
                "name": "Kitchen",
                "is_head": false
            }
        ],
        "locale": "ru",
        "last_activity_at": "2026-11-03T09:40:00+05:00",
        "created_at": "2025-02-11T10:00:00+05:00",
        "updated_at": "2026-10-28T16:12:00+05:00",
        "deleted_at": null,
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    }
]
```

### GET /users/{id} — Retrieve an employee

Employee card: position, locations (primary/additional, manager flag), teams, administrator and web-cabinet flags, last activity, and what the token may do with this employee (`can`).

MCP tool: `users_get` · Permission: `users.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |

Example response `data`:

```json
{
    "id": 311,
    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "first_name": "Aigerim",
    "last_name": "Sadykova",
    "name": "Aigerim Sadykova",
    "email": "aigerim@coffeelab.kz",
    "phone": "+77011234567",
    "position": {
        "id": 5,
        "name": "Barista"
    },
    "status": "active",
    "is_admin": false,
    "is_owner": false,
    "web_access": true,
    "works_anywhere": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave",
            "kind": "home",
            "is_manager": false
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Kitchen",
            "is_head": false
        }
    ],
    "locale": "ru",
    "last_activity_at": "2026-11-03T09:40:00+05:00",
    "created_at": "2025-02-11T10:00:00+05:00",
    "updated_at": "2026-10-28T16:12:00+05:00",
    "deleted_at": null,
    "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "can": {
        "edit": true,
        "see_contacts": true,
        "manage_login": true,
        "deactivate": true
    }
}
```

### POST /users — Create an employee

Adds an employee with access to the mobile app and the web cabinet. An invitation with a generated password is emailed to the employee. Email is the login and must be unique across jeCheck. Attach the employee to locations with `locations_staff_set`; administrator rights and app-only access are set with `access_users_account_update`.

MCP tool: `users_create` · Permission: `users.create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `first_name` | body | string | yes | First name |
| `last_name` | body | string | yes | Last name |
| `email` | body | string | yes | Email — the login |
| `position_id` | body | integer | no | Position id (see `GET /users/positions`) |

Example response `data`:

```json
{
    "id": 311,
    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "first_name": "Aigerim",
    "last_name": "Sadykova",
    "name": "Aigerim Sadykova",
    "email": "aigerim@coffeelab.kz",
    "phone": "+77011234567",
    "position": {
        "id": 5,
        "name": "Barista"
    },
    "status": "active",
    "is_admin": false,
    "is_owner": false,
    "web_access": true,
    "works_anywhere": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave",
            "kind": "home",
            "is_manager": false
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Kitchen",
            "is_head": false
        }
    ],
    "locale": "ru",
    "last_activity_at": "2026-11-03T09:40:00+05:00",
    "created_at": "2025-02-11T10:00:00+05:00",
    "updated_at": "2026-10-28T16:12:00+05:00",
    "deleted_at": null,
    "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "can": {
        "edit": true,
        "see_contacts": true,
        "manage_login": true,
        "deactivate": true
    }
}
```

### PATCH /users/{id} — Update an employee

Partial update of name, position and email. Changing the email (login) additionally requires `users.access` and `users.contacts` for the employee; the employee is notified at the new address. Only an administrator can change the login of an administrator.

MCP tool: `users_update` · Permission: `users.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |
| `first_name` | body | string | no | First name |
| `last_name` | body | string | no | Last name |
| `email` | body | string | no | New email (login) |
| `position_id` | body | integer | no | Position id; `null` clears the position |

Example response `data`:

```json
{
    "id": 311,
    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "first_name": "Aigerim",
    "last_name": "Sadykova",
    "name": "Aigerim Sadykova",
    "email": "aigerim@coffeelab.kz",
    "phone": "+77011234567",
    "position": {
        "id": 5,
        "name": "Barista"
    },
    "status": "active",
    "is_admin": false,
    "is_owner": false,
    "web_access": true,
    "works_anywhere": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave",
            "kind": "home",
            "is_manager": false
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Kitchen",
            "is_head": false
        }
    ],
    "locale": "ru",
    "last_activity_at": "2026-11-03T09:40:00+05:00",
    "created_at": "2025-02-11T10:00:00+05:00",
    "updated_at": "2026-10-28T16:12:00+05:00",
    "deleted_at": null,
    "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "can": {
        "edit": true,
        "see_contacts": true,
        "manage_login": true,
        "deactivate": true
    }
}
```

### POST /users/{id}/deactivate — Deactivate an employee

Deletes the employee the same way as the web app: sign-in to the app and the cabinet is blocked immediately, mobile tokens are revoked, history stays. You cannot deactivate yourself or the company owner; only an administrator can deactivate an administrator.

MCP tool: `users_deactivate` · Permission: `users.delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |

Example response `data`:

```json
{
    "id": 311,
    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "first_name": "Aigerim",
    "last_name": "Sadykova",
    "name": "Aigerim Sadykova",
    "email": "aigerim@coffeelab.kz",
    "phone": "+77011234567",
    "position": {
        "id": 5,
        "name": "Barista"
    },
    "status": "deleted",
    "is_admin": false,
    "is_owner": false,
    "web_access": true,
    "works_anywhere": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave",
            "kind": "home",
            "is_manager": false
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Kitchen",
            "is_head": false
        }
    ],
    "locale": "ru",
    "last_activity_at": "2026-11-03T09:40:00+05:00",
    "created_at": "2025-02-11T10:00:00+05:00",
    "updated_at": "2026-10-28T16:12:00+05:00",
    "deleted_at": "2026-11-03T12:00:00+05:00",
    "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "can": {
        "edit": true,
        "see_contacts": true,
        "manage_login": true,
        "deactivate": true
    }
}
```

### POST /users/{id}/restore — Restore a deactivated employee

MCP tool: `users_restore` · Permission: `users.delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |

Example response `data`:

```json
{
    "id": 311,
    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "first_name": "Aigerim",
    "last_name": "Sadykova",
    "name": "Aigerim Sadykova",
    "email": "aigerim@coffeelab.kz",
    "phone": "+77011234567",
    "position": {
        "id": 5,
        "name": "Barista"
    },
    "status": "active",
    "is_admin": false,
    "is_owner": false,
    "web_access": true,
    "works_anywhere": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave",
            "kind": "home",
            "is_manager": false
        }
    ],
    "teams": [
        {
            "id": 4,
            "name": "Kitchen",
            "is_head": false
        }
    ],
    "locale": "ru",
    "last_activity_at": "2026-11-03T09:40:00+05:00",
    "created_at": "2025-02-11T10:00:00+05:00",
    "updated_at": "2026-10-28T16:12:00+05:00",
    "deleted_at": null,
    "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
    "can": {
        "edit": true,
        "see_contacts": true,
        "manage_login": true,
        "deactivate": true
    }
}
```

### POST /users/{id}/reset-password — Reset password by email

Generates a new password and emails it to the employee. Existing mobile sessions are signed out.

MCP tool: `users_reset_password` · Permission: `users.access`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |

Example response `data`:

```json
{
    "sent_to": "aigerim@coffeelab.kz"
}
```

### POST /users/{id}/temporary-password — Issue a temporary password

For an employee without access to email: returns a one-time password to hand over in person. It is not emailed or stored in clear text; the employee must set their own password at the first sign-in. Written to the access log.

MCP tool: `users_temporary_password` · Permission: `users.access`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |

Example response `data`:

```json
{
    "password": "Kq7m-Xr4t",
    "must_change": true
}
```

### POST /users/{id}/invite — Resend the invitation

Emails the invitation again with a sign-in link and a new password (the old password stops working).

MCP tool: `users_invite` · Permission: `users.access`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |

Example response `data`:

```json
{
    "sent_to": "aigerim@coffeelab.kz"
}
```

### GET /users/positions — List positions

The positions directory with the number of active employees in each. Positions are used to route corrective tasks, assign tests, instructions and HACCP logs.

MCP tool: `users_positions_list` · Permission: `users.view` or `users.positions`

Example response `data`:

```json
[
    {
        "id": 5,
        "name": "Barista",
        "employees_count": 23
    }
]
```

### POST /users/positions — Create a position

MCP tool: `users_positions_create` · Permission: `users.positions`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Position name |

Example response `data`:

```json
{
    "id": 6,
    "name": "Shift lead",
    "employees_count": 0
}
```

### PATCH /users/positions/{id} — Rename a position

MCP tool: `users_positions_update` · Permission: `users.positions`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Position id |
| `name` | body | string | yes | New name |

Example response `data`:

```json
{
    "id": 6,
    "name": "Shift supervisor",
    "employees_count": 3
}
```

### DELETE /users/positions/{id} — Delete a position

Employees with this position are left without a position.

MCP tool: `users_positions_delete` · Permission: `users.positions`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Position id |

Example response `data`:

```json
{
    "deleted": true
}
```

## Checklists

Audit checklists and their folders: list, read the full structure (sections, questions, answer types, weights, photo requirements, display conditions), create a checklist from a JSON structure, edit, duplicate, move, delete and restore, and control who may run a checklist in the mobile app.

### GET /checklists — List checklists

Checklists of the company, recently changed first, with structure size and usage over the last 30 days. Without `checklists.edit` only checklists available to the token user are returned (see "who can run" in the checklist). Deleted checklists (trash, last 30 days) — with `deleted=true` and the `checklists.delete` permission.

MCP tool: `checklists_list` · Permission: `checklists.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `folder_id` | query | integer | no | Only checklists of this folder; `0` — checklists without a folder |
| `search` | query | string | no | Search in name and description |
| `updated_since` | query | string | no | Only checklists changed after this moment — use for incremental sync |
| `deleted` | query | boolean | no | Return deleted checklists (trash) instead of active ones; requires `checklists.delete` |

Example response `data`:

```json
[
    {
        "id": 5845,
        "name": "Daily dining room check",
        "description": "Opening check of the hall, restrooms and staff",
        "folder": {
            "id": 31,
            "name": "Front of house"
        },
        "scoring": "points",
        "sections_count": 4,
        "questions_count": 18,
        "author": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "audits_30d": 12,
        "avg_result_30d": 81.4,
        "last_audit_at": "2026-11-02T18:40:00+05:00",
        "restricted": true,
        "ai_summary": false,
        "require_location": true,
        "require_signature": false,
        "deleted": false,
        "created_at": "2026-03-14T09:10:00+05:00",
        "updated_at": "2026-10-28T16:22:00+05:00",
        "url": "https://app.jecheck.io/ui/checklists/5845/edit"
    }
]
```

### GET /checklists/{id} — Retrieve a checklist with its structure

Settings, who may run it (`access`; empty — everyone) and the full structure: sections in order, questions with answer type, weight, photo requirement, type settings (options, norms, `show_if` display conditions, `fail_all`), responsible for fixing violations and auto-task settings. `scoring`: `points` — result is the share of earned weight, `percent` — share of compliant answers.

MCP tool: `checklists_get` · Permission: `checklists.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |

Example response `data`:

```json
{
    "id": 5845,
    "name": "Daily dining room check",
    "description": "Opening check of the hall, restrooms and staff",
    "folder": {
        "id": 31,
        "name": "Front of house"
    },
    "scoring": "points",
    "sections_count": 4,
    "questions_count": 18,
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "audits_30d": 12,
    "avg_result_30d": 81.4,
    "last_audit_at": "2026-11-02T18:40:00+05:00",
    "restricted": true,
    "ai_summary": false,
    "require_location": true,
    "require_signature": false,
    "deleted": false,
    "created_at": "2026-03-14T09:10:00+05:00",
    "updated_at": "2026-10-28T16:22:00+05:00",
    "url": "https://app.jecheck.io/ui/checklists/5845/edit",
    "available_from": "00:01",
    "available_to": "23:59",
    "select_manager": false,
    "send_to_location": false,
    "scenario_id": null,
    "access": [
        {
            "type": "position",
            "id": 14
        },
        {
            "type": "department",
            "id": 12
        }
    ],
    "sections": [
        {
            "id": 21626,
            "name": "Kitchen",
            "responsible": null,
            "questions": [
                {
                    "id": 197426,
                    "number": 1,
                    "text": "Fridge temperature is within the norm",
                    "description": "Check the display of fridge #2",
                    "type": "number",
                    "weight": 5,
                    "can_skip": false,
                    "photo_required": true,
                    "settings": {
                        "unit": "°C",
                        "min": 2,
                        "max": 6,
                        "decimals": 1,
                        "fail_all": true
                    },
                    "responsible": {
                        "to": "role",
                        "position_id": 14
                    },
                    "auto_task": {
                        "enabled": true,
                        "deadline_hours": 24
                    }
                }
            ]
        }
    ]
}
```

### POST /checklists — Create a checklist

Creates a checklist with sections and questions in one call. Minimal question: `{"text": "Floors are clean"}` — a yes/no question with weight 1. Display conditions (`settings.show_if`) reference question ids, so add them later with `checklists.structure.update`.

MCP tool: `checklists_create` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Checklist name (at least 5 characters) |
| `description` | body | string | no | Description |
| `folder_id` | body | integer | no | Folder id (see `checklists.folders.list`) |
| `scoring` | body | string (points, percent) | no | `points` — weighted score (default), `percent` — share of compliant answers |
| `sections` | body | array<object> | no | Sections with questions, in order |

Example response `data`:

```json
{
    "id": 5845,
    "name": "Daily dining room check",
    "description": "Opening check of the hall, restrooms and staff",
    "folder": {
        "id": 31,
        "name": "Front of house"
    },
    "scoring": "points",
    "sections_count": 4,
    "questions_count": 18,
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "audits_30d": 12,
    "avg_result_30d": 81.4,
    "last_audit_at": "2026-11-02T18:40:00+05:00",
    "restricted": true,
    "ai_summary": false,
    "require_location": true,
    "require_signature": false,
    "deleted": false,
    "created_at": "2026-03-14T09:10:00+05:00",
    "updated_at": "2026-10-28T16:22:00+05:00",
    "url": "https://app.jecheck.io/ui/checklists/5845/edit",
    "available_from": "00:01",
    "available_to": "23:59",
    "select_manager": false,
    "send_to_location": false,
    "scenario_id": null,
    "access": [
        {
            "type": "position",
            "id": 14
        },
        {
            "type": "department",
            "id": 12
        }
    ],
    "sections": [
        {
            "id": 21626,
            "name": "Kitchen",
            "responsible": null,
            "questions": [
                {
                    "id": 197426,
                    "number": 1,
                    "text": "Fridge temperature is within the norm",
                    "description": "Check the display of fridge #2",
                    "type": "number",
                    "weight": 5,
                    "can_skip": false,
                    "photo_required": true,
                    "settings": {
                        "unit": "°C",
                        "min": 2,
                        "max": 6,
                        "decimals": 1,
                        "fail_all": true
                    },
                    "responsible": {
                        "to": "role",
                        "position_id": 14
                    },
                    "auto_task": {
                        "enabled": true,
                        "deadline_hours": 24
                    }
                }
            ]
        }
    ]
}
```

### PUT /checklists/{id}/structure — Replace the checklist structure

Sends the whole structure at once (as in `checklists.get`). Pass `id` for sections and questions you keep — their audit history stays linked; items without `id` are created, items not listed are removed. Existing sections cannot be reordered (new ones are appended at the end); questions can be reordered and moved between sections. For an existing question without `settings` the current settings are kept.

MCP tool: `checklists_structure_update` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |
| `name` | body | string | no | New name (kept if omitted) |
| `description` | body | string | no | New description (kept if omitted) |
| `sections` | body | array<object> | yes | The complete new structure. Existing sections keep their order (new sections can only be appended); sections and questions not listed are removed |

Example response `data`:

```json
{
    "id": 5845,
    "name": "Daily dining room check",
    "description": "Opening check of the hall, restrooms and staff",
    "folder": {
        "id": 31,
        "name": "Front of house"
    },
    "scoring": "points",
    "sections_count": 4,
    "questions_count": 18,
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "audits_30d": 12,
    "avg_result_30d": 81.4,
    "last_audit_at": "2026-11-02T18:40:00+05:00",
    "restricted": true,
    "ai_summary": false,
    "require_location": true,
    "require_signature": false,
    "deleted": false,
    "created_at": "2026-03-14T09:10:00+05:00",
    "updated_at": "2026-10-28T16:22:00+05:00",
    "url": "https://app.jecheck.io/ui/checklists/5845/edit",
    "available_from": "00:01",
    "available_to": "23:59",
    "select_manager": false,
    "send_to_location": false,
    "scenario_id": null,
    "access": [
        {
            "type": "position",
            "id": 14
        },
        {
            "type": "department",
            "id": 12
        }
    ],
    "sections": [
        {
            "id": 21626,
            "name": "Kitchen",
            "responsible": null,
            "questions": [
                {
                    "id": 197426,
                    "number": 1,
                    "text": "Fridge temperature is within the norm",
                    "description": "Check the display of fridge #2",
                    "type": "number",
                    "weight": 5,
                    "can_skip": false,
                    "photo_required": true,
                    "settings": {
                        "unit": "°C",
                        "min": 2,
                        "max": 6,
                        "decimals": 1,
                        "fail_all": true
                    },
                    "responsible": {
                        "to": "role",
                        "position_id": 14
                    },
                    "auto_task": {
                        "enabled": true,
                        "deadline_hours": 24
                    }
                }
            ]
        }
    ]
}
```

### PATCH /checklists/{id} — Update checklist settings

Partial update of the checklist settings: send only the fields to change. The structure is changed with `checklists.structure.update`. `ai_summary` (AI report on each audit) requires the AI feature of the plan.

MCP tool: `checklists_update` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |
| `name` | body | string | no | Name |
| `description` | body | string | no | Description |
| `folder_id` | body | integer | no | Folder id; `null` — no folder |
| `scoring` | body | string (points, percent) | no | Scoring mode |
| `ai_summary` | body | boolean | no | Generate an AI report for each audit |
| `require_location` | body | boolean | no | The app records the auditor geolocation |
| `require_signature` | body | boolean | no | The audit must be signed |
| `select_manager` | body | boolean | no | The auditor picks the location manager at the end of the audit |
| `send_to_location` | body | boolean | no | Send the report to the location managers |
| `available_from` | body | string | no | The checklist can be started from this time of day, `HH:MM` |
| `available_to` | body | string | no | …and until this time of day, `HH:MM` |
| `scenario_id` | body | integer | no | Automation scenario for violations; `null` — the company default |

Example response `data`:

```json
{
    "id": 5845,
    "name": "Daily dining room check",
    "description": "Opening check of the hall, restrooms and staff",
    "folder": {
        "id": 31,
        "name": "Front of house"
    },
    "scoring": "points",
    "sections_count": 4,
    "questions_count": 18,
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "audits_30d": 12,
    "avg_result_30d": 81.4,
    "last_audit_at": "2026-11-02T18:40:00+05:00",
    "restricted": true,
    "ai_summary": false,
    "require_location": true,
    "require_signature": false,
    "deleted": false,
    "created_at": "2026-03-14T09:10:00+05:00",
    "updated_at": "2026-10-28T16:22:00+05:00",
    "url": "https://app.jecheck.io/ui/checklists/5845/edit",
    "available_from": "00:01",
    "available_to": "23:59",
    "select_manager": false,
    "send_to_location": false,
    "scenario_id": null,
    "access": [
        {
            "type": "position",
            "id": 14
        },
        {
            "type": "department",
            "id": 12
        }
    ],
    "sections": [
        {
            "id": 21626,
            "name": "Kitchen",
            "responsible": null,
            "questions": [
                {
                    "id": 197426,
                    "number": 1,
                    "text": "Fridge temperature is within the norm",
                    "description": "Check the display of fridge #2",
                    "type": "number",
                    "weight": 5,
                    "can_skip": false,
                    "photo_required": true,
                    "settings": {
                        "unit": "°C",
                        "min": 2,
                        "max": 6,
                        "decimals": 1,
                        "fail_all": true
                    },
                    "responsible": {
                        "to": "role",
                        "position_id": 14
                    },
                    "auto_task": {
                        "enabled": true,
                        "deadline_hours": 24
                    }
                }
            ]
        }
    ]
}
```

### POST /checklists/{id}/duplicate — Duplicate a checklist

Copies the checklist with all sections, questions, settings and access rules. The copy gets the original name with a fixed "copy" suffix (written in Russian, as in the web app) — rename it with `checklists.update`.

MCP tool: `checklists_duplicate` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |

Example response `data`:

```json
{
    "id": 5845,
    "name": "Daily dining room check",
    "description": "Opening check of the hall, restrooms and staff",
    "folder": {
        "id": 31,
        "name": "Front of house"
    },
    "scoring": "points",
    "sections_count": 4,
    "questions_count": 18,
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "audits_30d": 12,
    "avg_result_30d": 81.4,
    "last_audit_at": "2026-11-02T18:40:00+05:00",
    "restricted": true,
    "ai_summary": false,
    "require_location": true,
    "require_signature": false,
    "deleted": false,
    "created_at": "2026-03-14T09:10:00+05:00",
    "updated_at": "2026-10-28T16:22:00+05:00",
    "url": "https://app.jecheck.io/ui/checklists/5845/edit",
    "available_from": "00:01",
    "available_to": "23:59",
    "select_manager": false,
    "send_to_location": false,
    "scenario_id": null,
    "access": [
        {
            "type": "position",
            "id": 14
        },
        {
            "type": "department",
            "id": 12
        }
    ],
    "sections": [
        {
            "id": 21626,
            "name": "Kitchen",
            "responsible": null,
            "questions": [
                {
                    "id": 197426,
                    "number": 1,
                    "text": "Fridge temperature is within the norm",
                    "description": "Check the display of fridge #2",
                    "type": "number",
                    "weight": 5,
                    "can_skip": false,
                    "photo_required": true,
                    "settings": {
                        "unit": "°C",
                        "min": 2,
                        "max": 6,
                        "decimals": 1,
                        "fail_all": true
                    },
                    "responsible": {
                        "to": "role",
                        "position_id": 14
                    },
                    "auto_task": {
                        "enabled": true,
                        "deadline_hours": 24
                    }
                }
            ]
        }
    ]
}
```

### DELETE /checklists/{id} — Delete a checklist

Moves the checklist to the trash for 30 days (restore with `checklists.restore`). Audits already conducted stay in the journal and analytics.

MCP tool: `checklists_delete` · Permission: `checklists.delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /checklists/{id}/restore — Restore a deleted checklist

Returns a checklist from the trash: it is available again in the list, the planner and the app.

MCP tool: `checklists_restore` · Permission: `checklists.delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |

Example response `data`:

```json
{
    "id": 5845,
    "name": "Daily dining room check",
    "description": "Opening check of the hall, restrooms and staff",
    "folder": {
        "id": 31,
        "name": "Front of house"
    },
    "scoring": "points",
    "sections_count": 4,
    "questions_count": 18,
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "audits_30d": 12,
    "avg_result_30d": 81.4,
    "last_audit_at": "2026-11-02T18:40:00+05:00",
    "restricted": true,
    "ai_summary": false,
    "require_location": true,
    "require_signature": false,
    "deleted": false,
    "created_at": "2026-03-14T09:10:00+05:00",
    "updated_at": "2026-10-28T16:22:00+05:00",
    "url": "https://app.jecheck.io/ui/checklists/5845/edit",
    "available_from": "00:01",
    "available_to": "23:59",
    "select_manager": false,
    "send_to_location": false,
    "scenario_id": null,
    "access": [
        {
            "type": "position",
            "id": 14
        },
        {
            "type": "department",
            "id": 12
        }
    ],
    "sections": [
        {
            "id": 21626,
            "name": "Kitchen",
            "responsible": null,
            "questions": [
                {
                    "id": 197426,
                    "number": 1,
                    "text": "Fridge temperature is within the norm",
                    "description": "Check the display of fridge #2",
                    "type": "number",
                    "weight": 5,
                    "can_skip": false,
                    "photo_required": true,
                    "settings": {
                        "unit": "°C",
                        "min": 2,
                        "max": 6,
                        "decimals": 1,
                        "fail_all": true
                    },
                    "responsible": {
                        "to": "role",
                        "position_id": 14
                    },
                    "auto_task": {
                        "enabled": true,
                        "deadline_hours": 24
                    }
                }
            ]
        }
    ]
}
```

### POST /checklists/move — Move checklists to a folder

MCP tool: `checklists_move` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `ids` | body | array<integer> | yes | Checklist ids |
| `folder_id` | body | integer | yes | Target folder id; `null` — no folder |

Example response `data`:

```json
{
    "moved": 3
}
```

### GET /checklists/{id}/access — Get who can run a checklist

Rules of "who can run" the checklist in the mobile app: employees (`user`), positions (`position`), locations (`department`) and teams (`team`). An empty list means everyone; administrators can always run any checklist.

MCP tool: `checklists_access_get` · Permission: `checklists.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |

Example response `data`:

```json
{
    "rules": [
        {
            "type": "position",
            "id": 14
        },
        {
            "type": "user",
            "id": 311
        }
    ]
}
```

### PUT /checklists/{id}/access — Set who can run a checklist

Replaces the access rules. Subjects of another company are ignored. An empty `rules` list makes the checklist available to everyone. The planner only lets you assign audits of a checklist to people who can run it.

MCP tool: `checklists_access_set` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |
| `rules` | body | array<object> | yes | Rules; empty — available to everyone |

Example response `data`:

```json
{
    "rules": [
        {
            "type": "position",
            "id": 14
        }
    ]
}
```

### PUT /checklists/{id}/responsible — Set who fixes violations of a section or question

"Who fixes" a violation: `managers` — location managers, `role` — employees of a position (`position_id`), `team` — a team queue (`team_id`), `user` — a specific employee (`user_id`). `null` — default (a question inherits its section, a section — location managers). Automation scenarios and auto-tasks assign violations by this setting.

MCP tool: `checklists_responsible_set` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |
| `target` | body | string (section, question) | yes | What to configure |
| `target_id` | body | integer | yes | Section or question id of this checklist |
| `responsible` | body | object | yes | Target, or `null` to reset to the default |

Example response `data`:

```json
{
    "target": "question",
    "target_id": 197426,
    "responsible": {
        "to": "role",
        "position_id": 14
    }
}
```

### POST /checklists/{id}/media — Upload a reference photo

Uploads a reference image ("how it should look") and returns its public URL. Put the URL into `settings.media` of a question with `checklists.structure.update` (up to 5 per question).

MCP tool: `checklists_media_upload` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Checklist id |
| `image` | body | file | yes | Image file, up to 10 MB |

Example response `data`:

```json
{
    "url": "https://jecheck.s3.eu-west-1.amazonaws.com/checklists/761/reference/9f1c2b.jpg"
}
```

### GET /checklists/folders — List checklist folders

Folders are flat and shared with the mobile app.

MCP tool: `checklists_folders_list` · Permission: `checklists.view`

Example response `data`:

```json
[
    {
        "id": 31,
        "name": "Front of house",
        "color": "blue",
        "checklists_count": 7
    }
]
```

### POST /checklists/folders — Create a checklist folder

MCP tool: `checklists_folders_create` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Folder name, unique in the company |
| `color` | body | string (blue, green, amber, red, violet, teal, gray) | no | Color label |
| `checklist_ids` | body | array<integer> | no | Checklists to move into the new folder |

Example response `data`:

```json
{
    "id": 31,
    "name": "Front of house",
    "color": "blue",
    "checklists_count": 7
}
```

### PATCH /checklists/folders/{id} — Rename or recolor a checklist folder

MCP tool: `checklists_folders_update` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Folder id |
| `name` | body | string | no | New name |
| `color` | body | string (blue, green, amber, red, violet, teal, gray) | no | Color label |

Example response `data`:

```json
{
    "id": 31,
    "name": "Front of house",
    "color": "blue",
    "checklists_count": 7
}
```

### DELETE /checklists/folders/{id} — Delete a checklist folder

Deletes the folder; its checklists are not deleted and move to "no folder".

MCP tool: `checklists_folders_delete` · Permission: `checklists.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Folder id |

Example response `data`:

```json
{
    "deleted": true
}
```

## Audits

Conducted audits (inspections by checklist): the audit journal with filters, the result card with sections, failed questions, comments and photos, PDF reports, deletion and submitting a new audit. Visibility follows the web journal — audits of the locations where the token has `audits.view`.

### GET /audits — List audits

The audit journal, newest first: checklist, location, auditor, result and status. `status`: `completed` — result 70% or higher, `failed` — below 70%, `in_progress` — not finished. Mystery-shopper audits have `mystery: true` and no auditor.

MCP tool: `audits_list` · Permission: `audits.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `date_from` | query | string | no | Conducted on or after this date |
| `date_to` | query | string | no | Conducted on or before this date |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |
| `status` | query | array<string> | no | Filter by status: completed, failed, in_progress |
| `result_from` | query | number | no | Result from, % |
| `result_to` | query | number | no | Result to, % |
| `search` | query | string | no | Search by location, checklist or auditor name |
| `created_since` | query | string | no | Only audits received after this moment — use for incremental sync |
| `sort` | query | string (date_desc, date_asc, result_asc, result_desc) | no |  |

Example response `data`:

```json
[
    {
        "id": 99812,
        "uuid": "9a72f859-63a3-4046-9616-8aaf730073ff",
        "checklist": {
            "id": 5845,
            "name": "Daily dining room check"
        },
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "auditor": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "mystery": false,
        "status": "completed",
        "result": 82.4,
        "report_status": "ready",
        "conducted_at": "2026-11-03T09:40:00+05:00",
        "created_at": "2026-11-03T09:41:12+05:00",
        "report_url": "https://app.jecheck.io/api/reports/newVersionSavePdf/9a72f859-63a3-4046-9616-8aaf730073ff"
    }
]
```

### GET /audits/{id} — Retrieve an audit result

The result card, calculated exactly like the PDF report: score, target and `level` (`bad` — not passed, `warn` — passed below target, `ok` — target reached / excellent), sections with their results, every answered question (`result`: `ok`, `violation`, `na`, `info` for unscored answers) with comments and photos, violations sorted by weight and repetition, previous audit of the same checklist at the location, the AI report if enabled, and corrective tasks created from this audit. Own audits are always visible; others need `audits.view` on the location.

MCP tool: `audits_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Audit id |

Example response `data`:

```json
{
    "id": 99812,
    "uuid": "9a72f859-63a3-4046-9616-8aaf730073ff",
    "checklist": {
        "id": 5845,
        "name": "Daily dining room check"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "auditor": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "mystery": false,
    "status": "completed",
    "result": 82.4,
    "report_status": "ready",
    "conducted_at": "2026-11-03T09:40:00+05:00",
    "created_at": "2026-11-03T09:41:12+05:00",
    "report_url": "https://app.jecheck.io/api/reports/newVersionSavePdf/9a72f859-63a3-4046-9616-8aaf730073ff",
    "scoring": "points",
    "target": 85,
    "level": "warn",
    "failed_by": [],
    "counts": {
        "checked": 18,
        "ok": 15,
        "violations": 3,
        "na": 1,
        "critical": 1,
        "repeats": 1,
        "photos": 6
    },
    "previous": {
        "result": 76.5,
        "date": "2026-10-27",
        "delta": 5.9
    },
    "sections": [
        {
            "id": 21626,
            "name": "Kitchen",
            "result": 71.4,
            "checked": 7,
            "ok": 5,
            "violations": 2,
            "na": 0,
            "questions": [
                {
                    "question_id": 197426,
                    "number": "1.2",
                    "text": "Fridge temperature is within the norm",
                    "result": "violation",
                    "answer": "9.1 °C",
                    "weight": 5,
                    "tier": "critical",
                    "repeat": 2,
                    "fail_all": false,
                    "comment": "Door seal is torn",
                    "photos": [
                        {
                            "url": "https://jecheck.s3.eu-west-1.amazonaws.com/audits/761/fridge.jpg",
                            "name": "fridge.jpg"
                        }
                    ],
                    "files": []
                }
            ]
        }
    ],
    "violations": [
        {
            "question_id": 197426,
            "number": "1.2",
            "text": "Fridge temperature is within the norm",
            "result": "violation",
            "answer": "9.1 °C",
            "weight": 5,
            "tier": "critical",
            "repeat": 2,
            "fail_all": false,
            "comment": "Door seal is torn",
            "photos": [
                {
                    "url": "https://jecheck.s3.eu-west-1.amazonaws.com/audits/761/fridge.jpg",
                    "name": "fridge.jpg"
                }
            ],
            "files": [],
            "section": "Kitchen"
        }
    ],
    "ai": {
        "summary": "Cold chain is the main risk: two fridges out of range.",
        "verdict": "Needs attention",
        "recommendations": [
            "Replace the door seal of fridge #2"
        ],
        "plan": [
            {
                "priority": "high",
                "action": "Replace the seal",
                "owner": "Chef",
                "section": "Kitchen"
            }
        ]
    },
    "signature_url": null,
    "geo": {
        "lat": 43.23811,
        "lon": 76.94572
    },
    "tasks": [
        {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "title": "Replace the broken fridge seal",
            "status": "in_progress",
            "due_date": "2026-11-04",
            "question_id": 197426
        }
    ]
}
```

### GET /audits/{id}/pdf — Get the PDF report link

Returns a public link to the PDF report (no login needed — share it carefully) and the online report link. `ready: false` — the PDF is still being generated, try again in a minute. Requires `audits.export` on the audit location; each call is recorded in the access log.

MCP tool: `audits_pdf` · Permission: `audits.export`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Audit id |

Example response `data`:

```json
{
    "ready": true,
    "pdf_url": "https://app.jecheck.io/api/reports/pdf/9a72f859-63a3-4046-9616-8aaf730073ff",
    "online_url": "https://app.jecheck.io/api/reports/newVersionSavePdf/9a72f859-63a3-4046-9616-8aaf730073ff"
}
```

### POST /audits — Submit an audit

Submits a conducted audit for a checklist at a location — the same processing as the mobile app: the server scores every answer by the question type and settings, calculates the result, starts the PDF report and runs automation (corrective tasks for violations). The token user becomes the auditor; the checklist must be available to them and the location must be one where they work or may view audits. `yes_no` questions take `result` (`ok`, `violation`, `na`); `text` — `text`; other types — `value` (e.g. `{"number": 4.5}`, `{"option": "a1"}`, `{"options": ["a1","b2"]}`, `{"date": "2026-11-03"}`, `{"code": "4870001234567"}`, `{"user_id": 311}`). Photos cannot be attached through the API yet.

MCP tool: `audits_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `checklist_id` | body | integer | yes | Checklist id |
| `location_id` | body | integer | yes | Location id |
| `conducted_at` | body | string | no | When the audit was conducted (ISO 8601); default — now |
| `answers` | body | array<object> | yes | Answers; questions without an answer are not saved |
| `latitude` | body | number | no | Auditor geolocation |
| `longitude` | body | number | no | Auditor geolocation |

Example response `data`:

```json
{
    "id": 99812,
    "uuid": "9a72f859-63a3-4046-9616-8aaf730073ff",
    "checklist": {
        "id": 5845,
        "name": "Daily dining room check"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "auditor": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "mystery": false,
    "status": "completed",
    "result": 82.4,
    "report_status": "ready",
    "conducted_at": "2026-11-03T09:40:00+05:00",
    "created_at": "2026-11-03T09:41:12+05:00",
    "report_url": "https://app.jecheck.io/api/reports/newVersionSavePdf/9a72f859-63a3-4046-9616-8aaf730073ff",
    "scoring": "points",
    "target": 85,
    "level": "warn",
    "failed_by": [],
    "counts": {
        "checked": 18,
        "ok": 15,
        "violations": 3,
        "na": 1,
        "critical": 1,
        "repeats": 1,
        "photos": 6
    },
    "previous": {
        "result": 76.5,
        "date": "2026-10-27",
        "delta": 5.9
    },
    "sections": [
        {
            "id": 21626,
            "name": "Kitchen",
            "result": 71.4,
            "checked": 7,
            "ok": 5,
            "violations": 2,
            "na": 0,
            "questions": [
                {
                    "question_id": 197426,
                    "number": "1.2",
                    "text": "Fridge temperature is within the norm",
                    "result": "violation",
                    "answer": "9.1 °C",
                    "weight": 5,
                    "tier": "critical",
                    "repeat": 2,
                    "fail_all": false,
                    "comment": "Door seal is torn",
                    "photos": [
                        {
                            "url": "https://jecheck.s3.eu-west-1.amazonaws.com/audits/761/fridge.jpg",
                            "name": "fridge.jpg"
                        }
                    ],
                    "files": []
                }
            ]
        }
    ],
    "violations": [
        {
            "question_id": 197426,
            "number": "1.2",
            "text": "Fridge temperature is within the norm",
            "result": "violation",
            "answer": "9.1 °C",
            "weight": 5,
            "tier": "critical",
            "repeat": 2,
            "fail_all": false,
            "comment": "Door seal is torn",
            "photos": [
                {
                    "url": "https://jecheck.s3.eu-west-1.amazonaws.com/audits/761/fridge.jpg",
                    "name": "fridge.jpg"
                }
            ],
            "files": [],
            "section": "Kitchen"
        }
    ],
    "ai": {
        "summary": "Cold chain is the main risk: two fridges out of range.",
        "verdict": "Needs attention",
        "recommendations": [
            "Replace the door seal of fridge #2"
        ],
        "plan": [
            {
                "priority": "high",
                "action": "Replace the seal",
                "owner": "Chef",
                "section": "Kitchen"
            }
        ]
    },
    "signature_url": null,
    "geo": {
        "lat": 43.23811,
        "lon": 76.94572
    },
    "tasks": [
        {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "title": "Replace the broken fridge seal",
            "status": "in_progress",
            "due_date": "2026-11-04",
            "question_id": 197426
        }
    ]
}
```

### DELETE /audits/{id} — Delete an audit

Permanently deletes the audit with its answers and attachments: it disappears from the journal, analytics and reports and cannot be restored. Corrective tasks created from it stay. Requires `audits.delete` on the audit location.

MCP tool: `audits_delete` · Permission: `audits.delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Audit id |

Example response `data`:

```json
{
    "deleted": true
}
```

## Analytics

Audit analytics with the same numbers as the web analytics pages: company overview, location ranking and drill-down, checklist and question statistics, top violations, auditor strictness, plan fulfilment, corrective tasks, the AI control brief and employee tests. Figures cover only the locations where the token has `analytics.view`. Long lists end with an item `{"_truncated": N}` saying how many rows were cut.

### GET /analytics/overview — Get the audit overview

Company summary for a period with the previous period for comparison: number of audits, average score, pass rate (≥ 70%), violations, location coverage, plan fulfilment, open and overdue tasks; trend by day/week/month; score distribution; 5 best and 5 worst locations; top violations; locations without audits; ready-made insights. Start here for "how are we doing" questions.

MCP tool: `analytics_overview` · Permission: `analytics.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` requires `from` and `to` |
| `from` | query | string | no | Start of a custom period |
| `to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03",
        "previous": {
            "from": "2026-09-04",
            "to": "2026-10-04"
        },
        "bucket": "day",
        "restricted_to_user_locations": false
    },
    "kpis": {
        "audits": {
            "value": 18,
            "prev": 48
        },
        "avg": {
            "value": 80,
            "prev": 75.9
        },
        "pass": {
            "value": 83.3,
            "prev": 64.6
        },
        "violations": {
            "value": 28,
            "prev": 90,
            "per_audit": 1.6,
            "prev_per_audit": 1.9,
            "rate": 19.4
        },
        "coverage": {
            "value": 5,
            "total": 5,
            "prev": 5
        },
        "auditors": {
            "value": 4,
            "prev": 6
        },
        "plan": {
            "value": 33,
            "prev": 75,
            "done": 8,
            "planned": 24
        },
        "tasks": {
            "open": 37,
            "overdue": 35
        }
    },
    "trend": [
        {
            "bucket": "2026-10-05",
            "audits": 1,
            "avg": 82.4,
            "pass": 100,
            "violations": 1
        }
    ],
    "distribution": [
        {
            "label": "under 50%",
            "value": 0,
            "tone": "danger"
        }
    ],
    "best_locations": [
        {
            "id": 4638,
            "name": "Park St",
            "audits": 3,
            "avg": 86.3,
            "delta": 30.9
        }
    ],
    "worst_locations": [
        {
            "id": 4638,
            "name": "Station Sq",
            "audits": 3,
            "avg": 72.2,
            "delta": 3.4
        }
    ],
    "top_violations": [
        {
            "id": 197426,
            "question": "Cleaning log is filled in",
            "checklist": "Daily dining room check",
            "section": "Restroom",
            "fails": 5,
            "answered": 10,
            "rate": 50,
            "locations": 4
        }
    ],
    "locations_without_audits": [],
    "insights": [
        "Average score rose by 4.1 pp: 80% vs 75.9% in the previous period."
    ]
}
```

### GET /analytics/locations — Rank locations

Location ranking for a period: audits, average score and change vs the previous period, pass rate, violations per audit, last audit and days since. For "which locations are worst", "where have we not audited for a long time".

MCP tool: `analytics_locations` · Permission: `analytics.view` · Plan feature: `analytics_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` requires `from` and `to` |
| `from` | query | string | no | Start of a custom period |
| `to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |
| `sort` | query | string (worst, best, most_audits, longest_unaudited, biggest_drop) | no | Sort order |
| `limit` | query | integer | no | How many locations to return |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03",
        "previous": {
            "from": "2026-09-04",
            "to": "2026-10-04"
        },
        "bucket": "day",
        "restricted_to_user_locations": false
    },
    "total_locations": 5,
    "locations": [
        {
            "id": 4638,
            "name": "Station Sq",
            "audits": 3,
            "prev_audits": 12,
            "avg": 72.2,
            "prev_avg": 68.8,
            "delta": 3.4,
            "pass_rate": 67,
            "violations": 6,
            "viol_per_audit": 2,
            "last_audit": "2026-10-15",
            "days_since": 19
        }
    ]
}
```

### GET /analytics/locations/{location_id} — Get location analytics

One location in detail for a period: KPIs with the previous period, checklists, top violations of the location, the last 12 audits with report links, auditors.

MCP tool: `analytics_location` · Permission: `analytics.view` · Plan feature: `analytics_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | path | integer | yes | Location id |
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` requires `from` and `to` |
| `from` | query | string | no | Start of a custom period |
| `to` | query | string | no | End of a custom period |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03",
        "previous": {
            "from": "2026-09-04",
            "to": "2026-10-04"
        },
        "bucket": "day",
        "restricted_to_user_locations": false
    },
    "id": 4638,
    "name": "Embankment",
    "address": "Embankment, 3",
    "cur": {
        "audits": 7,
        "avg": 78.2,
        "pass_rate": 100,
        "locations": 1,
        "auditors": 3,
        "violations": 13,
        "viol_per_audit": 1.9,
        "viol_rate": 21.7
    },
    "prev": {
        "audits": 7,
        "avg": 90.4,
        "pass_rate": 100,
        "locations": 1,
        "auditors": 3,
        "violations": 5,
        "viol_per_audit": 0.7,
        "viol_rate": 10
    },
    "checklists": [
        {
            "id": 5845,
            "name": "Daily dining room check",
            "audits": 4,
            "avg": 77.8,
            "min": 72.2,
            "last": "2026-10-14"
        }
    ],
    "violations": [
        {
            "id": 197428,
            "question": "Staff wear uniforms and badges",
            "checklist": "Daily dining room check",
            "section": "Staff",
            "fails": 3,
            "answered": 4,
            "rate": 75,
            "locations": 1
        }
    ],
    "recent": [
        {
            "result": 77.8,
            "date": "2026-10-14 17:19",
            "checklist": "Daily dining room check",
            "auditor": "Marina Lebedeva",
            "report_url": "https://app.jecheck.io/api/reports/newVersionSavePdf/9a72f859-63a3-4046-9616-8aaf730073ff"
        }
    ],
    "auditors": [
        {
            "id": 6321,
            "name": "Marina Lebedeva",
            "audits": 4,
            "avg": 80.4
        }
    ]
}
```

### GET /analytics/checklists — Get checklist statistics

Checklists for a period: audits, average score and change, pass rate, violations. With `checklist_id` — also `selected`: compliance by section, questions sorted by violation rate (most problematic first), locations, and violations repeated at the same location. Useful to find weak spots of a checklist.

MCP tool: `analytics_checklists` · Permission: `analytics.view` · Plan feature: `analytics_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` requires `from` and `to` |
| `from` | query | string | no | Start of a custom period |
| `to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |
| `checklist_id` | query | integer | no | Checklist id for a detailed breakdown |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03",
        "previous": {
            "from": "2026-09-04",
            "to": "2026-10-04"
        },
        "bucket": "day",
        "restricted_to_user_locations": false
    },
    "checklists": [
        {
            "id": 5845,
            "name": "Daily dining room check",
            "type": "points",
            "audits": 10,
            "prev_audits": 16,
            "avg": 79.1,
            "prev_avg": 77.7,
            "delta": 1.4,
            "pass_rate": 90,
            "locations": 5,
            "violations": 17,
            "viol_rate": 19.5,
            "last": "2026-10-15"
        }
    ],
    "selected": {
        "id": 5845,
        "name": "Daily dining room check",
        "cur": {
            "audits": 10,
            "avg": 79.1,
            "pass_rate": 90,
            "locations": 5,
            "auditors": 2,
            "violations": 17,
            "viol_per_audit": 1.7,
            "viol_rate": 19.5
        },
        "prev": {
            "audits": 16,
            "avg": 77.7,
            "pass_rate": 62.5,
            "locations": 5,
            "auditors": 2,
            "violations": 29,
            "viol_per_audit": 1.8,
            "viol_rate": 20.9
        },
        "sections": [
            {
                "id": 21626,
                "name": "Hall",
                "questions": 4,
                "violations": 6,
                "compliance": 83.8
            }
        ],
        "questions_by_violations": [
            {
                "n": 6,
                "question": "Cleaning log is filled in",
                "section": "Restroom",
                "yes": 5,
                "no": 5,
                "skip": 0,
                "rate": 50,
                "locations": 4,
                "comments": 5
            }
        ],
        "locations": [
            {
                "id": 4638,
                "name": "Park St",
                "audits": 1,
                "avg": 58.8,
                "min": 58.8
            }
        ],
        "repeat_violations": [
            {
                "location": "Embankment",
                "question": "Staff wear uniforms and badges",
                "fails": 3,
                "audits": 4,
                "last": "2026-10-14"
            }
        ]
    }
}
```

### GET /analytics/violations — List top violations

The most frequent violations (checklist questions answered "violation") for a period: how many times, out of how many answers, rate %, on how many locations, checklist and section.

MCP tool: `analytics_violations` · Permission: `analytics.view` · Plan feature: `analytics_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` requires `from` and `to` |
| `from` | query | string | no | Start of a custom period |
| `to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |
| `limit` | query | integer | no | How many violations to return |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03",
        "previous": {
            "from": "2026-09-04",
            "to": "2026-10-04"
        },
        "bucket": "day",
        "restricted_to_user_locations": false
    },
    "violations": [
        {
            "id": 197426,
            "question": "Cleaning log is filled in",
            "checklist": "Daily dining room check",
            "section": "Restroom",
            "fails": 5,
            "answered": 10,
            "rate": 50,
            "locations": 4
        }
    ]
}
```

### GET /analytics/team — Get auditor statistics

Auditors for a period: number of audits, average score and deviation from the company average (strictness), locations, checklists, active days, photos / violations / comments per audit, last audit. Requires `analytics.team`; only audits of the token's locations are counted.

MCP tool: `analytics_team` · Permission: `analytics.team` · Plan feature: `analytics_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` requires `from` and `to` |
| `from` | query | string | no | Start of a custom period |
| `to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03",
        "previous": {
            "from": "2026-09-04",
            "to": "2026-10-04"
        },
        "bucket": "day",
        "restricted_to_user_locations": false
    },
    "company_avg": 80,
    "auditors": [
        {
            "id": 6321,
            "name": "Marina Lebedeva",
            "audits": 10,
            "prev_audits": 19,
            "avg": 82.7,
            "deviation": 3.6,
            "locations": 5,
            "checklists": 3,
            "active_days": 7,
            "photos_per_audit": 0.6,
            "viol_per_audit": 1.4,
            "comments_per_audit": 1.4,
            "last": "2026-10-14"
        }
    ]
}
```

### GET /analytics/plan — Get plan fulfilment

Audit plan fulfilment for a period by the planner rules (an audit counts within "planned date ± tolerance" for the same checklist and location): due / done / overdue (window still open) / missed and % with the previous period; breakdowns by location, auditor and checklist (worst first); missed audits; audits planned for the next 7 days; share of unplanned audits.

MCP tool: `analytics_plan` · Permission: `analytics.view` · Plan feature: `analytics_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` requires `from` and `to` |
| `from` | query | string | no | Start of a custom period |
| `to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03",
        "previous": {
            "from": "2026-09-04",
            "to": "2026-10-04"
        },
        "bucket": "day",
        "restricted_to_user_locations": false
    },
    "cur": {
        "due": 24,
        "done": 8,
        "overdue": 0,
        "missed": 16,
        "rate": 33
    },
    "prev": {
        "due": 24,
        "done": 18,
        "overdue": 0,
        "missed": 6,
        "rate": 75
    },
    "byLocation": [
        {
            "id": 4638,
            "name": "Park St",
            "due": 8,
            "done": 1,
            "overdue": 0,
            "missed": 7,
            "rate": 13
        }
    ],
    "byAuditor": [
        {
            "id": 6324,
            "name": "Artem Volkov",
            "due": 4,
            "done": 0,
            "overdue": 0,
            "missed": 4,
            "rate": 0
        }
    ],
    "byChecklist": [
        {
            "id": 5848,
            "name": "Shift opening",
            "due": 4,
            "done": 0,
            "overdue": 0,
            "missed": 4,
            "rate": 0
        }
    ],
    "missed": [
        {
            "date": "2026-10-30",
            "location": "Park St",
            "checklist": "Shift opening",
            "auditor": "Artem Volkov"
        }
    ],
    "upcoming": 9,
    "unplanned": {
        "total": 18,
        "unplanned": 14
    },
    "hasPlan": true
}
```

### GET /analytics/tasks — Get task statistics

Tasks (including corrective actions from audits) for a period: created / closed / overdue, completion %, average closing time, on time; a snapshot of open tasks now (new, in progress, in review, overdue); breakdowns by priority, assignee and location; overdue tasks.

MCP tool: `analytics_tasks` · Permission: `analytics.view` · Plan feature: `analytics_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` requires `from` and `to` |
| `from` | query | string | no | Start of a custom period |
| `to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only audits by these users |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03",
        "previous": {
            "from": "2026-09-04",
            "to": "2026-10-04"
        },
        "bucket": "day",
        "restricted_to_user_locations": false
    },
    "cur": {
        "created": 63,
        "created_done": 25,
        "created_rejected": 1,
        "created_open": 37,
        "created_overdue": 35,
        "completion": 40,
        "done": 25,
        "close_days": 0.4,
        "on_time": 56
    },
    "prev": {
        "created": 0,
        "created_done": 0,
        "created_rejected": 0,
        "created_open": 0,
        "created_overdue": 0,
        "completion": null,
        "done": 0,
        "close_days": null,
        "on_time": null
    },
    "snapshot": {
        "open": 37,
        "new": 30,
        "progress": 4,
        "review": 3,
        "overdue": 35,
        "soon": 0,
        "no_deadline": 0
    },
    "trackedSince": "2026-10-08 17:07:00",
    "byPriority": [
        {
            "key": 0,
            "label": "Urgent",
            "open": 1,
            "overdue": 1
        }
    ],
    "byExecutor": [
        {
            "id": 6323,
            "name": "Elena Morozova",
            "open": 14,
            "overdue": 12,
            "done": 5,
            "assigned": 19,
            "completion": 26,
            "close_days": 0.1,
            "on_time": 60
        }
    ],
    "byLocation": [
        {
            "id": 4637,
            "name": "Central",
            "open": 20,
            "overdue": 18,
            "done": 13
        }
    ],
    "overdueList": [
        {
            "uuid": "25180680-8ef5-a075-575b-cceed90f6bc0",
            "title": "Replace the broken fridge seal",
            "deadline": "2026-10-12",
            "days": 22,
            "priority": 1,
            "location": "Central",
            "executor": "Daria Kim"
        }
    ]
}
```

### GET /analytics/control-brief — Get the AI control brief

The AI control brief from the home page: open problems found automatically (unstable and chronic violations, score drops, reused photos, soft auditors, missed plan, overdue tasks, HACCP, tests, instructions) with title, text, evidence facts, severity and status — most severe first, up to 15. With `signal` — one card by its UUID. Visibility follows `audits.view`; staff-related cards need `control.manage`.

MCP tool: `analytics_control_brief` · Permission: `audits.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `signal` | query | string | no | UUID of one brief card |

Example response `data`:

```json
{
    "signals": [
        {
            "uuid": "4a4007c6-b9a3-461b-8f1b-e584fbe7c45d",
            "detector": "haccp",
            "severity": 3,
            "status": "open",
            "title": "Central: HACCP risk",
            "text": "1 critical deviation not fixed on time (log \"Storage temperature\", 21 h overdue).",
            "facts": {
                "dept": "Central",
                "deviations": 1,
                "overdue_hours": 21
            },
            "location_id": 4637,
            "checklist_id": null,
            "ai": {
                "why": "The fridge alarm was ignored overnight.",
                "advice": "Check the fridge and log the corrective action."
            },
            "since": "2026-10-17"
        }
    ]
}
```

### GET /analytics/tests — Get employee test statistics

Employee tests (Testing section): title, status, pass score, questions, attempts, average %, passed; assigned / passed / overdue assignments; answers waiting for manual review. Up to 40 tests, recently changed first.

MCP tool: `analytics_tests` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `query` | query | string | no | Part of the test title |

Example response `data`:

```json
{
    "tests": [
        {
            "id": 12,
            "title": "Barista standards",
            "status": "published",
            "category": "Barista",
            "pass_score": 80,
            "questions": 8,
            "attempts": 4,
            "avg_percent": 68.5,
            "passed_attempts": 2,
            "assigned": 7,
            "assigned_passed": 2,
            "overdue": 4,
            "pending_review": 1,
            "url": "https://app.jecheck.io/ui/testing/12/edit"
        }
    ]
}
```

## Audit planner

Planned audits: the calendar of scheduled inspections with computed statuses (planned, in progress, overdue, missed, done), audit plans and recurring series across locations, rescheduling, reassigning, cancelling and marking as done, and plan templates.

### GET /planner/audits — List planned audits

Planned audits (calendar entries) in a date range, ordered by date. The status is computed on read: `done` — marked manually (`done_by: manual`) or matched by a conducted audit of the same checklist and location within the window `date ± tolerance` (`done_by: audit`, `audit_id`); `planned`, `inprogress` (today, start time passed), `overdue` (date passed, window still open), `missed` (window closed). Visible: locations where the token has `planner.view` or `planner.manage`, plus audits assigned to the token user.

MCP tool: `planner_audits_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `date_from` | query | string | no | From date (inclusive); default — today |
| `date_to` | query | string | no | To date (inclusive); default — 30 days after `date_from`; the range is limited to 366 days |
| `location_ids` | query | array<integer> | no | Only these locations |
| `auditor_ids` | query | array<integer> | no | Only these auditors |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `status` | query | array<string> | no | Filter by status: planned, inprogress, overdue, missed, done |
| `mine` | query | boolean | no | Only audits assigned to the token user |

Example response `data`:

```json
[
    {
        "id": 48211,
        "date": "2026-11-05",
        "time": "09:00",
        "duration_min": 60,
        "checklist": {
            "id": 5845,
            "name": "Daily dining room check"
        },
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "auditor": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "status": "planned",
        "done_by": null,
        "audit_id": null,
        "audit_date": null,
        "window_from": "2026-11-04",
        "window_to": "2026-11-06",
        "plan_key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
        "series_id": 1842,
        "url": "https://app.jecheck.io/ui/planner?date=2026-11-05"
    }
]
```

### GET /planner/audits/{id} — Retrieve a planned audit

MCP tool: `planner_audits_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Planned audit id |

Example response `data`:

```json
{
    "id": 48211,
    "date": "2026-11-05",
    "time": "09:00",
    "duration_min": 60,
    "checklist": {
        "id": 5845,
        "name": "Daily dining room check"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "auditor": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "status": "planned",
    "done_by": null,
    "audit_id": null,
    "audit_date": null,
    "window_from": "2026-11-04",
    "window_to": "2026-11-06",
    "plan_key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
    "series_id": 1842,
    "url": "https://app.jecheck.io/ui/planner?date=2026-11-05"
}
```

### POST /planner/audits/{id}/reschedule — Reschedule a planned audit

Moves the planned audit to another date and/or time; reminders are sent again. Requires `planner.manage` on its location.

MCP tool: `planner_audits_reschedule`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Planned audit id |
| `date` | body | string | yes | New date |
| `time` | body | string | no | New start time `HH:MM` (kept if omitted) |

Example response `data`:

```json
{
    "id": 48211,
    "date": "2026-11-05",
    "time": "09:00",
    "duration_min": 60,
    "checklist": {
        "id": 5845,
        "name": "Daily dining room check"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "auditor": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "status": "planned",
    "done_by": null,
    "audit_id": null,
    "audit_date": null,
    "window_from": "2026-11-04",
    "window_to": "2026-11-06",
    "plan_key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
    "series_id": 1842,
    "url": "https://app.jecheck.io/ui/planner?date=2026-11-05"
}
```

### POST /planner/audits/{id}/reassign — Reassign a planned audit

Assigns the planned audit to another auditor; the checklist must be available to them. Requires `planner.manage` on its location.

MCP tool: `planner_audits_reassign`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Planned audit id |
| `auditor_id` | body | integer | yes | New auditor (user id) |

Example response `data`:

```json
{
    "id": 48211,
    "date": "2026-11-05",
    "time": "09:00",
    "duration_min": 60,
    "checklist": {
        "id": 5845,
        "name": "Daily dining room check"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "auditor": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "status": "planned",
    "done_by": null,
    "audit_id": null,
    "audit_date": null,
    "window_from": "2026-11-04",
    "window_to": "2026-11-06",
    "plan_key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
    "series_id": 1842,
    "url": "https://app.jecheck.io/ui/planner?date=2026-11-05"
}
```

### POST /planner/audits/{id}/complete — Mark a planned audit as done

Manual "done" mark (e.g. the audit was done on paper). Allowed with `planner.manage` on the location or to the assigned auditor.

MCP tool: `planner_audits_complete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Planned audit id |

Example response `data`:

```json
{
    "id": 48211,
    "date": "2026-11-05",
    "time": "09:00",
    "duration_min": 60,
    "checklist": {
        "id": 5845,
        "name": "Daily dining room check"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "auditor": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "status": "planned",
    "done_by": null,
    "audit_id": null,
    "audit_date": null,
    "window_from": "2026-11-04",
    "window_to": "2026-11-06",
    "plan_key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
    "series_id": 1842,
    "url": "https://app.jecheck.io/ui/planner?date=2026-11-05"
}
```

### POST /planner/audits/{id}/uncomplete — Remove the manual done mark

The status is computed again. Fails with 422 if the audit was not marked manually.

MCP tool: `planner_audits_uncomplete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Planned audit id |

Example response `data`:

```json
{
    "id": 48211,
    "date": "2026-11-05",
    "time": "09:00",
    "duration_min": 60,
    "checklist": {
        "id": 5845,
        "name": "Daily dining room check"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "auditor": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "status": "planned",
    "done_by": null,
    "audit_id": null,
    "audit_date": null,
    "window_from": "2026-11-04",
    "window_to": "2026-11-06",
    "plan_key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
    "series_id": 1842,
    "url": "https://app.jecheck.io/ui/planner?date=2026-11-05"
}
```

### DELETE /planner/audits/{id} — Cancel a planned audit

Removes one planned audit from its plan. A done audit cannot be cancelled. Requires `planner.manage` on its location.

MCP tool: `planner_audits_cancel`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Planned audit id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /planner/plans — List audit plans

Active plans whose locations are all within the token's `planner.manage` permission. A plan is a set of recurring series of one checklist on several locations.

MCP tool: `planner_plans_list` · Permission: `planner.manage`

Example response `data`:

```json
[
    {
        "key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
        "title": "Weekly hall check",
        "checklist": {
            "id": 5845,
            "name": "Daily dining room check"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave"
            },
            {
                "id": 14,
                "name": "Dostyk Plaza"
            }
        ],
        "auditor": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "recur": "weekly",
        "weekdays": [
            1,
            4
        ],
        "month_days": [],
        "time": "09:00",
        "duration_min": 60,
        "start_date": "2026-11-03",
        "end_date": "2027-02-03",
        "tolerance_days": 1,
        "notify": {
            "email": true,
            "day1": true,
            "hour1": false,
            "escalate": true
        },
        "series_ids": [
            1842,
            1843
        ]
    }
]
```

### GET /planner/plans/{key} — Retrieve an audit plan

MCP tool: `planner_plans_get` · Permission: `planner.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key` | path | string | yes | Plan key (UUID, or `s<id>` for older single-series plans) |

Example response `data`:

```json
{
    "key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
    "title": "Weekly hall check",
    "checklist": {
        "id": 5845,
        "name": "Daily dining room check"
    },
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        },
        {
            "id": 14,
            "name": "Dostyk Plaza"
        }
    ],
    "auditor": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "recur": "weekly",
    "weekdays": [
        1,
        4
    ],
    "month_days": [],
    "time": "09:00",
    "duration_min": 60,
    "start_date": "2026-11-03",
    "end_date": "2027-02-03",
    "tolerance_days": 1,
    "notify": {
        "email": true,
        "day1": true,
        "hour1": false,
        "escalate": true
    },
    "series_ids": [
        1842,
        1843
    ]
}
```

### POST /planner/plans — Create an audit plan

Creates a plan: one series per location with planned audits generated by the recurrence rule. Requires `planner.manage` on every location of the plan. The response contains the plan and how many audits were scheduled.

MCP tool: `planner_plans_create` · Permission: `planner.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `title` | body | string | no | Plan title; default "Plan: <checklist>" |
| `checklist_id` | body | integer | yes | Checklist to audit |
| `location_ids` | body | array<integer> | yes | Locations — one series per location |
| `auditor_id` | body | integer | yes | Auditor (user id); the checklist must be available to them |
| `recur` | body | string (none, daily, weekdays, weekly, monthly) | yes | `none` — once on `start_date`, `daily`, `weekdays` (Mon–Fri), `weekly` (on `weekdays`), `monthly` (on `month_days`) |
| `weekdays` | body | array<integer> | no | For `weekly`: days of week, 0 = Sunday … 6 = Saturday |
| `month_days` | body | array<integer> | no | For `monthly`: days of month 1–31 |
| `time` | body | string | no | Start time `HH:MM`, default 09:00 |
| `duration_min` | body | integer | no | Planned duration, minutes (default 60) |
| `start_date` | body | string | yes | First day of the plan |
| `end_date` | body | string | no | Last day; default depends on the recurrence |
| `tolerance_days` | body | integer | no | An audit conducted within ± this many days of the planned date counts; default depends on the frequency |
| `notify` | body | object | no | Notifications |

Example response `data`:

```json
{
    "plan": {
        "key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
        "title": "Weekly hall check",
        "checklist": {
            "id": 5845,
            "name": "Daily dining room check"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave"
            },
            {
                "id": 14,
                "name": "Dostyk Plaza"
            }
        ],
        "auditor": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "recur": "weekly",
        "weekdays": [
            1,
            4
        ],
        "month_days": [],
        "time": "09:00",
        "duration_min": 60,
        "start_date": "2026-11-03",
        "end_date": "2027-02-03",
        "tolerance_days": 1,
        "notify": {
            "email": true,
            "day1": true,
            "hour1": false,
            "escalate": true
        },
        "series_ids": [
            1842,
            1843
        ]
    },
    "scheduled": 28
}
```

### PUT /planner/plans/{key} — Update an audit plan

Replaces the plan settings. Past audits stay as they are; upcoming ones (from today, except done) are regenerated. Removed locations lose their series, added ones get new series. Requires `planner.manage` on the old and new locations. `upcoming` — planned audits from today.

MCP tool: `planner_plans_update` · Permission: `planner.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key` | path | string | yes | Plan key (UUID, or `s<id>` for older single-series plans) |
| `title` | body | string | no | Plan title; default "Plan: <checklist>" |
| `checklist_id` | body | integer | yes | Checklist to audit |
| `location_ids` | body | array<integer> | yes | Locations — one series per location |
| `auditor_id` | body | integer | yes | Auditor (user id); the checklist must be available to them |
| `recur` | body | string (none, daily, weekdays, weekly, monthly) | yes | `none` — once on `start_date`, `daily`, `weekdays` (Mon–Fri), `weekly` (on `weekdays`), `monthly` (on `month_days`) |
| `weekdays` | body | array<integer> | no | For `weekly`: days of week, 0 = Sunday … 6 = Saturday |
| `month_days` | body | array<integer> | no | For `monthly`: days of month 1–31 |
| `time` | body | string | no | Start time `HH:MM`, default 09:00 |
| `duration_min` | body | integer | no | Planned duration, minutes (default 60) |
| `start_date` | body | string | yes | First day of the plan |
| `end_date` | body | string | no | Last day; default depends on the recurrence |
| `tolerance_days` | body | integer | no | An audit conducted within ± this many days of the planned date counts; default depends on the frequency |
| `notify` | body | object | no | Notifications |

Example response `data`:

```json
{
    "plan": {
        "key": "0b7c1f9e-2d4a-4f6b-8e3c-5a9d7b1c2e4f",
        "title": "Weekly hall check",
        "checklist": {
            "id": 5845,
            "name": "Daily dining room check"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave"
            },
            {
                "id": 14,
                "name": "Dostyk Plaza"
            }
        ],
        "auditor": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "recur": "weekly",
        "weekdays": [
            1,
            4
        ],
        "month_days": [],
        "time": "09:00",
        "duration_min": 60,
        "start_date": "2026-11-03",
        "end_date": "2027-02-03",
        "tolerance_days": 1,
        "notify": {
            "email": true,
            "day1": true,
            "hour1": false,
            "escalate": true
        },
        "series_ids": [
            1842,
            1843
        ]
    },
    "upcoming": 18
}
```

### DELETE /planner/plans/{key} — Delete an audit plan

Stops the plan: its series are retired and upcoming planned audits (except done) are removed; history stays. `removed` — how many upcoming audits were removed.

MCP tool: `planner_plans_delete` · Permission: `planner.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `key` | path | string | yes | Plan key (UUID, or `s<id>` for older single-series plans) |

Example response `data`:

```json
{
    "deleted": true,
    "removed": 12
}
```

### GET /planner/templates — List plan templates

Saved plan settings shared in the company, to create similar plans quickly.

MCP tool: `planner_templates_list` · Permission: `planner.manage`

Example response `data`:

```json
[
    {
        "id": 7,
        "name": "Weekly hall check",
        "settings": {
            "title": "Weekly hall check",
            "checklist_id": 5845,
            "location_ids": [
                12,
                14
            ],
            "auditor_id": 311,
            "recur": "weekly",
            "weekdays": [
                1,
                4
            ],
            "month_days": [],
            "time": "09:00",
            "duration_min": 60,
            "tolerance_days": 1,
            "notify": {
                "email": true,
                "day1": true,
                "hour1": false,
                "escalate": true
            }
        }
    }
]
```

### POST /planner/templates — Save a plan template

MCP tool: `planner_templates_create` · Permission: `planner.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Template name |
| `settings` | body | object | yes | Any subset of the plan fields (same names as in `planner.plans.create`) |

Example response `data`:

```json
{
    "id": 7,
    "name": "Weekly hall check",
    "settings": {
        "title": "Weekly hall check",
        "checklist_id": 5845,
        "location_ids": [
            12,
            14
        ],
        "auditor_id": 311,
        "recur": "weekly",
        "weekdays": [
            1,
            4
        ],
        "month_days": [],
        "time": "09:00",
        "duration_min": 60,
        "tolerance_days": 1,
        "notify": {
            "email": true,
            "day1": true,
            "hour1": false,
            "escalate": true
        }
    }
}
```

### DELETE /planner/templates/{id} — Delete a plan template

MCP tool: `planner_templates_delete` · Permission: `planner.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Template id |

Example response `data`:

```json
{
    "deleted": true
}
```

## Tasks

Tasks and corrective actions: create, assign, comment, attach files, move through statuses. Visibility follows the web app: the token sees tasks of its locations (`tasks.view_all`), its teams, its projects and the tasks it is involved in.

### GET /tasks — List tasks

Tasks visible to the token, newest first. Closed tasks are included; filter with `status`. Deleted tasks are never returned.

MCP tool: `tasks_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `status` | query | array<string> | no | Filter by status: new, in_progress, review, approval, done, rejected |
| `priority` | query | array<string> | no | Filter by priority: urgent, high, medium, low |
| `location_ids` | query | array<integer> | no | Only these locations |
| `assignee_id` | query | integer | no | Only tasks where this user is an executor |
| `author_id` | query | integer | no | Only tasks created by this user |
| `team_id` | query | integer | no | Only tasks of this team |
| `project_id` | query | integer | no | Only tasks of this project |
| `source` | query | string (manual, auto, haccp, request, hr, process, routine) | no | How the task was created |
| `due_from` | query | string | no | Due date from (inclusive) |
| `due_to` | query | string | no | Due date to (inclusive) |
| `overdue` | query | boolean | no | Only open tasks past their due date |
| `updated_since` | query | string | no | Only tasks changed after this moment — use for incremental sync |
| `search` | query | string | no | Search in title and description |
| `sort` | query | string (created_desc, created_asc, updated_desc, due_asc) | no |  |

Example response `data`:

```json
[
    {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "key": "OPS-42",
        "title": "Replace the broken fridge seal",
        "status": "in_progress",
        "priority": "high",
        "assignees": [
            {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            }
        ],
        "watchers": [],
        "author": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "team_id": null,
        "project_id": null,
        "due_date": "2026-11-10",
        "tags": [
            "Equipment"
        ],
        "source": "auto",
        "needs_photo": true,
        "needs_review": true,
        "comments_count": 2,
        "files_count": 1,
        "subtasks": {
            "done": 1,
            "total": 3
        },
        "audit_id": 99812,
        "created_at": "2026-11-03T10:24:00+05:00",
        "updated_at": "2026-11-03T12:01:00+05:00",
        "completed_at": null,
        "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
    }
]
```

### GET /tasks/{uuid} — Retrieve a task

Full task card: description, subtasks, files (signed links valid for 1 hour), comments and the statuses the token may set now.

MCP tool: `tasks_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "key": "OPS-42",
    "title": "Replace the broken fridge seal",
    "status": "in_progress",
    "priority": "high",
    "assignees": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ],
    "watchers": [],
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "team_id": null,
    "project_id": null,
    "due_date": "2026-11-10",
    "tags": [
        "Equipment"
    ],
    "source": "auto",
    "needs_photo": true,
    "needs_review": true,
    "comments_count": 2,
    "files_count": 1,
    "subtasks": {
        "done": 1,
        "total": 3
    },
    "audit_id": 99812,
    "created_at": "2026-11-03T10:24:00+05:00",
    "updated_at": "2026-11-03T12:01:00+05:00",
    "completed_at": null,
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
    "start_date": null,
    "estimate_minutes": null,
    "allowed_statuses": [
        "in_progress",
        "review"
    ],
    "can_delete": false,
    "audit": {
        "id": 99812,
        "checklist": "Kitchen daily check",
        "score": 82,
        "date": "2026-11-03T09:40:00+05:00"
    },
    "subtask_items": [
        {
            "id": 501,
            "title": "Order the seal",
            "done": true
        }
    ],
    "files": [
        {
            "id": 77,
            "name": "fridge.jpg",
            "size": 184220,
            "is_image": true,
            "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…",
            "created_at": "2026-11-03T10:30:00+05:00"
        }
    ],
    "comments": [
        {
            "id": 9001,
            "author": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "text": "Seal ordered, arrives tomorrow.",
            "file": null,
            "created_at": "2026-11-03T11:00:00+05:00"
        }
    ]
}
```

### POST /tasks — Create a task

Creates a task with the same rules as the web app: notifications go to assignees, a task with a team and no assignee lands in the team queue (auto-assignment if enabled).

MCP tool: `tasks_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `title` | body | string | yes | Task title |
| `description` | body | string | no | Plain-text description |
| `priority` | body | string (urgent, high, medium, low) | no | Priority, default `medium` |
| `assignee_ids` | body | array<integer> | no | Executors (user ids) |
| `watcher_ids` | body | array<integer> | no | Watchers (user ids) |
| `location_id` | body | integer | no | Location id. Must be one of the locations where the token may create tasks |
| `team_id` | body | integer | no | Team (department) id — the task goes to the team queue if no assignee |
| `due_date` | body | string | no | Due date, YYYY-MM-DD |
| `tags` | body | array<string> | no | Tags, up to 10 |
| `project_id` | body | integer | no | Project id (requires editor role in the project) |
| `start_date` | body | string | no | Start date (projects, Gantt) |
| `estimate_minutes` | body | integer | no | Estimate, minutes |
| `status` | body | string (new, in_progress, review, done, rejected) | no | Initial status, default `new` |
| `subtasks` | body | array<string> | no | Checklist items to create with the task |
| `files` | body | array<file> | no | Attachments (multipart `files[]`; in MCP — objects with `name` and `content_base64`) |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "key": "OPS-42",
    "title": "Replace the broken fridge seal",
    "status": "in_progress",
    "priority": "high",
    "assignees": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ],
    "watchers": [],
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "team_id": null,
    "project_id": null,
    "due_date": "2026-11-10",
    "tags": [
        "Equipment"
    ],
    "source": "auto",
    "needs_photo": true,
    "needs_review": true,
    "comments_count": 2,
    "files_count": 1,
    "subtasks": {
        "done": 1,
        "total": 3
    },
    "audit_id": 99812,
    "created_at": "2026-11-03T10:24:00+05:00",
    "updated_at": "2026-11-03T12:01:00+05:00",
    "completed_at": null,
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
    "start_date": null,
    "estimate_minutes": null,
    "allowed_statuses": [
        "in_progress",
        "review"
    ],
    "can_delete": false,
    "audit": {
        "id": 99812,
        "checklist": "Kitchen daily check",
        "score": 82,
        "date": "2026-11-03T09:40:00+05:00"
    },
    "subtask_items": [
        {
            "id": 501,
            "title": "Order the seal",
            "done": true
        }
    ],
    "files": [
        {
            "id": 77,
            "name": "fridge.jpg",
            "size": 184220,
            "is_image": true,
            "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…",
            "created_at": "2026-11-03T10:30:00+05:00"
        }
    ],
    "comments": [
        {
            "id": 9001,
            "author": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "text": "Seal ordered, arrives tomorrow.",
            "file": null,
            "created_at": "2026-11-03T11:00:00+05:00"
        }
    ]
}
```

### PATCH /tasks/{uuid} — Update a task

Partial update: send only the fields to change. Arrays (`assignee_ids`, `watcher_ids`, `tags`) replace the current values.

MCP tool: `tasks_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `title` | body | string | no | Task title |
| `description` | body | string | no | Plain-text description |
| `priority` | body | string (urgent, high, medium, low) | no | Priority |
| `assignee_ids` | body | array<integer> | no | Executors (user ids) |
| `watcher_ids` | body | array<integer> | no | Watchers (user ids) |
| `location_id` | body | integer | no | Location id. Must be one of the locations where the token may create tasks |
| `team_id` | body | integer | no | Team (department) id — the task goes to the team queue if no assignee |
| `due_date` | body | string | no | Due date, YYYY-MM-DD |
| `tags` | body | array<string> | no | Tags, up to 10 |
| `project_id` | body | integer | no | Project id (requires editor role in the project) |
| `start_date` | body | string | no | Start date (projects, Gantt) |
| `estimate_minutes` | body | integer | no | Estimate, minutes |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "key": "OPS-42",
    "title": "Replace the broken fridge seal",
    "status": "in_progress",
    "priority": "high",
    "assignees": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ],
    "watchers": [],
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "team_id": null,
    "project_id": null,
    "due_date": "2026-11-10",
    "tags": [
        "Equipment"
    ],
    "source": "auto",
    "needs_photo": true,
    "needs_review": true,
    "comments_count": 2,
    "files_count": 1,
    "subtasks": {
        "done": 1,
        "total": 3
    },
    "audit_id": 99812,
    "created_at": "2026-11-03T10:24:00+05:00",
    "updated_at": "2026-11-03T12:01:00+05:00",
    "completed_at": null,
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
    "start_date": null,
    "estimate_minutes": null,
    "allowed_statuses": [
        "in_progress",
        "review"
    ],
    "can_delete": false,
    "audit": {
        "id": 99812,
        "checklist": "Kitchen daily check",
        "score": 82,
        "date": "2026-11-03T09:40:00+05:00"
    },
    "subtask_items": [
        {
            "id": 501,
            "title": "Order the seal",
            "done": true
        }
    ],
    "files": [
        {
            "id": 77,
            "name": "fridge.jpg",
            "size": 184220,
            "is_image": true,
            "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…",
            "created_at": "2026-11-03T10:30:00+05:00"
        }
    ],
    "comments": [
        {
            "id": 9001,
            "author": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "text": "Seal ordered, arrives tomorrow.",
            "file": null,
            "created_at": "2026-11-03T11:00:00+05:00"
        }
    ]
}
```

### POST /tasks/{uuid}/status — Change task status

Moves the task to another status. Tasks of the corrective-action loop allow only `allowed_statuses` from the task card (e.g. a photo "after" is required before `review`). `approval` is set by process routes only.

MCP tool: `tasks_status`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `status` | body | string (new, in_progress, review, done, rejected) | yes |  |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "key": "OPS-42",
    "title": "Replace the broken fridge seal",
    "status": "in_progress",
    "priority": "high",
    "assignees": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ],
    "watchers": [],
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "team_id": null,
    "project_id": null,
    "due_date": "2026-11-10",
    "tags": [
        "Equipment"
    ],
    "source": "auto",
    "needs_photo": true,
    "needs_review": true,
    "comments_count": 2,
    "files_count": 1,
    "subtasks": {
        "done": 1,
        "total": 3
    },
    "audit_id": 99812,
    "created_at": "2026-11-03T10:24:00+05:00",
    "updated_at": "2026-11-03T12:01:00+05:00",
    "completed_at": null,
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
    "start_date": null,
    "estimate_minutes": null,
    "allowed_statuses": [
        "in_progress",
        "review"
    ],
    "can_delete": false,
    "audit": {
        "id": 99812,
        "checklist": "Kitchen daily check",
        "score": 82,
        "date": "2026-11-03T09:40:00+05:00"
    },
    "subtask_items": [
        {
            "id": 501,
            "title": "Order the seal",
            "done": true
        }
    ],
    "files": [
        {
            "id": 77,
            "name": "fridge.jpg",
            "size": 184220,
            "is_image": true,
            "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…",
            "created_at": "2026-11-03T10:30:00+05:00"
        }
    ],
    "comments": [
        {
            "id": 9001,
            "author": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "text": "Seal ordered, arrives tomorrow.",
            "file": null,
            "created_at": "2026-11-03T11:00:00+05:00"
        }
    ]
}
```

### DELETE /tasks/{uuid} — Delete a task

Soft-deletes the task. Allowed to the author or with the `tasks.delete` permission.

MCP tool: `tasks_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /tasks/{uuid}/duplicate — Duplicate a task

MCP tool: `tasks_duplicate`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "key": "OPS-42",
    "title": "Replace the broken fridge seal",
    "status": "in_progress",
    "priority": "high",
    "assignees": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ],
    "watchers": [],
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "team_id": null,
    "project_id": null,
    "due_date": "2026-11-10",
    "tags": [
        "Equipment"
    ],
    "source": "auto",
    "needs_photo": true,
    "needs_review": true,
    "comments_count": 2,
    "files_count": 1,
    "subtasks": {
        "done": 1,
        "total": 3
    },
    "audit_id": 99812,
    "created_at": "2026-11-03T10:24:00+05:00",
    "updated_at": "2026-11-03T12:01:00+05:00",
    "completed_at": null,
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
    "start_date": null,
    "estimate_minutes": null,
    "allowed_statuses": [
        "in_progress",
        "review"
    ],
    "can_delete": false,
    "audit": {
        "id": 99812,
        "checklist": "Kitchen daily check",
        "score": 82,
        "date": "2026-11-03T09:40:00+05:00"
    },
    "subtask_items": [
        {
            "id": 501,
            "title": "Order the seal",
            "done": true
        }
    ],
    "files": [
        {
            "id": 77,
            "name": "fridge.jpg",
            "size": 184220,
            "is_image": true,
            "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…",
            "created_at": "2026-11-03T10:30:00+05:00"
        }
    ],
    "comments": [
        {
            "id": 9001,
            "author": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "text": "Seal ordered, arrives tomorrow.",
            "file": null,
            "created_at": "2026-11-03T11:00:00+05:00"
        }
    ]
}
```

### POST /tasks/{uuid}/take — Take a task from the team queue

Assigns a queued team task to the acting user (must be a member of the team).

MCP tool: `tasks_take`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "key": "OPS-42",
    "title": "Replace the broken fridge seal",
    "status": "in_progress",
    "priority": "high",
    "assignees": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ],
    "watchers": [],
    "author": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "team_id": null,
    "project_id": null,
    "due_date": "2026-11-10",
    "tags": [
        "Equipment"
    ],
    "source": "auto",
    "needs_photo": true,
    "needs_review": true,
    "comments_count": 2,
    "files_count": 1,
    "subtasks": {
        "done": 1,
        "total": 3
    },
    "audit_id": 99812,
    "created_at": "2026-11-03T10:24:00+05:00",
    "updated_at": "2026-11-03T12:01:00+05:00",
    "completed_at": null,
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "description": "Seal on fridge #2 is torn, temperature rises to +9 °C.",
    "start_date": null,
    "estimate_minutes": null,
    "allowed_statuses": [
        "in_progress",
        "review"
    ],
    "can_delete": false,
    "audit": {
        "id": 99812,
        "checklist": "Kitchen daily check",
        "score": 82,
        "date": "2026-11-03T09:40:00+05:00"
    },
    "subtask_items": [
        {
            "id": 501,
            "title": "Order the seal",
            "done": true
        }
    ],
    "files": [
        {
            "id": 77,
            "name": "fridge.jpg",
            "size": 184220,
            "is_image": true,
            "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…",
            "created_at": "2026-11-03T10:30:00+05:00"
        }
    ],
    "comments": [
        {
            "id": 9001,
            "author": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "text": "Seal ordered, arrives tomorrow.",
            "file": null,
            "created_at": "2026-11-03T11:00:00+05:00"
        }
    ]
}
```

### POST /tasks/bulk — Bulk update tasks

Applies the same change to up to 200 tasks. Tasks that cannot be changed (rules of the loop, permissions) are skipped; the response says how many were updated.

MCP tool: `tasks_bulk`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuids` | body | array<string> | yes | Task UUIDs |
| `status` | body | string (new, in_progress, review, done, rejected) | no |  |
| `priority` | body | string (urgent, high, medium, low) | no |  |
| `due_date` | body | string | no |  |
| `assignee_ids` | body | array<integer> | no |  |
| `location_id` | body | integer | no |  |
| `team_id` | body | integer | no |  |
| `delete` | body | boolean | no | Delete the tasks instead of updating |

Example response `data`:

```json
{
    "updated": 18,
    "requested": 20
}
```

### GET /tasks/{uuid}/history — Get task history

Audit trail of the task: creation, status changes, reassignment, due date changes, comments by automation.

MCP tool: `tasks_history`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
[
    {
        "type": "status",
        "payload": {
            "from": "new",
            "to": "in_progress"
        },
        "actor": "user",
        "user": "Aigerim Sadykova",
        "created_at": "2026-11-03T11:00:00+05:00"
    }
]
```

### GET /tasks/{uuid}/comments — List task comments

MCP tool: `tasks_comments_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
[
    {
        "id": 9001,
        "author": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "text": "Seal ordered, arrives tomorrow.",
        "file": null,
        "created_at": "2026-11-03T11:00:00+05:00"
    }
]
```

### POST /tasks/{uuid}/comments — Add a comment

Adds a comment from the acting user. Participants are notified as in the web app.

MCP tool: `tasks_comments_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `text` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 9001,
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "text": "Seal ordered, arrives tomorrow.",
    "file": null,
    "created_at": "2026-11-03T11:00:00+05:00"
}
```

### PATCH /tasks/comments/{id} — Edit a comment

Only the author of the comment can edit it.

MCP tool: `tasks_comments_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Comment id |
| `text` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 9001,
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "text": "Seal ordered, arrives tomorrow.",
    "file": null,
    "created_at": "2026-11-03T11:00:00+05:00"
}
```

### DELETE /tasks/comments/{id} — Delete a comment

MCP tool: `tasks_comments_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Comment id |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /tasks/{uuid}/subtasks — Add a subtask

MCP tool: `tasks_subtasks_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `title` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 502,
    "title": "Install the seal",
    "done": false
}
```

### PATCH /tasks/subtasks/{id} — Update a subtask

Rename a subtask or mark it done / not done.

MCP tool: `tasks_subtasks_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Subtask id |
| `title` | body | string | no |  |
| `done` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 502,
    "title": "Install the seal",
    "done": true
}
```

### DELETE /tasks/subtasks/{id} — Delete a subtask

MCP tool: `tasks_subtasks_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Subtask id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /tasks/{uuid}/files — List task files

Attachments with signed download links (valid for 1 hour, no token needed).

MCP tool: `tasks_files_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
[
    {
        "id": 77,
        "name": "fridge.jpg",
        "size": 184220,
        "is_image": true,
        "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…",
        "created_at": "2026-11-03T10:30:00+05:00"
    }
]
```

### POST /tasks/{uuid}/files — Upload files to a task

Multipart upload, field `files[]`, up to 10 files. In MCP pass `files` as objects with `name` and `content_base64`.

MCP tool: `tasks_files_upload`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `files` | body | array<file> | yes |  |

Example response `data`:

```json
[
    {
        "id": 77,
        "name": "fridge.jpg",
        "size": 184220,
        "is_image": true,
        "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…",
        "created_at": "2026-11-03T10:30:00+05:00"
    }
]
```

### DELETE /tasks/files/{id} — Delete a task file

MCP tool: `tasks_files_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | File id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /tasks/tags — List task tags

Tag suggestions and tags already used in the company.

MCP tool: `tasks_tags`

Example response `data`:

```json
[
    "Equipment",
    "Kitchen",
    "Cleaning"
]
```

## Projects

Projects with task numbers (KEY-N), sections, board columns, milestones, custom fields, members with project roles, external guests, status updates and automation rules. Access follows the project role (viewer, commenter, editor, admin); `projects.manage` acts as admin in every project. Tasks themselves are managed in the Tasks section with `project_id`.

### GET /projects — List projects

Projects the token can open: where it is a member or owner, projects open to the whole company, and with `projects.manage` — all projects. Ordered by status (active first), then by last change.

MCP tool: `projects_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `status` | query | array<string> | no | Filter by status: active, paused, done, archived |
| `starred` | query | boolean | no | Only projects starred by the acting user |
| `member` | query | boolean | no | Only projects where the acting user is a member or owner |
| `search` | query | string | no | Search in name and key |

Example response `data`:

```json
[
    {
        "id": 58,
        "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "key": "OPEN",
        "name": "Opening: Abay Ave",
        "description": "Launch of the new coffee shop on Abay Ave.",
        "icon": "rocket_launch",
        "color": "#5b54e8",
        "status": "active",
        "health": "on_track",
        "visibility": "members",
        "start_date": "2026-11-01",
        "due_date": "2026-12-15",
        "owner": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "role": "admin",
        "created_at": "2026-10-28T09:00:00+05:00",
        "updated_at": "2026-11-03T12:10:00+05:00",
        "url": "https://app.jecheck.io/ui/projects/8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "stats": {
            "total": 42,
            "done": 18,
            "open": 23,
            "overdue": 3,
            "progress": 44
        },
        "members_count": 6,
        "guests_count": 1,
        "starred": true,
        "last_update_at": "2026-11-02T18:00:00+05:00"
    }
]
```

### GET /projects/templates — List project templates

Built-in templates and the company's own templates (saved from projects). Pass the template `id` as `template` when creating a project. Company templates are listed only with `projects.create`.

MCP tool: `projects_templates_list`

Example response `data`:

```json
[
    {
        "id": "opening",
        "name": "Opening a new location",
        "description": "Everything from lease to soft opening",
        "icon": "storefront",
        "company": false,
        "tasks_count": 34,
        "days": 60
    }
]
```

### GET /projects/{uuid} — Retrieve a project

Project card with the acting user's role and permissions, sections, board columns, milestones, custom fields, members, guests (only for project admins with `projects.guests`) and progress figures.

MCP tool: `projects_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "key": "OPEN",
    "name": "Opening: Abay Ave",
    "description": "Launch of the new coffee shop on Abay Ave.",
    "icon": "rocket_launch",
    "color": "#5b54e8",
    "status": "active",
    "health": "on_track",
    "visibility": "members",
    "start_date": "2026-11-01",
    "due_date": "2026-12-15",
    "owner": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "role": "admin",
    "created_at": "2026-10-28T09:00:00+05:00",
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/projects/8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "completed_at": null,
    "starred": true,
    "settings": {
        "guest_scope": "all",
        "time_tracking": true
    },
    "permissions": {
        "edit": true,
        "comment": true,
        "manage": true,
        "invite_guests": true,
        "join": false
    },
    "sections": [
        {
            "id": 201,
            "name": "Equipment"
        }
    ],
    "columns": [
        {
            "id": 301,
            "name": "In progress",
            "status": "in_progress",
            "color": "#f59e0b",
            "wip_limit": 5
        }
    ],
    "milestones": [
        {
            "id": 401,
            "title": "Soft opening",
            "description": "",
            "due_date": "2026-12-01",
            "done": false
        }
    ],
    "fields": [
        {
            "id": 501,
            "name": "Supplier",
            "type": "select",
            "options": [
                {
                    "id": "o_k2m9qa",
                    "label": "Metro",
                    "color": "#22c55e"
                }
            ],
            "on_card": true
        }
    ],
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "role": "admin"
        }
    ],
    "guests": [
        {
            "id": 12,
            "name": "Ivan Petrov",
            "email": "ivan@buildco.kz",
            "company": "BuildCo",
            "role": "editor",
            "tasks_count": 4,
            "last_seen_at": "2026-11-02T15:30:00+05:00",
            "invited_at": "2026-10-29T10:00:00+05:00",
            "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
        }
    ],
    "stats": {
        "total": 42,
        "done": 18,
        "open": 23,
        "overdue": 3,
        "unassigned": 2,
        "progress": 44,
        "forecast": "2026-12-20"
    }
}
```

### POST /projects — Create a project

Creates a project, empty or from a template (see `projects.templates.list`). The creator and the owner become project admins. Requires `projects.create`.

MCP tool: `projects_create` · Permission: `projects.create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Project name |
| `key` | body | string | no | Task number prefix: 2–10 Latin letters/digits starting with a letter (e.g. `OPEN`). Generated from the name if omitted |
| `description` | body | string | no | Description |
| `icon` | body | string | no | Material Symbols icon name, e.g. `rocket_launch` |
| `color` | body | string | no | Hex color, e.g. `#5b54e8` |
| `visibility` | body | string (members, company) | no | `members` — only members see the project; `company` — every employee can open it and join |
| `start_date` | body | string | no | Start date |
| `due_date` | body | string | no | Due date |
| `owner_id` | body | integer | no | Owner (user id); always an admin of the project |
| `template` | body | string | no | Template id: a built-in template key or a company template UUID |
| `members` | body | array<object> | no | Members to add |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "key": "OPEN",
    "name": "Opening: Abay Ave",
    "description": "Launch of the new coffee shop on Abay Ave.",
    "icon": "rocket_launch",
    "color": "#5b54e8",
    "status": "active",
    "health": "on_track",
    "visibility": "members",
    "start_date": "2026-11-01",
    "due_date": "2026-12-15",
    "owner": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "role": "admin",
    "created_at": "2026-10-28T09:00:00+05:00",
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/projects/8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "completed_at": null,
    "starred": true,
    "settings": {
        "guest_scope": "all",
        "time_tracking": true
    },
    "permissions": {
        "edit": true,
        "comment": true,
        "manage": true,
        "invite_guests": true,
        "join": false
    },
    "sections": [
        {
            "id": 201,
            "name": "Equipment"
        }
    ],
    "columns": [
        {
            "id": 301,
            "name": "In progress",
            "status": "in_progress",
            "color": "#f59e0b",
            "wip_limit": 5
        }
    ],
    "milestones": [
        {
            "id": 401,
            "title": "Soft opening",
            "description": "",
            "due_date": "2026-12-01",
            "done": false
        }
    ],
    "fields": [
        {
            "id": 501,
            "name": "Supplier",
            "type": "select",
            "options": [
                {
                    "id": "o_k2m9qa",
                    "label": "Metro",
                    "color": "#22c55e"
                }
            ],
            "on_card": true
        }
    ],
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "role": "admin"
        }
    ],
    "guests": [
        {
            "id": 12,
            "name": "Ivan Petrov",
            "email": "ivan@buildco.kz",
            "company": "BuildCo",
            "role": "editor",
            "tasks_count": 4,
            "last_seen_at": "2026-11-02T15:30:00+05:00",
            "invited_at": "2026-10-29T10:00:00+05:00",
            "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
        }
    ],
    "stats": {
        "total": 42,
        "done": 18,
        "open": 23,
        "overdue": 3,
        "unassigned": 2,
        "progress": 44,
        "forecast": "2026-12-20"
    }
}
```

### PATCH /projects/{uuid} — Update a project

Partial update of project settings. Requires the admin role in the project (or `projects.manage`). Setting `status` to `done` stamps the completion time.

MCP tool: `projects_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `name` | body | string | no | Project name |
| `key` | body | string | no | Task number prefix: 2–10 Latin letters/digits starting with a letter (e.g. `OPEN`). Generated from the name if omitted |
| `description` | body | string | no | Description |
| `icon` | body | string | no | Material Symbols icon name, e.g. `rocket_launch` |
| `color` | body | string | no | Hex color, e.g. `#5b54e8` |
| `visibility` | body | string (members, company) | no | `members` — only members see the project; `company` — every employee can open it and join |
| `start_date` | body | string | no | Start date |
| `due_date` | body | string | no | Due date |
| `owner_id` | body | integer | no | Owner (user id); always an admin of the project |
| `status` | body | string (active, paused, done, archived) | no | Project status |
| `health` | body | string (on_track, at_risk, off_track) | no | Health indicator |
| `guest_scope` | body | string (all, assigned) | no | What guests see: all tasks or only tasks assigned to them |
| `time_tracking` | body | boolean | no | Enable time logging in tasks |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "key": "OPEN",
    "name": "Opening: Abay Ave",
    "description": "Launch of the new coffee shop on Abay Ave.",
    "icon": "rocket_launch",
    "color": "#5b54e8",
    "status": "active",
    "health": "on_track",
    "visibility": "members",
    "start_date": "2026-11-01",
    "due_date": "2026-12-15",
    "owner": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "role": "admin",
    "created_at": "2026-10-28T09:00:00+05:00",
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/projects/8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "completed_at": null,
    "starred": true,
    "settings": {
        "guest_scope": "all",
        "time_tracking": true
    },
    "permissions": {
        "edit": true,
        "comment": true,
        "manage": true,
        "invite_guests": true,
        "join": false
    },
    "sections": [
        {
            "id": 201,
            "name": "Equipment"
        }
    ],
    "columns": [
        {
            "id": 301,
            "name": "In progress",
            "status": "in_progress",
            "color": "#f59e0b",
            "wip_limit": 5
        }
    ],
    "milestones": [
        {
            "id": 401,
            "title": "Soft opening",
            "description": "",
            "due_date": "2026-12-01",
            "done": false
        }
    ],
    "fields": [
        {
            "id": 501,
            "name": "Supplier",
            "type": "select",
            "options": [
                {
                    "id": "o_k2m9qa",
                    "label": "Metro",
                    "color": "#22c55e"
                }
            ],
            "on_card": true
        }
    ],
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "role": "admin"
        }
    ],
    "guests": [
        {
            "id": 12,
            "name": "Ivan Petrov",
            "email": "ivan@buildco.kz",
            "company": "BuildCo",
            "role": "editor",
            "tasks_count": 4,
            "last_seen_at": "2026-11-02T15:30:00+05:00",
            "invited_at": "2026-10-29T10:00:00+05:00",
            "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
        }
    ],
    "stats": {
        "total": 42,
        "done": 18,
        "open": 23,
        "overdue": 3,
        "unassigned": 2,
        "progress": 44,
        "forecast": "2026-12-20"
    }
}
```

### POST /projects/{uuid}/archive — Archive a project

Moves the project to the archive (status `archived`). Restore it with `projects.update` and `status: active`. Requires the admin role in the project.

MCP tool: `projects_archive`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "key": "OPEN",
    "name": "Opening: Abay Ave",
    "description": "Launch of the new coffee shop on Abay Ave.",
    "icon": "rocket_launch",
    "color": "#5b54e8",
    "status": "active",
    "health": "on_track",
    "visibility": "members",
    "start_date": "2026-11-01",
    "due_date": "2026-12-15",
    "owner": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "role": "admin",
    "created_at": "2026-10-28T09:00:00+05:00",
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/projects/8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "completed_at": null,
    "starred": true,
    "settings": {
        "guest_scope": "all",
        "time_tracking": true
    },
    "permissions": {
        "edit": true,
        "comment": true,
        "manage": true,
        "invite_guests": true,
        "join": false
    },
    "sections": [
        {
            "id": 201,
            "name": "Equipment"
        }
    ],
    "columns": [
        {
            "id": 301,
            "name": "In progress",
            "status": "in_progress",
            "color": "#f59e0b",
            "wip_limit": 5
        }
    ],
    "milestones": [
        {
            "id": 401,
            "title": "Soft opening",
            "description": "",
            "due_date": "2026-12-01",
            "done": false
        }
    ],
    "fields": [
        {
            "id": 501,
            "name": "Supplier",
            "type": "select",
            "options": [
                {
                    "id": "o_k2m9qa",
                    "label": "Metro",
                    "color": "#22c55e"
                }
            ],
            "on_card": true
        }
    ],
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "role": "admin"
        }
    ],
    "guests": [
        {
            "id": 12,
            "name": "Ivan Petrov",
            "email": "ivan@buildco.kz",
            "company": "BuildCo",
            "role": "editor",
            "tasks_count": 4,
            "last_seen_at": "2026-11-02T15:30:00+05:00",
            "invited_at": "2026-10-29T10:00:00+05:00",
            "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
        }
    ],
    "stats": {
        "total": 42,
        "done": 18,
        "open": 23,
        "overdue": 3,
        "unassigned": 2,
        "progress": 44,
        "forecast": "2026-12-20"
    }
}
```

### DELETE /projects/{uuid} — Delete a project

Deletes the project together with its tasks (soft delete) and revokes guest links. Requires the admin role in the project.

MCP tool: `projects_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /projects/{uuid}/duplicate — Duplicate a project

Creates a copy with the same structure and, optionally, tasks (dates shift relative to the new start date) and people. Requires `projects.create`.

MCP tool: `projects_duplicate` · Permission: `projects.create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `name` | body | string | no | Name of the copy, default "<name> (copy)" |
| `start_date` | body | string | no | Start date of the copy, default today |
| `tasks` | body | boolean | no | Copy tasks |
| `people` | body | boolean | no | Keep assignees in copied tasks |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "key": "OPEN",
    "name": "Opening: Abay Ave",
    "description": "Launch of the new coffee shop on Abay Ave.",
    "icon": "rocket_launch",
    "color": "#5b54e8",
    "status": "active",
    "health": "on_track",
    "visibility": "members",
    "start_date": "2026-11-01",
    "due_date": "2026-12-15",
    "owner": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "role": "admin",
    "created_at": "2026-10-28T09:00:00+05:00",
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/projects/8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "completed_at": null,
    "starred": true,
    "settings": {
        "guest_scope": "all",
        "time_tracking": true
    },
    "permissions": {
        "edit": true,
        "comment": true,
        "manage": true,
        "invite_guests": true,
        "join": false
    },
    "sections": [
        {
            "id": 201,
            "name": "Equipment"
        }
    ],
    "columns": [
        {
            "id": 301,
            "name": "In progress",
            "status": "in_progress",
            "color": "#f59e0b",
            "wip_limit": 5
        }
    ],
    "milestones": [
        {
            "id": 401,
            "title": "Soft opening",
            "description": "",
            "due_date": "2026-12-01",
            "done": false
        }
    ],
    "fields": [
        {
            "id": 501,
            "name": "Supplier",
            "type": "select",
            "options": [
                {
                    "id": "o_k2m9qa",
                    "label": "Metro",
                    "color": "#22c55e"
                }
            ],
            "on_card": true
        }
    ],
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "role": "admin"
        }
    ],
    "guests": [
        {
            "id": 12,
            "name": "Ivan Petrov",
            "email": "ivan@buildco.kz",
            "company": "BuildCo",
            "role": "editor",
            "tasks_count": 4,
            "last_seen_at": "2026-11-02T15:30:00+05:00",
            "invited_at": "2026-10-29T10:00:00+05:00",
            "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
        }
    ],
    "stats": {
        "total": 42,
        "done": 18,
        "open": 23,
        "overdue": 3,
        "unassigned": 2,
        "progress": 44,
        "forecast": "2026-12-20"
    }
}
```

### POST /projects/{uuid}/template — Save a project as a company template

Saves the project structure and tasks as a company template. Requires `projects.create`.

MCP tool: `projects_templates_create` · Permission: `projects.create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `name` | body | string | no | Template name, default — project name |

Example response `data`:

```json
{
    "id": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
    "name": "Opening a location",
    "company": true
}
```

### DELETE /projects/templates/{template_uuid} — Delete a company template

MCP tool: `projects_templates_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `template_uuid` | path | string | yes | Company template UUID |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /projects/{uuid}/star — Star or unstar a project

Starred projects are shown in the side menu of the acting user.

MCP tool: `projects_star`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `on` | body | boolean | no | `true` — star, `false` — unstar |

Example response `data`:

```json
{
    "starred": true
}
```

### POST /projects/{uuid}/join — Join a project

Joins a project open to the whole company as an editor.

MCP tool: `projects_join`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "key": "OPEN",
    "name": "Opening: Abay Ave",
    "description": "Launch of the new coffee shop on Abay Ave.",
    "icon": "rocket_launch",
    "color": "#5b54e8",
    "status": "active",
    "health": "on_track",
    "visibility": "members",
    "start_date": "2026-11-01",
    "due_date": "2026-12-15",
    "owner": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "role": "admin",
    "created_at": "2026-10-28T09:00:00+05:00",
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/projects/8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "completed_at": null,
    "starred": true,
    "settings": {
        "guest_scope": "all",
        "time_tracking": true
    },
    "permissions": {
        "edit": true,
        "comment": true,
        "manage": true,
        "invite_guests": true,
        "join": false
    },
    "sections": [
        {
            "id": 201,
            "name": "Equipment"
        }
    ],
    "columns": [
        {
            "id": 301,
            "name": "In progress",
            "status": "in_progress",
            "color": "#f59e0b",
            "wip_limit": 5
        }
    ],
    "milestones": [
        {
            "id": 401,
            "title": "Soft opening",
            "description": "",
            "due_date": "2026-12-01",
            "done": false
        }
    ],
    "fields": [
        {
            "id": 501,
            "name": "Supplier",
            "type": "select",
            "options": [
                {
                    "id": "o_k2m9qa",
                    "label": "Metro",
                    "color": "#22c55e"
                }
            ],
            "on_card": true
        }
    ],
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "role": "admin"
        }
    ],
    "guests": [
        {
            "id": 12,
            "name": "Ivan Petrov",
            "email": "ivan@buildco.kz",
            "company": "BuildCo",
            "role": "editor",
            "tasks_count": 4,
            "last_seen_at": "2026-11-02T15:30:00+05:00",
            "invited_at": "2026-10-29T10:00:00+05:00",
            "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
        }
    ],
    "stats": {
        "total": 42,
        "done": 18,
        "open": 23,
        "overdue": 3,
        "unassigned": 2,
        "progress": 44,
        "forecast": "2026-12-20"
    }
}
```

### GET /projects/{uuid}/stats — Get project statistics

Progress, workload by assignee, milestones progress, weekly velocity (8 weeks), burn-up and the completion forecast.

MCP tool: `projects_stats`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
{
    "total": 42,
    "done": 18,
    "open": 23,
    "overdue": 3,
    "unassigned": 2,
    "done_week": 5,
    "created_week": 7,
    "due_week": 6,
    "progress": 44,
    "estimate_minutes": 9600,
    "spent_minutes": 4100,
    "workload": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "open": 6,
            "overdue": 1
        }
    ],
    "milestones": [
        {
            "id": 401,
            "title": "Soft opening",
            "due_date": "2026-12-01",
            "done": false,
            "total": 12,
            "closed": 5
        }
    ],
    "velocity": [
        {
            "week": "2026-10-27",
            "closed": 5
        }
    ],
    "burnup": [
        {
            "date": "2026-11-01",
            "scope": 40,
            "done": 15
        }
    ],
    "forecast": "2026-12-20",
    "avg_per_week": 4.5
}
```

### GET /projects/{uuid}/activity — List project activity

Latest project and task events (up to 60), newest first. Page back with `before` = `at` of the last event.

MCP tool: `projects_activity`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `before` | query | string | no | Only events before this moment |

Example response `data`:

```json
[
    {
        "scope": "task",
        "type": "status",
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "guest_name": null,
        "payload": {
            "from": "new",
            "to": "in_progress"
        },
        "task": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "key": "OPEN-12",
            "title": "Install the espresso machine"
        },
        "at": "2026-11-03T12:01:00+05:00"
    }
]
```

### GET /projects/{uuid}/members — List project members

MCP tool: `projects_members_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
[
    {
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "role": "admin"
    }
]
```

### POST /projects/{uuid}/members — Add project members

Adds employees with one project role (default `editor`); existing members get the new role. Added people are notified. Requires the admin role in the project.

MCP tool: `projects_members_add`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `user_ids` | body | array<integer> | yes | User ids |
| `role` | body | string (admin, editor, commenter, viewer) | no | Project role |

Example response `data`:

```json
[
    {
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "role": "editor"
    }
]
```

### PATCH /projects/{uuid}/members/{user_id} — Change a member's project role

MCP tool: `projects_members_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `user_id` | path | integer | yes | User id |
| `role` | body | string (admin, editor, commenter, viewer) | yes |  |

Example response `data`:

```json
[
    {
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "role": "commenter"
    }
]
```

### DELETE /projects/{uuid}/members/{user_id} — Remove a project member

Removes a member (admin role required) or leaves the project when `user_id` is the acting user. The owner cannot be removed.

MCP tool: `projects_members_remove`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `user_id` | path | integer | yes | User id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /projects/{uuid}/guests — List project guests

External participants with personal links (`url` gives access without an account — keep it secret). Requires the admin role in the project and `projects.guests`.

MCP tool: `projects_guests_list` · Permission: `projects.guests`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
[
    {
        "id": 12,
        "name": "Ivan Petrov",
        "email": "ivan@buildco.kz",
        "company": "BuildCo",
        "role": "editor",
        "tasks_count": 4,
        "last_seen_at": "2026-11-02T15:30:00+05:00",
        "invited_at": "2026-10-29T10:00:00+05:00",
        "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
    }
]
```

### POST /projects/{uuid}/guests — Invite a project guest

Invites a contractor or client by e-mail: they get a personal link to the project (up to 50 guests per project). Requires the admin role in the project and `projects.guests`.

MCP tool: `projects_guests_invite` · Permission: `projects.guests`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `name` | body | string | yes | Guest name |
| `email` | body | string | yes | Guest e-mail |
| `company` | body | string | no | Guest company |
| `role` | body | string (editor, commenter, viewer) | no | Guest role |
| `message` | body | string | no | Personal message in the invitation |
| `locale` | body | string | no | Invitation language (ru, en, kk, …), default — company language |
| `send` | body | boolean | no | Send the invitation e-mail |

Example response `data`:

```json
{
    "id": 12,
    "name": "Ivan Petrov",
    "email": "ivan@buildco.kz",
    "company": "BuildCo",
    "role": "editor",
    "tasks_count": 4,
    "last_seen_at": "2026-11-02T15:30:00+05:00",
    "invited_at": "2026-10-29T10:00:00+05:00",
    "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
}
```

### PATCH /projects/{uuid}/guests/{guest_id} — Update a project guest

Changes the role, name or company; `resend` re-sends the invitation, `rotate` issues a new link (the old one stops working).

MCP tool: `projects_guests_update` · Permission: `projects.guests`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `guest_id` | path | integer | yes | Guest id |
| `role` | body | string (editor, commenter, viewer) | no |  |
| `name` | body | string | no |  |
| `company` | body | string | no |  |
| `resend` | body | boolean | no | Re-send the invitation e-mail |
| `rotate` | body | boolean | no | Issue a new personal link |

Example response `data`:

```json
{
    "id": 12,
    "name": "Ivan Petrov",
    "email": "ivan@buildco.kz",
    "company": "BuildCo",
    "role": "editor",
    "tasks_count": 4,
    "last_seen_at": "2026-11-02T15:30:00+05:00",
    "invited_at": "2026-10-29T10:00:00+05:00",
    "url": "https://app.jecheck.io/pg/Ab12Cd34Ef56Gh78Ij90Kl12Mn34Op56"
}
```

### DELETE /projects/{uuid}/guests/{guest_id} — Revoke a guest link

MCP tool: `projects_guests_revoke` · Permission: `projects.guests`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `guest_id` | path | integer | yes | Guest id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /projects/{uuid}/tasks — List project tasks (board)

Tasks of the project in board order (`rank`) with section, column, milestone, dates, estimate, logged time and custom field values (`fields`: field id → value). Closed tasks are included. Use the Tasks section for the full task card and changes.

MCP tool: `projects_tasks_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `uuid` | path | string | yes | Project UUID |
| `section_id` | query | integer | no | Only this section |
| `column_id` | query | integer | no | Only this board column |
| `milestone_id` | query | integer | no | Only this milestone |
| `status` | query | array<string> | no | Filter by status: new, in_progress, review, approval, done, rejected |
| `assignee_id` | query | integer | no | Only tasks assigned to this user |

Example response `data`:

```json
[
    {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "key": "OPEN-12",
        "title": "Install the espresso machine",
        "status": "in_progress",
        "priority": "high",
        "assignees": [
            {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            }
        ],
        "section_id": 201,
        "column_id": 301,
        "milestone_id": 401,
        "start_date": "2026-11-04",
        "due_date": "2026-11-10",
        "estimate_minutes": 240,
        "spent_minutes": 90,
        "tags": [
            "Equipment"
        ],
        "fields": {
            "501": "o_k2m9qa"
        },
        "guest_ids": [
            12
        ],
        "blocked_by_open": 0,
        "rank": 3.5,
        "completed_at": null,
        "updated_at": "2026-11-03T12:01:00+05:00",
        "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
    }
]
```

### POST /projects/{uuid}/tasks/{task_uuid}/move — Move a task on the board

Same as drag and drop: puts the task between `before_uuid` and `after_uuid` (rank), and optionally into another column (its status changes accordingly — loop rules may keep it), section or milestone. Requires the editor role.

MCP tool: `projects_tasks_move`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `task_uuid` | path | string | yes | Task UUID |
| `column_id` | body | integer | no | Target board column |
| `section_id` | body | integer | no | Target section (`null` — no section) |
| `milestone_id` | body | integer | no | Target milestone (`null` — none) |
| `before_uuid` | body | string | no | Task that should end up right before this one |
| `after_uuid` | body | string | no | Task that should end up right after this one |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "status": "in_progress",
    "column_id": 301,
    "section_id": 201,
    "milestone_id": 401,
    "rank": 3.5
}
```

### GET /projects/tasks/{uuid} — Get the project part of a task

Project-specific data of a task: section, column, milestone, custom field values, guests, links to other tasks and time logs.

MCP tool: `projects_tasks_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID (the task must belong to a project) |

Example response `data`:

```json
{
    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "project": {
        "id": 58,
        "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "key": "OPEN",
        "name": "Opening: Abay Ave"
    },
    "role": "editor",
    "section_id": 201,
    "column_id": 301,
    "milestone_id": 401,
    "fields": {
        "501": "o_k2m9qa"
    },
    "guest_ids": [
        12
    ],
    "links": [
        {
            "id": 77,
            "type": "blocked_by",
            "task": {
                "uuid": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
                "key": "OPEN-9",
                "title": "Deliver the machine",
                "status": "done",
                "due_date": "2026-11-05"
            }
        }
    ],
    "time_logs": [
        {
            "id": 901,
            "minutes": 90,
            "date": "2026-11-03",
            "note": "Plumbing",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "guest_name": null
        }
    ],
    "spent_minutes": 90
}
```

### PATCH /projects/tasks/{uuid} — Update the project part of a task

Sets section, milestone, board column, custom field values (`fields`: field id → value; `null` clears; select — option id, multiselect — array of option ids, user — user id) and assigned guests. Requires the editor role.

MCP tool: `projects_tasks_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID (the task must belong to a project) |
| `section_id` | body | integer | no |  |
| `milestone_id` | body | integer | no |  |
| `column_id` | body | integer | no | Board column (changes the status as on the board) |
| `fields` | body | object | no | Custom field values: {"<field id>": value} |
| `guest_ids` | body | array<integer> | no | Project guests assigned to the task (replaces the list) |

Example response `data`:

```json
{
    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "project": {
        "id": 58,
        "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "key": "OPEN",
        "name": "Opening: Abay Ave"
    },
    "role": "editor",
    "section_id": 201,
    "column_id": 301,
    "milestone_id": 401,
    "fields": {
        "501": "o_k2m9qa"
    },
    "guest_ids": [
        12
    ],
    "links": [
        {
            "id": 77,
            "type": "blocked_by",
            "task": {
                "uuid": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
                "key": "OPEN-9",
                "title": "Deliver the machine",
                "status": "done",
                "due_date": "2026-11-05"
            }
        }
    ],
    "time_logs": [
        {
            "id": 901,
            "minutes": 90,
            "date": "2026-11-03",
            "note": "Plumbing",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "guest_name": null
        }
    ],
    "spent_minutes": 90
}
```

### POST /projects/tasks/{uuid}/links — Link two tasks

Adds a dependency or relation: `blocks`, `blocked_by`, `relates`, `duplicates`. Cycles of `blocks` are rejected.

MCP tool: `projects_tasks_links_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID (the task must belong to a project) |
| `other_uuid` | body | string | yes | The other task UUID |
| `type` | body | string (blocks, blocked_by, relates, duplicates) | yes |  |

Example response `data`:

```json
{
    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "project": {
        "id": 58,
        "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "key": "OPEN",
        "name": "Opening: Abay Ave"
    },
    "role": "editor",
    "section_id": 201,
    "column_id": 301,
    "milestone_id": 401,
    "fields": {
        "501": "o_k2m9qa"
    },
    "guest_ids": [
        12
    ],
    "links": [
        {
            "id": 77,
            "type": "blocked_by",
            "task": {
                "uuid": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
                "key": "OPEN-9",
                "title": "Deliver the machine",
                "status": "done",
                "due_date": "2026-11-05"
            }
        }
    ],
    "time_logs": [
        {
            "id": 901,
            "minutes": 90,
            "date": "2026-11-03",
            "note": "Plumbing",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "guest_name": null
        }
    ],
    "spent_minutes": 90
}
```

### DELETE /projects/tasks/{uuid}/links/{id} — Remove a task link

MCP tool: `projects_tasks_links_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID (the task must belong to a project) |
| `id` | path | integer | yes | Link id |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /projects/tasks/{uuid}/time — Log time on a task

MCP tool: `projects_tasks_time_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID (the task must belong to a project) |
| `minutes` | body | integer | yes | Minutes spent (1–1440) |
| `date` | body | string | no | Day of work, default today |
| `note` | body | string | no | Note |

Example response `data`:

```json
{
    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "project": {
        "id": 58,
        "uuid": "8c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "key": "OPEN",
        "name": "Opening: Abay Ave"
    },
    "role": "editor",
    "section_id": 201,
    "column_id": 301,
    "milestone_id": 401,
    "fields": {
        "501": "o_k2m9qa"
    },
    "guest_ids": [
        12
    ],
    "links": [
        {
            "id": 77,
            "type": "blocked_by",
            "task": {
                "uuid": "5a4b3c2d-1e0f-4a9b-8c7d-6e5f4a3b2c1d",
                "key": "OPEN-9",
                "title": "Deliver the machine",
                "status": "done",
                "due_date": "2026-11-05"
            }
        }
    ],
    "time_logs": [
        {
            "id": 901,
            "minutes": 90,
            "date": "2026-11-03",
            "note": "Plumbing",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "guest_name": null
        }
    ],
    "spent_minutes": 90
}
```

### DELETE /projects/tasks/{uuid}/time/{id} — Delete a time log

Own time logs can be deleted by their author; any — by project admins.

MCP tool: `projects_tasks_time_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID (the task must belong to a project) |
| `id` | path | integer | yes | Time log id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /projects/{uuid}/updates — List status updates

MCP tool: `projects_updates_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
[
    {
        "id": 31,
        "health": "on_track",
        "body": "Equipment delivered, staff hired.",
        "stats": {
            "total": 42,
            "done": 18,
            "open": 23,
            "overdue": 3,
            "progress": 44,
            "forecast": "2026-12-20"
        },
        "author": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "created_at": "2026-11-02T18:00:00+05:00"
    }
]
```

### POST /projects/{uuid}/updates — Post a status update

Posts a status report with health; the project health changes and members are notified. Requires the editor role.

MCP tool: `projects_updates_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `health` | body | string (on_track, at_risk, off_track) | yes |  |
| `body` | body | string | yes | Report text |

Example response `data`:

```json
{
    "id": 31,
    "health": "on_track",
    "body": "Equipment delivered, staff hired.",
    "stats": {
        "total": 42,
        "done": 18,
        "open": 23,
        "overdue": 3,
        "progress": 44,
        "forecast": "2026-12-20"
    },
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "created_at": "2026-11-02T18:00:00+05:00"
}
```

### DELETE /projects/{uuid}/updates/{id} — Delete a status update

The author or a project admin can delete an update.

MCP tool: `projects_updates_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Update id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /projects/{uuid}/rules — List automation rules

Project automation rules ("when … if … then …"). Requires the admin role in the project. Ids inside `trigger`, `conditions` and `actions` refer to columns, sections, milestones, fields and users of this project.

MCP tool: `projects_rules_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |

Example response `data`:

```json
[
    {
        "id": 14,
        "name": "Done → notify the owner",
        "enabled": true,
        "trigger": {
            "type": "column_changed",
            "column": "305"
        },
        "conditions": [
            {
                "field": "priority",
                "op": "is",
                "value": "urgent"
            }
        ],
        "actions": [
            {
                "type": "notify",
                "to": [
                    "owner"
                ],
                "text": "Urgent task closed"
            }
        ],
        "runs": 12,
        "last_run_at": "2026-11-03T12:01:00+05:00"
    }
]
```

### POST /projects/{uuid}/rules — Create an automation rule

Triggers: task_created, column_changed, status_changed, section_changed, assigned, field_changed, commented, subtasks_done, unblocked, due_soon, overdue. Conditions (up to 10) on: priority, column, section, milestone, assignee, tag, title, field with op is/not/empty/not_empty/contains. Actions (1–12): move_column, move_section, set_milestone, assign, unassign, watchers, priority, due, start, tags, field, subtasks, comment, notify, create_task. Up to 50 rules per project.

MCP tool: `projects_rules_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `name` | body | string | no | Rule name |
| `trigger` | body | object | yes | {"type": "<trigger>", ...}: `column` for column_changed, `section` for section_changed, `status` for status_changed, `field`+`value` for field_changed, `days` for due_soon, `guest: true` for commented |
| `conditions` | body | array<object> | no | [{"field", "op", "value", "id"}]; `id` — custom field id when field = `field` |
| `actions` | body | array<object> | yes | [{"type", ...}], e.g. {"type": "move_column", "column": 301}, {"type": "assign", "users": [311], "mode": "add"}, {"type": "comment", "text": "…"} |
| `enabled` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 14,
    "name": "Done → notify the owner",
    "enabled": true,
    "trigger": {
        "type": "column_changed",
        "column": "305"
    },
    "conditions": [
        {
            "field": "priority",
            "op": "is",
            "value": "urgent"
        }
    ],
    "actions": [
        {
            "type": "notify",
            "to": [
                "owner"
            ],
            "text": "Urgent task closed"
        }
    ],
    "runs": 12,
    "last_run_at": "2026-11-03T12:01:00+05:00"
}
```

### PUT /projects/{uuid}/rules/{id} — Replace an automation rule

Replaces the whole rule definition (same fields as on creation).

MCP tool: `projects_rules_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Rule id |
| `name` | body | string | no | Rule name |
| `trigger` | body | object | yes | {"type": "<trigger>", ...}: `column` for column_changed, `section` for section_changed, `status` for status_changed, `field`+`value` for field_changed, `days` for due_soon, `guest: true` for commented |
| `conditions` | body | array<object> | no | [{"field", "op", "value", "id"}]; `id` — custom field id when field = `field` |
| `actions` | body | array<object> | yes | [{"type", ...}], e.g. {"type": "move_column", "column": 301}, {"type": "assign", "users": [311], "mode": "add"}, {"type": "comment", "text": "…"} |
| `enabled` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 14,
    "name": "Done → notify the owner",
    "enabled": true,
    "trigger": {
        "type": "column_changed",
        "column": "305"
    },
    "conditions": [
        {
            "field": "priority",
            "op": "is",
            "value": "urgent"
        }
    ],
    "actions": [
        {
            "type": "notify",
            "to": [
                "owner"
            ],
            "text": "Urgent task closed"
        }
    ],
    "runs": 12,
    "last_run_at": "2026-11-03T12:01:00+05:00"
}
```

### POST /projects/{uuid}/rules/{id}/toggle — Enable or disable a rule

MCP tool: `projects_rules_toggle`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Rule id |
| `enabled` | body | boolean | yes |  |

Example response `data`:

```json
{
    "id": 14,
    "name": "Done → notify the owner",
    "enabled": true,
    "trigger": {
        "type": "column_changed",
        "column": "305"
    },
    "conditions": [
        {
            "field": "priority",
            "op": "is",
            "value": "urgent"
        }
    ],
    "actions": [
        {
            "type": "notify",
            "to": [
                "owner"
            ],
            "text": "Urgent task closed"
        }
    ],
    "runs": 12,
    "last_run_at": "2026-11-03T12:01:00+05:00"
}
```

### DELETE /projects/{uuid}/rules/{id} — Delete an automation rule

MCP tool: `projects_rules_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Rule id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /projects/{uuid}/rules/{id}/runs — List rule runs

Last 50 runs of the rule: when, on which task, success or the reason of failure.

MCP tool: `projects_rules_runs`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Rule id |

Example response `data`:

```json
[
    {
        "trigger": "column_changed",
        "ok": true,
        "note": null,
        "task": {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "key": "OPEN-12",
            "title": "Install the espresso machine"
        },
        "at": "2026-11-03T12:01:00+05:00"
    }
]
```

### POST /projects/{uuid}/sections — Create a section

Adds a section to the project. Requires the editor role in the project.

MCP tool: `projects_sections_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `name` | body | string | yes | Section name |

Example response `data`:

```json
{
    "id": 201,
    "name": "Equipment"
}
```

### PATCH /projects/{uuid}/sections/{id} — Update a section

Partial update. Requires the editor role in the project.

MCP tool: `projects_sections_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Section id |
| `name` | body | string | yes | Section name |

Example response `data`:

```json
{
    "id": 201,
    "name": "Equipment"
}
```

### DELETE /projects/{uuid}/sections/{id} — Delete a section

Tasks of the section move to `move_to_id` or are left without a section.

MCP tool: `projects_sections_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Section id |
| `move_to_id` | query | integer | no | Where to move its tasks |

Example response `data`:

```json
{
    "deleted": true
}
```

### PUT /projects/{uuid}/sections/order — Reorder sections

MCP tool: `projects_sections_reorder`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `ids` | body | array<integer> | yes | All ids in the new order |

Example response `data`:

```json
[
    {
        "id": 201,
        "name": "Equipment"
    }
]
```

### POST /projects/{uuid}/columns — Create a column

Adds a column to the project. Requires the admin role in the project.

MCP tool: `projects_columns_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `name` | body | string | yes | Column name |
| `status` | body | string (new, in_progress, review, done, rejected) | yes | Task status the column stands for |
| `color` | body | string | no | Hex color |
| `wip_limit` | body | integer | no | Work-in-progress limit |

Example response `data`:

```json
{
    "id": 301,
    "name": "In progress",
    "status": "in_progress",
    "color": "#f59e0b",
    "wip_limit": 5
}
```

### PATCH /projects/{uuid}/columns/{id} — Update a column

Changing the column status moves its tasks to that status (normal status change with notifications).

MCP tool: `projects_columns_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Board column id |
| `name` | body | string | no | Column name |
| `status` | body | string (new, in_progress, review, done, rejected) | no | Task status the column stands for |
| `color` | body | string | no | Hex color |
| `wip_limit` | body | integer | no | Work-in-progress limit |

Example response `data`:

```json
{
    "id": 301,
    "name": "In progress",
    "status": "in_progress",
    "color": "#f59e0b",
    "wip_limit": 5
}
```

### DELETE /projects/{uuid}/columns/{id} — Delete a column

Tasks of the column move to `move_to_id` (or a column with the same status). The last column cannot be deleted.

MCP tool: `projects_columns_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Board column id |
| `move_to_id` | query | integer | no | Where to move its tasks |

Example response `data`:

```json
{
    "deleted": true
}
```

### PUT /projects/{uuid}/columns/order — Reorder columns

MCP tool: `projects_columns_reorder`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `ids` | body | array<integer> | yes | All ids in the new order |

Example response `data`:

```json
[
    {
        "id": 301,
        "name": "In progress",
        "status": "in_progress",
        "color": "#f59e0b",
        "wip_limit": 5
    }
]
```

### POST /projects/{uuid}/milestones — Create a milestone

Adds a milestone to the project. Requires the editor role in the project.

MCP tool: `projects_milestones_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `title` | body | string | yes | Milestone title |
| `description` | body | string | no |  |
| `due_date` | body | string | no |  |
| `done` | body | boolean | no | Mark as reached |

Example response `data`:

```json
{
    "id": 401,
    "title": "Soft opening",
    "description": "",
    "due_date": "2026-12-01",
    "done": false
}
```

### PATCH /projects/{uuid}/milestones/{id} — Update a milestone

Partial update. Requires the editor role in the project.

MCP tool: `projects_milestones_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Milestone id |
| `title` | body | string | no | Milestone title |
| `description` | body | string | no |  |
| `due_date` | body | string | no |  |
| `done` | body | boolean | no | Mark as reached |

Example response `data`:

```json
{
    "id": 401,
    "title": "Soft opening",
    "description": "",
    "due_date": "2026-12-01",
    "done": false
}
```

### DELETE /projects/{uuid}/milestones/{id} — Delete a milestone

Tasks of the milestone move to `move_to_id` or are left without a milestone.

MCP tool: `projects_milestones_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Milestone id |
| `move_to_id` | query | integer | no | Where to move its tasks |

Example response `data`:

```json
{
    "deleted": true
}
```

### PUT /projects/{uuid}/milestones/order — Reorder milestones

MCP tool: `projects_milestones_reorder`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `ids` | body | array<integer> | yes | All ids in the new order |

Example response `data`:

```json
[
    {
        "id": 401,
        "title": "Soft opening",
        "description": "",
        "due_date": "2026-12-01",
        "done": false
    }
]
```

### POST /projects/{uuid}/fields — Create a field

Adds a field to the project. Requires the admin role in the project.

MCP tool: `projects_fields_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `name` | body | string | yes | Field name |
| `type` | body | string (text, number, money, select, multiselect, date, checkbox, user, url) | yes | Field type (cannot be changed later) |
| `options` | body | array<object> | no | Options for select / multiselect (replaces the list) |
| `on_card` | body | boolean | no | Show on the board card |

Example response `data`:

```json
{
    "id": 501,
    "name": "Supplier",
    "type": "select",
    "options": [
        {
            "id": "o_k2m9qa",
            "label": "Metro",
            "color": "#22c55e"
        }
    ],
    "on_card": true
}
```

### PATCH /projects/{uuid}/fields/{id} — Update a field

Partial update. Requires the admin role in the project.

MCP tool: `projects_fields_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Custom field id |
| `name` | body | string | no | Field name |
| `options` | body | array<object> | no | Options for select / multiselect (replaces the list) |
| `on_card` | body | boolean | no | Show on the board card |

Example response `data`:

```json
{
    "id": 501,
    "name": "Supplier",
    "type": "select",
    "options": [
        {
            "id": "o_k2m9qa",
            "label": "Metro",
            "color": "#22c55e"
        }
    ],
    "on_card": true
}
```

### DELETE /projects/{uuid}/fields/{id} — Delete a field

Deletes the field and its values in all tasks.

MCP tool: `projects_fields_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `id` | path | integer | yes | Custom field id |
| `move_to_id` | query | integer | no | Where to move its tasks |

Example response `data`:

```json
{
    "deleted": true
}
```

### PUT /projects/{uuid}/fields/order — Reorder fields

MCP tool: `projects_fields_reorder`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Project UUID |
| `ids` | body | array<integer> | yes | All ids in the new order |

Example response `data`:

```json
[
    {
        "id": 501,
        "name": "Supplier",
        "type": "select",
        "options": [
            {
                "id": "o_k2m9qa",
                "label": "Metro",
                "color": "#22c55e"
            }
        ],
        "on_card": true
    }
]
```

## Processes & requests

Business processes (purchase, repair, refund…) with a request form and an approval route. Submit requests, see what waits for your decision, approve, return, reject, delegate, complete and track the route. Managing process definitions requires `processes.manage`; analytics — `processes.reports`.

### GET /processes — List processes available for requests

Published processes the acting user may submit a request for, with the published form and a summary of the route.

MCP tool: `processes_list`

Example response `data`:

```json
[
    {
        "id": 7,
        "name": "Purchase request",
        "description": "Buying equipment and supplies",
        "icon": "shopping_cart",
        "color": "#5b54e8",
        "prefix": "BUY",
        "form": {
            "title": {
                "mode": "template",
                "template": "{process} {number}: {f_item}"
            },
            "description": true,
            "location": "required",
            "priority": "input",
            "priority_value": "medium",
            "due": "input",
            "sla_hours": null,
            "fields": [
                {
                    "id": "f_item",
                    "type": "text",
                    "label": "What to buy",
                    "required": true
                },
                {
                    "id": "f_sum",
                    "type": "money",
                    "label": "Amount",
                    "required": true
                },
                {
                    "id": "f_invoice",
                    "type": "file",
                    "label": "Invoice",
                    "required": false,
                    "photo": false
                }
            ]
        },
        "location_ids": null,
        "route": [
            {
                "type": "approval",
                "name": "Manager approval"
            },
            {
                "type": "task",
                "name": "Purchase"
            }
        ]
    }
]
```

### GET /processes/{id} — Retrieve a process form

The published form of a process the acting user may submit. Field ids are the keys of `values` when submitting.

MCP tool: `processes_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |

Example response `data`:

```json
{
    "id": 7,
    "name": "Purchase request",
    "description": "Buying equipment and supplies",
    "icon": "shopping_cart",
    "color": "#5b54e8",
    "prefix": "BUY",
    "form": {
        "title": {
            "mode": "template",
            "template": "{process} {number}: {f_item}"
        },
        "description": true,
        "location": "required",
        "priority": "input",
        "priority_value": "medium",
        "due": "input",
        "sla_hours": null,
        "fields": [
            {
                "id": "f_item",
                "type": "text",
                "label": "What to buy",
                "required": true
            },
            {
                "id": "f_sum",
                "type": "money",
                "label": "Amount",
                "required": true
            },
            {
                "id": "f_invoice",
                "type": "file",
                "label": "Invoice",
                "required": false,
                "photo": false
            }
        ]
    },
    "location_ids": null,
    "route": [
        {
            "type": "approval",
            "name": "Manager approval"
        },
        {
            "type": "task",
            "name": "Purchase"
        }
    ]
}
```

### POST /processes/{id}/requests — Submit a request

Creates a request (a task with an approval route) by the published form. `values` — answers by field id. Files for `file` fields: multipart `files[]` (in MCP — objects with `name` and `content_base64`) plus `file_fields[]` with the field id of each file; if the form has one file field, `file_fields` may be omitted. Repeating the call with the same `client_uuid` returns the same request.

MCP tool: `processes_requests_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |
| `title` | body | string | no | Title (when the form asks for it; otherwise generated by the template) |
| `description` | body | string | no | Description (when the form has it) |
| `location_id` | body | integer | no | Location (required if the form says so) |
| `due_date` | body | string | no | Due date (when the form asks for it) |
| `priority` | body | string (urgent, high, medium, low) | no | Priority (when the form lets the author choose) |
| `values` | body | object | no | Answers: {"<field id>": value} |
| `watcher_ids` | body | array<integer> | no | Additional watchers |
| `files` | body | array<file> | no | Files for `file` fields |
| `file_fields` | body | array<string> | no | Field id for each file in `files` (same order) |
| `client_uuid` | body | string | no | Idempotency key |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### GET /processes/requests — List requests

Requests visible to the acting user (same visibility as tasks), newest first.

MCP tool: `processes_requests_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `process_id` | query | integer | no | Only this process |
| `state` | query | array<string> | no | Route state: running, returned, approved, done, rejected, cancelled |
| `mine` | query | boolean | no | Only requests submitted by the acting user |
| `location_ids` | query | array<integer> | no | Only these locations |
| `date_from` | query | string | no | Submitted from (inclusive) |
| `date_to` | query | string | no | Submitted to (inclusive) |
| `updated_since` | query | string | no | Only requests changed after this moment |
| `search` | query | string | no | Search in title and number |

Example response `data`:

```json
[
    {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "number": "BUY-42",
        "title": "Purchase request BUY-42: coffee grinder",
        "status": "approval",
        "state": "running",
        "process": {
            "id": 7,
            "name": "Purchase request"
        },
        "author": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "due_date": "2026-11-07",
        "started_at": "2026-11-03T10:00:00+05:00",
        "finished_at": null,
        "updated_at": "2026-11-03T10:05:00+05:00",
        "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
    }
]
```

### GET /processes/approvals — List requests waiting for me

Requests where the acting user must approve or accept the active step (`waiting` = `approval` / `review`), or that were returned to the acting user for rework (`rework`).

MCP tool: `processes_approvals`

Example response `data`:

```json
[
    {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "number": "BUY-42",
        "title": "Purchase request BUY-42: coffee grinder",
        "process": {
            "id": 7,
            "name": "Purchase request",
            "icon": "shopping_cart",
            "color": "#5b54e8"
        },
        "waiting": "approval",
        "step": "Manager approval",
        "since": "2026-11-03T10:00:00+05:00",
        "due_at": "2026-11-04T10:00:00+05:00",
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "author": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    }
]
```

### GET /processes/requests/{uuid} — Retrieve a request and its route

Form answers (file links valid for 1 hour), the route with decisions, the steps ahead (by current answers) and what the acting user may do now (`can`).

MCP tool: `processes_requests_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### POST /processes/requests/{uuid}/decide — Approve, return or reject a request

Decision of the acting user on the active approval/review step. `return` sends the request back to the author for rework; `reject` is available on approval steps. A comment may be required on reject/return (see `can.comment_required`).

MCP tool: `processes_requests_decide`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |
| `decision` | body | string (approve, return, reject) | yes |  |
| `comment` | body | string | no | Comment |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### POST /processes/requests/{uuid}/delegate — Delegate my decision

Passes the acting user's decision on the active step to another employee (if the step allows delegation).

MCP tool: `processes_requests_delegate`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |
| `user_id` | body | integer | yes | Employee to delegate to |
| `comment` | body | string | no | Comment |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### POST /processes/requests/{uuid}/complete — Complete the work step

Marks the active `task` step of the route as done; the request moves on.

MCP tool: `processes_requests_complete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |
| `comment` | body | string | no | Comment |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### POST /processes/requests/{uuid}/resubmit — Resubmit a returned request

The author sends a returned request again, optionally with corrected answers and title.

MCP tool: `processes_requests_resubmit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |
| `values` | body | object | no | Corrected answers: {"<field id>": value} |
| `title` | body | string | no |  |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### PATCH /processes/requests/{uuid}/values — Edit request answers

Changes answers of an open request — allowed when `can.edit_values` is true (the author before decisions, or `processes.override`).

MCP tool: `processes_requests_values`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |
| `values` | body | object | no | Answers to change: {"<field id>": value} |
| `title` | body | string | no |  |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### POST /processes/requests/{uuid}/cancel — Cancel a request

The author (if the process allows it) or a user with `processes.override` cancels an open request.

MCP tool: `processes_requests_cancel`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |
| `reason` | body | string | no | Reason |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### POST /processes/requests/{uuid}/skip — Skip the active step

Intervention: skips the active step of the route. Requires `processes.override` for the request location.

MCP tool: `processes_requests_skip`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### POST /processes/requests/{uuid}/reassign — Replace people on the active step

Intervention: replaces approvers / executors of the active step. Requires `processes.override`.

MCP tool: `processes_requests_reassign`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |
| `user_ids` | body | array<integer> | yes |  |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### POST /processes/requests/{uuid}/move — Move a request to another step

Intervention: returns the request to a step of the route (`override_steps[].key`). Requires `processes.override`.

MCP tool: `processes_requests_move`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Request (task) UUID |
| `step` | body | string | yes | Step key |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "number": "BUY-42",
    "title": "Purchase request BUY-42: coffee grinder",
    "status": "approval",
    "author": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "process": {
        "id": 7,
        "name": "Purchase request",
        "icon": "shopping_cart",
        "color": "#5b54e8"
    },
    "version": 3,
    "state": "running",
    "started_at": "2026-11-03T10:00:00+05:00",
    "finished_at": null,
    "values": [
        {
            "id": "f_item",
            "label": "What to buy",
            "type": "text",
            "value": "Coffee grinder",
            "text": "Coffee grinder",
            "files": []
        },
        {
            "id": "f_invoice",
            "label": "Invoice",
            "type": "file",
            "value": [
                77
            ],
            "text": "1",
            "files": [
                {
                    "id": 77,
                    "name": "invoice.pdf",
                    "is_image": false,
                    "url": "https://app.jecheck.io/ui/tasks/files/77?signature=…"
                }
            ]
        }
    ],
    "route": [
        {
            "id": 901,
            "key": "s_approve",
            "type": "approval",
            "name": "Manager approval",
            "state": "active",
            "rule": "any",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null,
            "due_at": "2026-11-04T10:00:00+05:00",
            "overdue": false,
            "people": [
                {
                    "user": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "decision": null,
                    "comment": null,
                    "decided_at": null,
                    "delegated_from": null
                }
            ]
        }
    ],
    "ahead": [
        {
            "key": "s_buy",
            "type": "task",
            "name": "Purchase",
            "users": [
                {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                }
            ]
        }
    ],
    "active_step": {
        "id": 901,
        "type": "approval",
        "name": "Manager approval",
        "rule": "any",
        "mine": true,
        "instructions": null,
        "require_photo": false
    },
    "can": {
        "decide": true,
        "delegate": true,
        "reject": true,
        "comment_required": true,
        "complete": false,
        "resubmit": false,
        "cancel": false,
        "edit_values": false,
        "override": false,
        "move_to": false
    },
    "override_steps": [],
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
}
```

### GET /processes/definitions — List all processes (library)

All processes of the company including drafts and archived ones, with request counters. Requires `processes.manage` or `processes.reports`.

MCP tool: `processes_definitions_list` · Permission: `processes.manage` or `processes.reports`

Example response `data`:

```json
[
    {
        "id": 7,
        "name": "Purchase request",
        "description": "Buying equipment and supplies",
        "icon": "shopping_cart",
        "color": "#5b54e8",
        "prefix": "BUY",
        "status": "active",
        "version": 3,
        "has_unpublished_changes": false,
        "published_at": "2026-10-20T09:00:00+05:00",
        "updated_at": "2026-10-20T09:00:00+05:00",
        "open_requests": 4,
        "total_requests": 120,
        "avg_minutes": 1440,
        "overdue_steps": 1,
        "fields_count": 5,
        "route": [
            {
                "type": "approval",
                "name": "Manager approval"
            }
        ]
    }
]
```

### GET /processes/presets — List process templates

Ready-made processes to start from (`preset` in `processes.definitions.create`).

MCP tool: `processes_presets` · Permission: `processes.manage`

Example response `data`:

```json
[
    {
        "key": "purchase",
        "name": "Purchase request",
        "description": "Approval and purchase",
        "icon": "shopping_cart",
        "color": "#5b54e8",
        "fields_count": 5,
        "route": [
            {
                "type": "approval",
                "name": "Manager approval"
            }
        ]
    }
]
```

### GET /processes/{id}/definition — Retrieve a process definition

Draft form, route and settings of a process, validation errors that block publishing and whether it has unpublished changes.

MCP tool: `processes_definitions_get` · Permission: `processes.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |

Example response `data`:

```json
{
    "id": 7,
    "name": "Purchase request",
    "description": "Buying equipment and supplies",
    "icon": "shopping_cart",
    "color": "#5b54e8",
    "prefix": "BUY",
    "status": "active",
    "version": 3,
    "has_unpublished_changes": false,
    "published_at": "2026-10-20T09:00:00+05:00",
    "form": {
        "title": {
            "mode": "template",
            "template": "{process} {number}: {f_item}"
        },
        "description": true,
        "location": "required",
        "priority": "input",
        "priority_value": "medium",
        "due": "input",
        "sla_hours": null,
        "fields": [
            {
                "id": "f_item",
                "type": "text",
                "label": "What to buy",
                "required": true
            },
            {
                "id": "f_sum",
                "type": "money",
                "label": "Amount",
                "required": true
            },
            {
                "id": "f_invoice",
                "type": "file",
                "label": "Invoice",
                "required": false,
                "photo": false
            }
        ]
    },
    "flow": {
        "steps": [
            {
                "id": "s_approve",
                "type": "approval",
                "approvers": {
                    "kind": "manager"
                },
                "rule": "any",
                "hours": 24
            }
        ]
    },
    "settings": {
        "submit": {
            "mode": "all"
        },
        "locations": null,
        "watchers": [],
        "author_cancel": true,
        "show_route": true
    },
    "errors": [],
    "has_requests": true
}
```

### POST /processes/definitions — Create a process

Creates a draft process, empty or from a template. Publish it with `processes.definitions.publish`.

MCP tool: `processes_definitions_create` · Permission: `processes.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `preset` | body | string (purchase, repair, refund, writeoff, advance, access, trip) | no | Template key |

Example response `data`:

```json
{
    "id": 7,
    "name": "Purchase request",
    "description": "Buying equipment and supplies",
    "icon": "shopping_cart",
    "color": "#5b54e8",
    "prefix": "BUY",
    "status": "active",
    "version": 3,
    "has_unpublished_changes": false,
    "published_at": "2026-10-20T09:00:00+05:00",
    "form": {
        "title": {
            "mode": "template",
            "template": "{process} {number}: {f_item}"
        },
        "description": true,
        "location": "required",
        "priority": "input",
        "priority_value": "medium",
        "due": "input",
        "sla_hours": null,
        "fields": [
            {
                "id": "f_item",
                "type": "text",
                "label": "What to buy",
                "required": true
            },
            {
                "id": "f_sum",
                "type": "money",
                "label": "Amount",
                "required": true
            },
            {
                "id": "f_invoice",
                "type": "file",
                "label": "Invoice",
                "required": false,
                "photo": false
            }
        ]
    },
    "flow": {
        "steps": [
            {
                "id": "s_approve",
                "type": "approval",
                "approvers": {
                    "kind": "manager"
                },
                "rule": "any",
                "hours": 24
            }
        ]
    },
    "settings": {
        "submit": {
            "mode": "all"
        },
        "locations": null,
        "watchers": [],
        "author_cancel": true,
        "show_route": true
    },
    "errors": [],
    "has_requests": true
}
```

### PUT /processes/{id}/definition — Save a process draft

Replaces the draft definition. Requests keep using the published version until `processes.definitions.publish`.

MCP tool: `processes_definitions_update` · Permission: `processes.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |
| `name` | body | string | yes | Process name |
| `description` | body | string | no |  |
| `icon` | body | string | no | Material Symbols icon name |
| `color` | body | string | no | Hex color |
| `prefix` | body | string | no | Request number prefix, e.g. `BUY` → BUY-42 |
| `form` | body | object | yes | Request form: title mode/template, description/location/priority/due settings and `fields` (types: text, textarea, number, money, date, datetime, select, multiselect, checkbox, user, location, file, url, phone, heading, note). Same format as returned by `processes.definitions.get` |
| `flow` | body | object | yes | Route: {"steps": [...]} with step types approval, task, review, condition, notify, set, watchers, finish. Conditions and timeout actions require the paid feature `processes_pro` |
| `settings` | body | object | yes | Who may submit (`submit`), available locations, default watchers, author cancel, route visibility |

Example response `data`:

```json
{
    "id": 7,
    "name": "Purchase request",
    "description": "Buying equipment and supplies",
    "icon": "shopping_cart",
    "color": "#5b54e8",
    "prefix": "BUY",
    "status": "active",
    "version": 3,
    "has_unpublished_changes": false,
    "published_at": "2026-10-20T09:00:00+05:00",
    "form": {
        "title": {
            "mode": "template",
            "template": "{process} {number}: {f_item}"
        },
        "description": true,
        "location": "required",
        "priority": "input",
        "priority_value": "medium",
        "due": "input",
        "sla_hours": null,
        "fields": [
            {
                "id": "f_item",
                "type": "text",
                "label": "What to buy",
                "required": true
            },
            {
                "id": "f_sum",
                "type": "money",
                "label": "Amount",
                "required": true
            },
            {
                "id": "f_invoice",
                "type": "file",
                "label": "Invoice",
                "required": false,
                "photo": false
            }
        ]
    },
    "flow": {
        "steps": [
            {
                "id": "s_approve",
                "type": "approval",
                "approvers": {
                    "kind": "manager"
                },
                "rule": "any",
                "hours": 24
            }
        ]
    },
    "settings": {
        "submit": {
            "mode": "all"
        },
        "locations": null,
        "watchers": [],
        "author_cancel": true,
        "show_route": true
    },
    "errors": [],
    "has_requests": true
}
```

### POST /processes/{id}/publish — Publish a process

Validates and publishes the draft; new requests use the new version. Returns 422 with the list of problems if the definition is not valid.

MCP tool: `processes_definitions_publish` · Permission: `processes.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |

Example response `data`:

```json
{
    "id": 7,
    "name": "Purchase request",
    "description": "Buying equipment and supplies",
    "icon": "shopping_cart",
    "color": "#5b54e8",
    "prefix": "BUY",
    "status": "active",
    "version": 3,
    "has_unpublished_changes": false,
    "published_at": "2026-10-20T09:00:00+05:00",
    "form": {
        "title": {
            "mode": "template",
            "template": "{process} {number}: {f_item}"
        },
        "description": true,
        "location": "required",
        "priority": "input",
        "priority_value": "medium",
        "due": "input",
        "sla_hours": null,
        "fields": [
            {
                "id": "f_item",
                "type": "text",
                "label": "What to buy",
                "required": true
            },
            {
                "id": "f_sum",
                "type": "money",
                "label": "Amount",
                "required": true
            },
            {
                "id": "f_invoice",
                "type": "file",
                "label": "Invoice",
                "required": false,
                "photo": false
            }
        ]
    },
    "flow": {
        "steps": [
            {
                "id": "s_approve",
                "type": "approval",
                "approvers": {
                    "kind": "manager"
                },
                "rule": "any",
                "hours": 24
            }
        ]
    },
    "settings": {
        "submit": {
            "mode": "all"
        },
        "locations": null,
        "watchers": [],
        "author_cancel": true,
        "show_route": true
    },
    "errors": [],
    "has_requests": true
}
```

### POST /processes/{id}/archive — Archive or restore a process

MCP tool: `processes_definitions_archive` · Permission: `processes.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |
| `archived` | body | boolean | no | `true` — archive, `false` — restore |

Example response `data`:

```json
{
    "id": 7,
    "name": "Purchase request",
    "description": "Buying equipment and supplies",
    "icon": "shopping_cart",
    "color": "#5b54e8",
    "prefix": "BUY",
    "status": "active",
    "version": 3,
    "has_unpublished_changes": false,
    "published_at": "2026-10-20T09:00:00+05:00",
    "form": {
        "title": {
            "mode": "template",
            "template": "{process} {number}: {f_item}"
        },
        "description": true,
        "location": "required",
        "priority": "input",
        "priority_value": "medium",
        "due": "input",
        "sla_hours": null,
        "fields": [
            {
                "id": "f_item",
                "type": "text",
                "label": "What to buy",
                "required": true
            },
            {
                "id": "f_sum",
                "type": "money",
                "label": "Amount",
                "required": true
            },
            {
                "id": "f_invoice",
                "type": "file",
                "label": "Invoice",
                "required": false,
                "photo": false
            }
        ]
    },
    "flow": {
        "steps": [
            {
                "id": "s_approve",
                "type": "approval",
                "approvers": {
                    "kind": "manager"
                },
                "rule": "any",
                "hours": 24
            }
        ]
    },
    "settings": {
        "submit": {
            "mode": "all"
        },
        "locations": null,
        "watchers": [],
        "author_cancel": true,
        "show_route": true
    },
    "errors": [],
    "has_requests": true
}
```

### POST /processes/{id}/duplicate — Duplicate a process

MCP tool: `processes_definitions_duplicate` · Permission: `processes.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |

Example response `data`:

```json
{
    "id": 7,
    "name": "Purchase request",
    "description": "Buying equipment and supplies",
    "icon": "shopping_cart",
    "color": "#5b54e8",
    "prefix": "BUY",
    "status": "active",
    "version": 3,
    "has_unpublished_changes": false,
    "published_at": "2026-10-20T09:00:00+05:00",
    "form": {
        "title": {
            "mode": "template",
            "template": "{process} {number}: {f_item}"
        },
        "description": true,
        "location": "required",
        "priority": "input",
        "priority_value": "medium",
        "due": "input",
        "sla_hours": null,
        "fields": [
            {
                "id": "f_item",
                "type": "text",
                "label": "What to buy",
                "required": true
            },
            {
                "id": "f_sum",
                "type": "money",
                "label": "Amount",
                "required": true
            },
            {
                "id": "f_invoice",
                "type": "file",
                "label": "Invoice",
                "required": false,
                "photo": false
            }
        ]
    },
    "flow": {
        "steps": [
            {
                "id": "s_approve",
                "type": "approval",
                "approvers": {
                    "kind": "manager"
                },
                "rule": "any",
                "hours": 24
            }
        ]
    },
    "settings": {
        "submit": {
            "mode": "all"
        },
        "locations": null,
        "watchers": [],
        "author_cancel": true,
        "show_route": true
    },
    "errors": [],
    "has_requests": true
}
```

### DELETE /processes/{id} — Delete a process

Only a process without requests can be deleted; otherwise archive it.

MCP tool: `processes_definitions_delete` · Permission: `processes.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |

Example response `data`:

```json
{
    "deleted": true
}
```

### PUT /processes/order — Reorder processes

MCP tool: `processes_definitions_reorder` · Permission: `processes.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `ids` | body | array<integer> | yes | Process ids in the new order |

Example response `data`:

```json
{
    "ok": true
}
```

### GET /processes/{id}/stats — Get process analytics

Requests by state, durations (average and median), time per stage, slowest approvers, who has requests waiting, by location and by week, recent requests. Limited to the locations of `processes.reports`.

MCP tool: `processes_stats` · Permission: `processes.reports` · Plan feature: `processes_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Process id |
| `days` | query | integer (7, 30, 90, 365) | no | Period, days |

Example response `data`:

```json
{
    "days": 90,
    "total": 120,
    "states": {
        "done": 100,
        "running": 12,
        "rejected": 8
    },
    "avg_minutes": 1440,
    "median_minutes": 900,
    "overdue_steps": 1,
    "rejected_share": 7,
    "stages": [
        {
            "key": "s_approve",
            "name": "Manager approval",
            "type": "approval",
            "count": 118,
            "avg_minutes": 300,
            "late": 4,
            "returned": 6,
            "rejected": 8
        }
    ],
    "approvers": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "count": 50,
            "avg_minutes": 420,
            "rejected": 3,
            "returned": 2
        }
    ],
    "waiting": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "count": 3,
            "oldest": "2026-11-01T09:00:00+05:00"
        }
    ],
    "locations": [
        {
            "location": {
                "id": 12,
                "name": "Abay Ave"
            },
            "count": 40,
            "open": 3
        }
    ],
    "weeks": [
        {
            "week": "2026-44",
            "from": "2026-10-26",
            "count": 9
        }
    ],
    "recent": [
        {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "number": "BUY-42",
            "title": "Purchase request BUY-42: coffee grinder",
            "location": {
                "id": 12,
                "name": "Abay Ave"
            },
            "author": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "state": "running",
            "started_at": "2026-11-03T10:00:00+05:00",
            "finished_at": null
        }
    ]
}
```

## Teams

Departments and teams (maintenance, IT, cleaning…): members and heads, served locations, task queue and its distribution, team schedule, QR request points. `org.view` sees all teams, a team head sees own teams, members see their teams and queue; changes require `org.manage`. Taking a queued task — `tasks.take`.

### GET /teams — List teams

With `org.view` — all teams of the company; otherwise the teams the acting user belongs to or heads.

MCP tool: `teams_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `kind` | query | string | no | Only teams of this direction |
| `location_id` | query | integer | no | Only teams serving this location |
| `mine` | query | boolean | no | Only teams the acting user belongs to |

Example response `data`:

```json
[
    {
        "id": 9,
        "uuid": "4b3a2c1d-0e9f-4a8b-9c7d-6e5f4a3b2c1d",
        "name": "Maintenance",
        "kind": "Repairs",
        "color": "#5249e6",
        "icon": "build",
        "description": "Equipment repairs in all coffee shops",
        "all_locations": false,
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave"
            }
        ],
        "routing": "load",
        "only_on_shift": true,
        "notify": "heads",
        "take_minutes": 30,
        "members": [
            {
                "user": {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                },
                "is_head": true
            }
        ],
        "members_count": 1,
        "is_member": true,
        "is_head": true
    }
]
```

### GET /teams/kinds — List team directions

Distinct directions (`kind`) of the company teams — targets for automation scenarios and QR request points.

MCP tool: `teams_kinds`

Example response `data`:

```json
[
    "Cleaning",
    "IT",
    "Repairs"
]
```

### GET /teams/{id} — Retrieve a team

Team settings, members with workload, 30-day figures (with `org.view` or the paid feature `teams_pro`) and recent tasks. Available to team heads and with `org.view`.

MCP tool: `teams_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Team id |

Example response `data`:

```json
{
    "id": 9,
    "uuid": "4b3a2c1d-0e9f-4a8b-9c7d-6e5f4a3b2c1d",
    "name": "Maintenance",
    "kind": "Repairs",
    "color": "#5249e6",
    "icon": "build",
    "description": "Equipment repairs in all coffee shops",
    "all_locations": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "routing": "load",
    "only_on_shift": true,
    "notify": "heads",
    "take_minutes": 30,
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "is_head": true
        }
    ],
    "members_count": 1,
    "is_member": true,
    "is_head": true,
    "stats": {
        "open": 7,
        "queued": 2,
        "overdue": 1,
        "done_30d": 41,
        "avg_take_minutes": 18,
        "avg_close_minutes": 640,
        "on_time_percent": 92
    },
    "workload": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "open_tasks": 3,
            "done_30d": 14
        }
    ],
    "recent_tasks": [
        {
            "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
            "title": "Coffee machine leaks",
            "status": "new",
            "assignees": [],
            "queued": true,
            "location": "Abay Ave",
            "due_date": "2026-11-04",
            "source": "request",
            "created_at": "2026-11-03T09:10:00+05:00"
        }
    ]
}
```

### GET /teams/{id}/schedule — Get the team schedule for a week

Published shifts and approved absences of team members for the week containing `from`. Available to team heads and with `org.view`.

MCP tool: `teams_schedule`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Team id |
| `from` | query | string | no | Any day of the week, default — current week |

Example response `data`:

```json
{
    "from": "2026-11-02",
    "days": [
        "2026-11-02",
        "2026-11-03"
    ],
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "on_shift_now": "Abay Ave",
            "days": [
                {
                    "date": "2026-11-02",
                    "shifts": [
                        {
                            "start": "09:00",
                            "end": "18:00",
                            "location": "Abay Ave",
                            "color": "#5b54e8"
                        }
                    ],
                    "absence": null
                }
            ]
        }
    ]
}
```

### GET /teams/queue — List the team task queue

Open tasks waiting in the queue of the acting user's teams (nobody has taken them yet), oldest first. With `org.view` any team may be requested via `team_id`. Take one with `tasks.take`, or assign with `teams.queue.assign` (team heads).

MCP tool: `teams_queue`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `team_id` | query | integer | no | Only this team |

Example response `data`:

```json
[
    {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "title": "Coffee machine leaks",
        "team_id": 9,
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "priority": "high",
        "due_date": "2026-11-04",
        "queued_at": "2026-11-03T09:10:00+05:00",
        "source": "request",
        "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
    }
]
```

### GET /teams/queue/{uuid}/candidates — List candidates for a queued task

Members of the task's team with open tasks, shift status (`here` — on shift at the task location, `elsewhere`) and absence. For team heads and administrators.

MCP tool: `teams_queue_candidates`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
[
    {
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "is_head": false,
        "open_tasks": 3,
        "shift": "here",
        "absent": false
    }
]
```

### POST /teams/queue/{uuid}/assign — Assign a queued task to a member

A team head (or administrator) assigns the task to a team member; the member is notified.

MCP tool: `teams_queue_assign`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `user_id` | body | integer | yes | Team member |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "title": "Coffee machine leaks",
    "team_id": 9,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "priority": "high",
    "due_date": "2026-11-04",
    "queued_at": "2026-11-03T09:10:00+05:00",
    "source": "request",
    "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "assignees": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ],
    "taken_at": "2026-11-03T09:20:00+05:00"
}
```

### POST /teams — Create a team

MCP tool: `teams_create` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Team name |
| `kind` | body | string | no | Direction (e.g. "Repairs", "IT") — used by automation scenarios and QR points to find the team |
| `color` | body | string | no | Hex color |
| `icon` | body | string | no | Material Symbols icon name |
| `description` | body | string | no |  |
| `all_locations` | body | boolean | no | Serves all locations (default `true`) |
| `location_ids` | body | array<integer> | no | Served locations when `all_locations` is false |
| `routing` | body | string (manual, load, round) | no | Queue distribution: manual, by load, round robin (auto-distribution requires the paid feature `teams_pro`) |
| `only_on_shift` | body | boolean | no | Auto-assign only to members on shift |
| `notify` | body | string (heads, members) | no | Who is notified about new queued tasks |
| `take_minutes` | body | integer | no | Escalate to heads if nobody takes a task within N minutes |
| `members` | body | array<object> | no | Full member list (replaces current members) |

Example response `data`:

```json
{
    "id": 9,
    "uuid": "4b3a2c1d-0e9f-4a8b-9c7d-6e5f4a3b2c1d",
    "name": "Maintenance",
    "kind": "Repairs",
    "color": "#5249e6",
    "icon": "build",
    "description": "Equipment repairs in all coffee shops",
    "all_locations": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "routing": "load",
    "only_on_shift": true,
    "notify": "heads",
    "take_minutes": 30,
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "is_head": true
        }
    ],
    "members_count": 1,
    "is_member": true,
    "is_head": true
}
```

### PATCH /teams/{id} — Update a team

Partial update. `members` replaces the whole member list; use the member operations for incremental changes.

MCP tool: `teams_update` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Team id |
| `name` | body | string | no | Team name |
| `kind` | body | string | no | Direction (e.g. "Repairs", "IT") — used by automation scenarios and QR points to find the team |
| `color` | body | string | no | Hex color |
| `icon` | body | string | no | Material Symbols icon name |
| `description` | body | string | no |  |
| `all_locations` | body | boolean | no | Serves all locations (default `true`) |
| `location_ids` | body | array<integer> | no | Served locations when `all_locations` is false |
| `routing` | body | string (manual, load, round) | no | Queue distribution: manual, by load, round robin (auto-distribution requires the paid feature `teams_pro`) |
| `only_on_shift` | body | boolean | no | Auto-assign only to members on shift |
| `notify` | body | string (heads, members) | no | Who is notified about new queued tasks |
| `take_minutes` | body | integer | no | Escalate to heads if nobody takes a task within N minutes |
| `members` | body | array<object> | no | Full member list (replaces current members) |

Example response `data`:

```json
{
    "id": 9,
    "uuid": "4b3a2c1d-0e9f-4a8b-9c7d-6e5f4a3b2c1d",
    "name": "Maintenance",
    "kind": "Repairs",
    "color": "#5249e6",
    "icon": "build",
    "description": "Equipment repairs in all coffee shops",
    "all_locations": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "routing": "load",
    "only_on_shift": true,
    "notify": "heads",
    "take_minutes": 30,
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "is_head": true
        }
    ],
    "members_count": 1,
    "is_member": true,
    "is_head": true
}
```

### DELETE /teams/{id} — Delete a team

Not allowed while the team queue has unassigned tasks. Its QR points are deactivated.

MCP tool: `teams_delete` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Team id |

Example response `data`:

```json
{
    "deleted": true
}
```

### PUT /teams/order — Reorder teams

MCP tool: `teams_reorder` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `ids` | body | array<integer> | yes | Team ids in the new order |

Example response `data`:

```json
{
    "ok": true
}
```

### POST /teams/{id}/members — Add team members

Adds employees to the team; with `from_team_id` moves them from another team (a head is moved as a regular member).

MCP tool: `teams_members_add` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Team id |
| `user_ids` | body | array<integer> | yes |  |
| `from_team_id` | body | integer | no | Move from this team |

Example response `data`:

```json
{
    "id": 9,
    "uuid": "4b3a2c1d-0e9f-4a8b-9c7d-6e5f4a3b2c1d",
    "name": "Maintenance",
    "kind": "Repairs",
    "color": "#5249e6",
    "icon": "build",
    "description": "Equipment repairs in all coffee shops",
    "all_locations": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "routing": "load",
    "only_on_shift": true,
    "notify": "heads",
    "take_minutes": 30,
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "is_head": true
        }
    ],
    "members_count": 1,
    "is_member": true,
    "is_head": true
}
```

### DELETE /teams/{id}/members/{user_id} — Remove a team member

MCP tool: `teams_members_remove` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Team id |
| `user_id` | path | integer | yes | User id |

Example response `data`:

```json
{
    "deleted": true
}
```

### PUT /teams/{id}/members/{user_id}/head — Make or unmake a team head

A head distributes the team queue and sees team figures. Making a non-member a head adds them to the team.

MCP tool: `teams_members_head` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Team id |
| `user_id` | path | integer | yes | User id |
| `is_head` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 9,
    "uuid": "4b3a2c1d-0e9f-4a8b-9c7d-6e5f4a3b2c1d",
    "name": "Maintenance",
    "kind": "Repairs",
    "color": "#5249e6",
    "icon": "build",
    "description": "Equipment repairs in all coffee shops",
    "all_locations": false,
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "routing": "load",
    "only_on_shift": true,
    "notify": "heads",
    "take_minutes": 30,
    "members": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "is_head": true
        }
    ],
    "members_count": 1,
    "is_member": true,
    "is_head": true
}
```

### GET /teams/request-points — List QR request points

QR codes placed at locations or equipment: guests and staff scan and leave a request that goes to the team queue. `url` is the public request page.

MCP tool: `teams_request_points_list` · Permission: `org.manage`

Example response `data`:

```json
[
    {
        "id": 21,
        "title": "Reception",
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "team_id": 9,
        "kind": null,
        "active": true,
        "requests_count": 17,
        "url": "https://app.jecheck.io/r/aB3dE5fG7hJ9kL1mN3pQ5rS7"
    }
]
```

### POST /teams/request-points — Create a QR request point

Requests go to `team_id` or, if not set, to the team of direction `kind` serving the location.

MCP tool: `teams_request_points_create` · Permission: `org.manage` · Plan feature: `teams_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `title` | body | string | yes | Where the code hangs, e.g. "Reception" |
| `location_id` | body | integer | yes |  |
| `team_id` | body | integer | no |  |
| `kind` | body | string | no | Direction (when no team) |
| `active` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 21,
    "title": "Reception",
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "team_id": 9,
    "kind": null,
    "active": true,
    "requests_count": 17,
    "url": "https://app.jecheck.io/r/aB3dE5fG7hJ9kL1mN3pQ5rS7"
}
```

### PATCH /teams/request-points/{id} — Update a QR request point

MCP tool: `teams_request_points_update` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Request point id |
| `title` | body | string | no |  |
| `location_id` | body | integer | no |  |
| `team_id` | body | integer | no |  |
| `kind` | body | string | no |  |
| `active` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 21,
    "title": "Reception",
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "team_id": 9,
    "kind": null,
    "active": true,
    "requests_count": 17,
    "url": "https://app.jecheck.io/r/aB3dE5fG7hJ9kL1mN3pQ5rS7"
}
```

### DELETE /teams/request-points/{id} — Delete a QR request point

MCP tool: `teams_request_points_delete` · Permission: `org.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Request point id |

Example response `data`:

```json
{
    "deleted": true
}
```

## Automation

Closed-loop automation: scenarios that turn audit violations into corrective tasks (who fixes, deadline, photo "after", confirmation, reminders, escalation), and the loop steps of such tasks — upload a photo "after", submit, accept or return. Scenarios require `automation.manage` and the paid feature `automation`.

### GET /automation/scenarios — List automation scenarios

MCP tool: `automation_scenarios_list` · Permission: `automation.manage`

Example response `data`:

```json
[
    {
        "id": 5,
        "name": "Standard",
        "is_default": true,
        "definition": {
            "v": 1,
            "steps": [
                {
                    "type": "assign",
                    "to": "responsible_role"
                },
                {
                    "type": "condition",
                    "field": "weight",
                    "op": "gte",
                    "value": 3,
                    "then": [
                        {
                            "type": "deadline",
                            "hours": 4
                        },
                        {
                            "type": "require_photo"
                        },
                        {
                            "type": "require_confirmation",
                            "by": "managers"
                        }
                    ],
                    "else": [
                        {
                            "type": "deadline",
                            "hours": 24
                        },
                        {
                            "type": "require_photo"
                        }
                    ]
                }
            ]
        },
        "checklists": [
            {
                "id": 101,
                "name": "Kitchen daily check"
            }
        ],
        "created_at": "2026-09-01T10:00:00+05:00",
        "updated_at": "2026-10-20T09:00:00+05:00"
    }
]
```

### GET /automation/scenarios/{id} — Retrieve a scenario

MCP tool: `automation_scenarios_get` · Permission: `automation.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Scenario id |

Example response `data`:

```json
{
    "id": 5,
    "name": "Standard",
    "is_default": true,
    "definition": {
        "v": 1,
        "steps": [
            {
                "type": "assign",
                "to": "responsible_role"
            },
            {
                "type": "condition",
                "field": "weight",
                "op": "gte",
                "value": 3,
                "then": [
                    {
                        "type": "deadline",
                        "hours": 4
                    },
                    {
                        "type": "require_photo"
                    },
                    {
                        "type": "require_confirmation",
                        "by": "managers"
                    }
                ],
                "else": [
                    {
                        "type": "deadline",
                        "hours": 24
                    },
                    {
                        "type": "require_photo"
                    }
                ]
            }
        ]
    },
    "checklists": [
        {
            "id": 101,
            "name": "Kitchen daily check"
        }
    ],
    "created_at": "2026-09-01T10:00:00+05:00",
    "updated_at": "2026-10-20T09:00:00+05:00"
}
```

### POST /automation/scenarios — Create a scenario

The definition is validated by the same engine as in the web editor; errors come back as `validation_failed` on `definition`.

MCP tool: `automation_scenarios_create` · Permission: `automation.manage` · Plan feature: `automation`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Scenario name |
| `definition` | body | object | yes | Block tree {"steps": [...]}. Action blocks: assign, deadline, require_photo, require_confirmation, remind, escalate, group, ai_text, ai_verify, digest, skip; `condition` blocks on weight, section, checklist_question, department, is_repeat, audit_score with `then` / `else` branches; assign targets: responsible_role, role, managers, auditor, user, team. See `automation.presets` for complete examples |
| `is_default` | body | boolean | no | Use for checklists without their own scenario |
| `checklist_ids` | body | array<integer> | no | Checklists that use this scenario (replaces the list) |

Example response `data`:

```json
{
    "id": 5,
    "name": "Standard",
    "is_default": true,
    "definition": {
        "v": 1,
        "steps": [
            {
                "type": "assign",
                "to": "responsible_role"
            },
            {
                "type": "condition",
                "field": "weight",
                "op": "gte",
                "value": 3,
                "then": [
                    {
                        "type": "deadline",
                        "hours": 4
                    },
                    {
                        "type": "require_photo"
                    },
                    {
                        "type": "require_confirmation",
                        "by": "managers"
                    }
                ],
                "else": [
                    {
                        "type": "deadline",
                        "hours": 24
                    },
                    {
                        "type": "require_photo"
                    }
                ]
            }
        ]
    },
    "checklists": [
        {
            "id": 101,
            "name": "Kitchen daily check"
        }
    ],
    "created_at": "2026-09-01T10:00:00+05:00",
    "updated_at": "2026-10-20T09:00:00+05:00"
}
```

### PATCH /automation/scenarios/{id} — Update a scenario

Partial update; `definition` replaces the whole block tree.

MCP tool: `automation_scenarios_update` · Permission: `automation.manage` · Plan feature: `automation`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Scenario id |
| `name` | body | string | no | Scenario name |
| `definition` | body | object | no | Block tree {"steps": [...]}. Action blocks: assign, deadline, require_photo, require_confirmation, remind, escalate, group, ai_text, ai_verify, digest, skip; `condition` blocks on weight, section, checklist_question, department, is_repeat, audit_score with `then` / `else` branches; assign targets: responsible_role, role, managers, auditor, user, team. See `automation.presets` for complete examples |
| `is_default` | body | boolean | no | Use for checklists without their own scenario |
| `checklist_ids` | body | array<integer> | no | Checklists that use this scenario (replaces the list) |

Example response `data`:

```json
{
    "id": 5,
    "name": "Standard",
    "is_default": true,
    "definition": {
        "v": 1,
        "steps": [
            {
                "type": "assign",
                "to": "responsible_role"
            },
            {
                "type": "condition",
                "field": "weight",
                "op": "gte",
                "value": 3,
                "then": [
                    {
                        "type": "deadline",
                        "hours": 4
                    },
                    {
                        "type": "require_photo"
                    },
                    {
                        "type": "require_confirmation",
                        "by": "managers"
                    }
                ],
                "else": [
                    {
                        "type": "deadline",
                        "hours": 24
                    },
                    {
                        "type": "require_photo"
                    }
                ]
            }
        ]
    },
    "checklists": [
        {
            "id": 101,
            "name": "Kitchen daily check"
        }
    ],
    "created_at": "2026-09-01T10:00:00+05:00",
    "updated_at": "2026-10-20T09:00:00+05:00"
}
```

### DELETE /automation/scenarios/{id} — Delete a scenario

Checklists that used it fall back to the default scenario.

MCP tool: `automation_scenarios_delete` · Permission: `automation.manage` · Plan feature: `automation`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Scenario id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /automation/presets — List scenario templates

Ready-made scenarios with complete definitions, plus the catalog of block types the editor supports.

MCP tool: `automation_presets` · Permission: `automation.manage`

Example response `data`:

```json
{
    "presets": [
        {
            "key": "standard",
            "name": "Standard",
            "description": "Critical — 4 hours, photo and confirmation; others — 24 hours and photo.",
            "definition": {
                "v": 1,
                "steps": [
                    {
                        "type": "assign",
                        "to": "responsible_role"
                    },
                    {
                        "type": "condition",
                        "field": "weight",
                        "op": "gte",
                        "value": 3,
                        "then": [
                            {
                                "type": "deadline",
                                "hours": 4
                            },
                            {
                                "type": "require_photo"
                            },
                            {
                                "type": "require_confirmation",
                                "by": "managers"
                            }
                        ],
                        "else": [
                            {
                                "type": "deadline",
                                "hours": 24
                            },
                            {
                                "type": "require_photo"
                            }
                        ]
                    }
                ]
            }
        }
    ],
    "actions": [
        "assign",
        "deadline",
        "require_photo",
        "require_confirmation",
        "remind",
        "escalate",
        "group",
        "ai_text",
        "ai_verify",
        "digest",
        "skip"
    ],
    "condition_fields": [
        "weight",
        "section",
        "checklist_question",
        "department",
        "is_repeat",
        "audit_score"
    ],
    "assign_to": [
        "responsible_role",
        "role",
        "managers",
        "auditor",
        "user",
        "team"
    ],
    "escalate_to": [
        "managers",
        "role",
        "user",
        "admins"
    ]
}
```

### POST /automation/preview — Preview a scenario on an audit

Dry run: what the (possibly unsaved) definition would do with each violation of a completed audit — no tasks are created.

MCP tool: `automation_preview` · Permission: `automation.manage` · Plan feature: `automation`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `audit_id` | body | integer | yes | Completed audit id |
| `definition` | body | object | yes |  |

Example response `data`:

```json
{
    "errors": [],
    "items": [
        {
            "action": "task",
            "assign": {
                "to": "responsible_role"
            },
            "deadline_hours": 4,
            "require_photo": true,
            "require_confirmation": "managers",
            "question": "Fridge temperature is within norm",
            "section": "Kitchen",
            "weight": 3,
            "assignees": [
                "Aigerim"
            ],
            "fallback": false,
            "is_repeat": false,
            "note": null
        }
    ]
}
```

### GET /automation/loop/{uuid} — Get the corrective-action loop of a task

For a task created from an audit violation: the audit, deadline, whether a photo "after" and confirmation are required, photos before/after, whether the acting user may accept it, AI verdict and the loop history.

MCP tool: `automation_loop_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "status": "review",
    "source": "auto",
    "audit": {
        "id": 99812,
        "uuid": "0a9b8c7d-6e5f-4a3b-8c1d-2e3f4a5b6c7d",
        "checklist": "Kitchen daily check",
        "score": 82,
        "confirmed_at": "2026-11-03T09:40:00+05:00"
    },
    "deadline_at": "2026-11-03T13:40:00+05:00",
    "requires_photo": true,
    "requires_confirmation": true,
    "has_after_photo": true,
    "can_confirm": true,
    "is_repeat": false,
    "reopen_count": 0,
    "unassigned_role": false,
    "seen_again": 0,
    "before_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/audits/before.jpg"
    ],
    "after_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/tasks/images/0001/after.jpg"
    ],
    "ai_verdict": null,
    "events": [
        {
            "type": "submitted",
            "label": "Submitted for review",
            "actor": "user",
            "user": "Aigerim",
            "payload": {
                "comment": "Seal replaced"
            },
            "at": "2026-11-03T12:30:00+05:00"
        }
    ]
}
```

### POST /automation/loop/{uuid}/photo — Upload a photo "after"

Attaches a photo of the fixed violation (JPEG/PNG/HEIC…, up to 15 MB; resized to 1920 px). Required before `submit` when `requires_photo` is true.

MCP tool: `automation_loop_photo`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `photo` | body | file | yes | Image file (multipart; in MCP — object with `name` and `content_base64`) |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "status": "review",
    "source": "auto",
    "audit": {
        "id": 99812,
        "uuid": "0a9b8c7d-6e5f-4a3b-8c1d-2e3f4a5b6c7d",
        "checklist": "Kitchen daily check",
        "score": 82,
        "confirmed_at": "2026-11-03T09:40:00+05:00"
    },
    "deadline_at": "2026-11-03T13:40:00+05:00",
    "requires_photo": true,
    "requires_confirmation": true,
    "has_after_photo": true,
    "can_confirm": true,
    "is_repeat": false,
    "reopen_count": 0,
    "unassigned_role": false,
    "seen_again": 0,
    "before_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/audits/before.jpg"
    ],
    "after_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/tasks/images/0001/after.jpg"
    ],
    "ai_verdict": null,
    "events": [
        {
            "type": "submitted",
            "label": "Submitted for review",
            "actor": "user",
            "user": "Aigerim",
            "payload": {
                "comment": "Seal replaced"
            },
            "at": "2026-11-03T12:30:00+05:00"
        }
    ]
}
```

### POST /automation/loop/{uuid}/submit — Mark the violation as fixed

The executor reports the fix: the task goes to review (if confirmation is required) or is completed. A queued team task becomes the acting user's.

MCP tool: `automation_loop_submit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "status": "review",
    "source": "auto",
    "audit": {
        "id": 99812,
        "uuid": "0a9b8c7d-6e5f-4a3b-8c1d-2e3f4a5b6c7d",
        "checklist": "Kitchen daily check",
        "score": 82,
        "confirmed_at": "2026-11-03T09:40:00+05:00"
    },
    "deadline_at": "2026-11-03T13:40:00+05:00",
    "requires_photo": true,
    "requires_confirmation": true,
    "has_after_photo": true,
    "can_confirm": true,
    "is_repeat": false,
    "reopen_count": 0,
    "unassigned_role": false,
    "seen_again": 0,
    "before_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/audits/before.jpg"
    ],
    "after_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/tasks/images/0001/after.jpg"
    ],
    "ai_verdict": null,
    "events": [
        {
            "type": "submitted",
            "label": "Submitted for review",
            "actor": "user",
            "user": "Aigerim",
            "payload": {
                "comment": "Seal replaced"
            },
            "at": "2026-11-03T12:30:00+05:00"
        }
    ]
}
```

### POST /automation/loop/{uuid}/confirm — Accept the fix

A confirmer (location manager, auditor or the one set by the scenario — see `can_confirm`) accepts the fix; the task is completed.

MCP tool: `automation_loop_confirm`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "status": "review",
    "source": "auto",
    "audit": {
        "id": 99812,
        "uuid": "0a9b8c7d-6e5f-4a3b-8c1d-2e3f4a5b6c7d",
        "checklist": "Kitchen daily check",
        "score": 82,
        "confirmed_at": "2026-11-03T09:40:00+05:00"
    },
    "deadline_at": "2026-11-03T13:40:00+05:00",
    "requires_photo": true,
    "requires_confirmation": true,
    "has_after_photo": true,
    "can_confirm": true,
    "is_repeat": false,
    "reopen_count": 0,
    "unassigned_role": false,
    "seen_again": 0,
    "before_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/audits/before.jpg"
    ],
    "after_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/tasks/images/0001/after.jpg"
    ],
    "ai_verdict": null,
    "events": [
        {
            "type": "submitted",
            "label": "Submitted for review",
            "actor": "user",
            "user": "Aigerim",
            "payload": {
                "comment": "Seal replaced"
            },
            "at": "2026-11-03T12:30:00+05:00"
        }
    ]
}
```

### POST /automation/loop/{uuid}/reject — Return the fix for rework

A confirmer returns the task to the executor with a reason; reminders restart.

MCP tool: `automation_loop_reject`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Task UUID |
| `reason` | body | string | yes |  |

Example response `data`:

```json
{
    "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
    "status": "review",
    "source": "auto",
    "audit": {
        "id": 99812,
        "uuid": "0a9b8c7d-6e5f-4a3b-8c1d-2e3f4a5b6c7d",
        "checklist": "Kitchen daily check",
        "score": 82,
        "confirmed_at": "2026-11-03T09:40:00+05:00"
    },
    "deadline_at": "2026-11-03T13:40:00+05:00",
    "requires_photo": true,
    "requires_confirmation": true,
    "has_after_photo": true,
    "can_confirm": true,
    "is_repeat": false,
    "reopen_count": 0,
    "unassigned_role": false,
    "seen_again": 0,
    "before_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/audits/before.jpg"
    ],
    "after_photos": [
        "https://bucket.s3.eu-central-1.amazonaws.com/tasks/images/0001/after.jpg"
    ],
    "ai_verdict": null,
    "events": [
        {
            "type": "submitted",
            "label": "Submitted for review",
            "actor": "user",
            "user": "Aigerim",
            "payload": {
                "comment": "Seal replaced"
            },
            "at": "2026-11-03T12:30:00+05:00"
        }
    ]
}
```

## Routines

Shift routines (SOPs without scores): routine templates with sections, steps and schedules; runs at locations with step marks, photos, completion and manager review. Visibility follows location permissions (`routines.view`, `routines.fill`, `routines.control`).

### GET /routines — List routines

Routine templates of the company (the library), in display order, with today's progress across all locations.

MCP tool: `routines_list` · Permission: `routines.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `active` | query | boolean | no | Only enabled (`true`) or disabled (`false`) routines |
| `category` | query | string (opening, closing, cleaning, shift, safety, maintenance, service, other) | no |  |
| `trigger_type` | query | string (schedule, after, on_issue, manual) | no |  |
| `location_id` | query | integer | no | Only routines that run at this location |
| `search` | query | string | no | Search in title |
| `updated_since` | query | string | no | Only routines changed after this moment |

Example response `data`:

```json
[
    {
        "id": 12,
        "uuid": "5b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
        "title": "Opening the shift",
        "description": "Everything that must be done before the first guest arrives.",
        "category": "opening",
        "color": "#f59e0b",
        "active": true,
        "trigger_type": "schedule",
        "trigger_text": "Daily, 08:00–09:00",
        "parent_routine_id": null,
        "location_ids": null,
        "position_ids": [],
        "steps_count": 8,
        "approval": false,
        "manual_start": false,
        "today": {
            "done": 3,
            "total": 4
        },
        "created_at": "2026-10-20T09:00:00+05:00",
        "updated_at": "2026-11-01T12:30:00+05:00",
        "url": "https://app.jecheck.io/ui/routines/12/edit"
    }
]
```

### GET /routines/{id} — Retrieve a routine

Full routine: trigger, conditions, control options, sections and steps with settings and display conditions.

MCP tool: `routines_get` · Permission: `routines.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Routine id |

Example response `data`:

```json
{
    "id": 12,
    "uuid": "5b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
    "title": "Opening the shift",
    "description": "Everything that must be done before the first guest arrives.",
    "category": "opening",
    "color": "#f59e0b",
    "active": true,
    "trigger_type": "schedule",
    "trigger_text": "Daily, 08:00–09:00",
    "parent_routine_id": null,
    "location_ids": null,
    "position_ids": [],
    "steps_count": 8,
    "approval": false,
    "manual_start": false,
    "today": {
        "done": 3,
        "total": 4
    },
    "created_at": "2026-10-20T09:00:00+05:00",
    "updated_at": "2026-11-01T12:30:00+05:00",
    "url": "https://app.jecheck.io/ui/routines/12/edit",
    "trigger": {
        "type": "schedule",
        "repeat": "daily",
        "windows": [
            {
                "from": "08:00",
                "to": "09:00",
                "label": "Morning"
            }
        ]
    },
    "conditions": {
        "date_from": null,
        "date_to": null,
        "skip_if_no_shift": false,
        "manual_start": false
    },
    "options": {
        "grace_minutes": 15,
        "late_hours": 2,
        "approval": "none",
        "sequential": false,
        "allow_na": true,
        "photo_from_camera": true,
        "issue_task": {
            "mode": "critical",
            "assignee": "manager",
            "assignee_id": null,
            "due_hours": 4,
            "priority": "high"
        }
    },
    "instruction_id": null,
    "template_key": "restaurant.opening",
    "sections": [
        {
            "id": 31,
            "title": "Restrooms",
            "position_ids": [],
            "repeat_for": [
                "Men",
                "Women"
            ],
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "show_if": null,
                    "instruction_id": null
                }
            ]
        }
    ]
}
```

### POST /routines — Create a routine

Creates a routine with the same validation as the web builder. It is created disabled unless `active` is `true`; enabling lays out today's and tomorrow's runs (past windows are never created).

MCP tool: `routines_create` · Permission: `routines.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `title` | body | string | yes | Routine title |
| `description` | body | string | no |  |
| `category` | body | string (opening, closing, cleaning, shift, safety, maintenance, service, other) | no |  |
| `color` | body | string | no | Hex color, e.g. `#f59e0b` |
| `active` | body | boolean | no | Enable right away (default `false`: created disabled, no runs) |
| `location_ids` | body | array<integer> | no | Locations; `null` — all locations of the company |
| `position_ids` | body | array<integer> | no | Job positions that perform the routine; empty — any staff of the location |
| `trigger` | body | object | no | When it runs. `{type: schedule, repeat: daily\|weekdays\|monthly\|dates\|every_n_days, weekdays?, month_days?, dates?, every_days?, start_date?, windows: [{from: "08:00", to: "09:00", label?}] \| interval: {from, to, every_minutes, duration_minutes}}`, `{type: after, routine_id, when: completed\|ended, delay_minutes, duration_minutes}`, `{type: on_issue, routine_id, step_id?, duration_minutes}`, `{type: manual, duration_minutes}`. Local time of the location |
| `conditions` | body | object | no | `{date_from, date_to, skip_if_no_shift, manual_start}` |
| `options` | body | object | no | Control: `{grace_minutes, late_hours, remind_open, remind_before, notify_missed, approval: none\|manager, sequential, allow_na, photo_from_camera, issue_task: {mode: none\|critical\|all, assignee: manager\|position\|user\|team, assignee_id, due_hours, priority}}` |
| `instruction_id` | body | integer | no | Knowledge-base article id |
| `sections` | body | array<object> | no | Sections with steps. Omitted — one placeholder step is created |

Example response `data`:

```json
{
    "id": 12,
    "uuid": "5b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
    "title": "Opening the shift",
    "description": "Everything that must be done before the first guest arrives.",
    "category": "opening",
    "color": "#f59e0b",
    "active": true,
    "trigger_type": "schedule",
    "trigger_text": "Daily, 08:00–09:00",
    "parent_routine_id": null,
    "location_ids": null,
    "position_ids": [],
    "steps_count": 8,
    "approval": false,
    "manual_start": false,
    "today": {
        "done": 3,
        "total": 4
    },
    "created_at": "2026-10-20T09:00:00+05:00",
    "updated_at": "2026-11-01T12:30:00+05:00",
    "url": "https://app.jecheck.io/ui/routines/12/edit",
    "trigger": {
        "type": "schedule",
        "repeat": "daily",
        "windows": [
            {
                "from": "08:00",
                "to": "09:00",
                "label": "Morning"
            }
        ]
    },
    "conditions": {
        "date_from": null,
        "date_to": null,
        "skip_if_no_shift": false,
        "manual_start": false
    },
    "options": {
        "grace_minutes": 15,
        "late_hours": 2,
        "approval": "none",
        "sequential": false,
        "allow_na": true,
        "photo_from_camera": true,
        "issue_task": {
            "mode": "critical",
            "assignee": "manager",
            "assignee_id": null,
            "due_hours": 4,
            "priority": "high"
        }
    },
    "instruction_id": null,
    "template_key": "restaurant.opening",
    "sections": [
        {
            "id": 31,
            "title": "Restrooms",
            "position_ids": [],
            "repeat_for": [
                "Men",
                "Women"
            ],
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "show_if": null,
                    "instruction_id": null
                }
            ]
        }
    ]
}
```

### PATCH /routines/{id} — Update a routine

Partial update: send only the fields to change. `sections` replaces the whole structure — pass existing `id`s of sections and steps to keep them. Future runs without marks are rebuilt.

MCP tool: `routines_update` · Permission: `routines.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Routine id |
| `title` | body | string | no | Routine title |
| `description` | body | string | no |  |
| `category` | body | string (opening, closing, cleaning, shift, safety, maintenance, service, other) | no |  |
| `color` | body | string | no | Hex color, e.g. `#f59e0b` |
| `active` | body | boolean | no | Enable / disable the routine |
| `location_ids` | body | array<integer> | no | Locations; `null` — all locations of the company |
| `position_ids` | body | array<integer> | no | Job positions that perform the routine; empty — any staff of the location |
| `trigger` | body | object | no | When it runs. `{type: schedule, repeat: daily\|weekdays\|monthly\|dates\|every_n_days, weekdays?, month_days?, dates?, every_days?, start_date?, windows: [{from: "08:00", to: "09:00", label?}] \| interval: {from, to, every_minutes, duration_minutes}}`, `{type: after, routine_id, when: completed\|ended, delay_minutes, duration_minutes}`, `{type: on_issue, routine_id, step_id?, duration_minutes}`, `{type: manual, duration_minutes}`. Local time of the location |
| `conditions` | body | object | no | `{date_from, date_to, skip_if_no_shift, manual_start}` |
| `options` | body | object | no | Control: `{grace_minutes, late_hours, remind_open, remind_before, notify_missed, approval: none\|manager, sequential, allow_na, photo_from_camera, issue_task: {mode: none\|critical\|all, assignee: manager\|position\|user\|team, assignee_id, due_hours, priority}}` |
| `instruction_id` | body | integer | no | Knowledge-base article id |
| `sections` | body | array<object> | no | Full structure: replaces sections and steps (steps with marks that are dropped go to the archive) |

Example response `data`:

```json
{
    "id": 12,
    "uuid": "5b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
    "title": "Opening the shift",
    "description": "Everything that must be done before the first guest arrives.",
    "category": "opening",
    "color": "#f59e0b",
    "active": true,
    "trigger_type": "schedule",
    "trigger_text": "Daily, 08:00–09:00",
    "parent_routine_id": null,
    "location_ids": null,
    "position_ids": [],
    "steps_count": 8,
    "approval": false,
    "manual_start": false,
    "today": {
        "done": 3,
        "total": 4
    },
    "created_at": "2026-10-20T09:00:00+05:00",
    "updated_at": "2026-11-01T12:30:00+05:00",
    "url": "https://app.jecheck.io/ui/routines/12/edit",
    "trigger": {
        "type": "schedule",
        "repeat": "daily",
        "windows": [
            {
                "from": "08:00",
                "to": "09:00",
                "label": "Morning"
            }
        ]
    },
    "conditions": {
        "date_from": null,
        "date_to": null,
        "skip_if_no_shift": false,
        "manual_start": false
    },
    "options": {
        "grace_minutes": 15,
        "late_hours": 2,
        "approval": "none",
        "sequential": false,
        "allow_na": true,
        "photo_from_camera": true,
        "issue_task": {
            "mode": "critical",
            "assignee": "manager",
            "assignee_id": null,
            "due_hours": 4,
            "priority": "high"
        }
    },
    "instruction_id": null,
    "template_key": "restaurant.opening",
    "sections": [
        {
            "id": 31,
            "title": "Restrooms",
            "position_ids": [],
            "repeat_for": [
                "Men",
                "Women"
            ],
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "show_if": null,
                    "instruction_id": null
                }
            ]
        }
    ]
}
```

### POST /routines/{id}/duplicate — Duplicate a routine

Creates a disabled copy with the same structure and settings.

MCP tool: `routines_duplicate` · Permission: `routines.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Routine id |

Example response `data`:

```json
{
    "id": 12,
    "uuid": "5b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
    "title": "Opening the shift",
    "description": "Everything that must be done before the first guest arrives.",
    "category": "opening",
    "color": "#f59e0b",
    "active": true,
    "trigger_type": "schedule",
    "trigger_text": "Daily, 08:00–09:00",
    "parent_routine_id": null,
    "location_ids": null,
    "position_ids": [],
    "steps_count": 8,
    "approval": false,
    "manual_start": false,
    "today": {
        "done": 3,
        "total": 4
    },
    "created_at": "2026-10-20T09:00:00+05:00",
    "updated_at": "2026-11-01T12:30:00+05:00",
    "url": "https://app.jecheck.io/ui/routines/12/edit",
    "trigger": {
        "type": "schedule",
        "repeat": "daily",
        "windows": [
            {
                "from": "08:00",
                "to": "09:00",
                "label": "Morning"
            }
        ]
    },
    "conditions": {
        "date_from": null,
        "date_to": null,
        "skip_if_no_shift": false,
        "manual_start": false
    },
    "options": {
        "grace_minutes": 15,
        "late_hours": 2,
        "approval": "none",
        "sequential": false,
        "allow_na": true,
        "photo_from_camera": true,
        "issue_task": {
            "mode": "critical",
            "assignee": "manager",
            "assignee_id": null,
            "due_hours": 4,
            "priority": "high"
        }
    },
    "instruction_id": null,
    "template_key": "restaurant.opening",
    "sections": [
        {
            "id": 31,
            "title": "Restrooms",
            "position_ids": [],
            "repeat_for": [
                "Men",
                "Women"
            ],
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "show_if": null,
                    "instruction_id": null
                }
            ]
        }
    ]
}
```

### DELETE /routines/{id} — Delete a routine

Disables and deletes the routine; unstarted runs are removed, history is kept. Not allowed while other routines are chained to it (`after` / `on_issue`).

MCP tool: `routines_delete` · Permission: `routines.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Routine id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /routines/templates — List routine templates

Ready-made routine packs by business type (restaurant, shop, hotel, …) that can be installed with `routines.templates.install`.

MCP tool: `routines_templates_list` · Permission: `routines.manage`

Example response `data`:

```json
[
    {
        "key": "restaurant",
        "title": "Restaurant / cafe",
        "description": "Opening, cleaning, closing and deliveries",
        "icon": "restaurant",
        "routines": [
            {
                "key": "opening",
                "title": "Opening the shift",
                "category": "opening",
                "trigger": "Daily, 08:00–09:00",
                "trigger_type": "schedule",
                "steps": 12,
                "repeat": false
            }
        ]
    }
]
```

### POST /routines/templates/install — Install a routine template

Installs a whole pack, or one routine of it (`routine_key`) together with its chain parent. Routines are created disabled.

MCP tool: `routines_templates_install` · Permission: `routines.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `pack` | body | string | yes | Pack key from `routines.templates.list` |
| `routine_key` | body | string | no | Install only this routine of the pack |

Example response `data`:

```json
[
    {
        "id": 12,
        "uuid": "5b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
        "title": "Opening the shift",
        "description": "Everything that must be done before the first guest arrives.",
        "category": "opening",
        "color": "#f59e0b",
        "active": true,
        "trigger_type": "schedule",
        "trigger_text": "Daily, 08:00–09:00",
        "parent_routine_id": null,
        "location_ids": null,
        "position_ids": [],
        "steps_count": 8,
        "approval": false,
        "manual_start": false,
        "today": {
            "done": 3,
            "total": 4
        },
        "created_at": "2026-10-20T09:00:00+05:00",
        "updated_at": "2026-11-01T12:30:00+05:00",
        "url": "https://app.jecheck.io/ui/routines/12/edit"
    }
]
```

### POST /routines/from-checklist/{checklist_id} — Create a routine from an audit checklist

Converts checklist questions into routine steps without scores. The checklist is not changed; the routine is created disabled.

MCP tool: `routines_from_checklist` · Permission: `routines.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `checklist_id` | path | integer | yes | Checklist id |

Example response `data`:

```json
{
    "id": 12,
    "uuid": "5b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
    "title": "Opening the shift",
    "description": "Everything that must be done before the first guest arrives.",
    "category": "opening",
    "color": "#f59e0b",
    "active": true,
    "trigger_type": "schedule",
    "trigger_text": "Daily, 08:00–09:00",
    "parent_routine_id": null,
    "location_ids": null,
    "position_ids": [],
    "steps_count": 8,
    "approval": false,
    "manual_start": false,
    "today": {
        "done": 3,
        "total": 4
    },
    "created_at": "2026-10-20T09:00:00+05:00",
    "updated_at": "2026-11-01T12:30:00+05:00",
    "url": "https://app.jecheck.io/ui/routines/12/edit",
    "trigger": {
        "type": "schedule",
        "repeat": "daily",
        "windows": [
            {
                "from": "08:00",
                "to": "09:00",
                "label": "Morning"
            }
        ]
    },
    "conditions": {
        "date_from": null,
        "date_to": null,
        "skip_if_no_shift": false,
        "manual_start": false
    },
    "options": {
        "grace_minutes": 15,
        "late_hours": 2,
        "approval": "none",
        "sequential": false,
        "allow_na": true,
        "photo_from_camera": true,
        "issue_task": {
            "mode": "critical",
            "assignee": "manager",
            "assignee_id": null,
            "due_hours": 4,
            "priority": "high"
        }
    },
    "instruction_id": null,
    "template_key": "restaurant.opening",
    "sections": [
        {
            "id": 31,
            "title": "Restrooms",
            "position_ids": [],
            "repeat_for": [
                "Men",
                "Women"
            ],
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "show_if": null,
                    "instruction_id": null
                }
            ]
        }
    ]
}
```

### GET /routines/runs — List routine runs

Runs at the locations visible to the token (`routines.view`), newest window first. `overdue` is computed: the due time passed but the run is not closed yet.

MCP tool: `routines_runs_list` · Permission: `routines.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `location_ids` | query | array<integer> | no | Only these locations |
| `date` | query | string | no | Only this local day |
| `date_from` | query | string | no | Local day from (inclusive) |
| `date_to` | query | string | no | Local day to (inclusive) |
| `routine_id` | query | integer | no | Only runs of this routine |
| `status` | query | array<string> | no | Filter by status: pending, in_progress, review, done, overdue, incomplete, missed, skipped |
| `user_id` | query | integer | no | Only runs started, completed or marked by this user |
| `trigger_type` | query | string (schedule, after, on_issue, manual) | no |  |
| `updated_since` | query | string | no | Only runs changed after this moment — use for incremental sync |

Example response `data`:

```json
[
    {
        "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
        "routine": {
            "id": 12,
            "title": "Opening the shift"
        },
        "category": "opening",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "day": "2026-11-03",
        "trigger_type": "schedule",
        "label": "Morning",
        "opens_at": "2026-11-03T08:00:00+05:00",
        "due_at": "2026-11-03T09:00:00+05:00",
        "status": "in_progress",
        "late": false,
        "progress": {
            "done": 5,
            "total": 8,
            "issues": 1
        },
        "started_at": "2026-11-03T08:04:00+05:00",
        "started_by": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "completed_at": null,
        "completed_by": null,
        "reviewed_at": null,
        "reviewed_by": null,
        "review_note": null,
        "skip_reason": null,
        "parent_run_uuid": null,
        "updated_at": "2026-11-03T08:31:00+05:00",
        "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d"
    }
]
```

### GET /routines/runs/{uuid} — Retrieve a routine run

Run with sections, steps and marks (photos as signed links valid for 1 hour), required steps still missing and the actions the token may perform now (`can`).

MCP tool: `routines_runs_get` · Permission: `routines.view` or `routines.fill`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Run UUID |

Example response `data`:

```json
{
    "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "routine": {
        "id": 12,
        "title": "Opening the shift"
    },
    "category": "opening",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "day": "2026-11-03",
    "trigger_type": "schedule",
    "label": "Morning",
    "opens_at": "2026-11-03T08:00:00+05:00",
    "due_at": "2026-11-03T09:00:00+05:00",
    "status": "in_progress",
    "late": false,
    "progress": {
        "done": 5,
        "total": 8,
        "issues": 1
    },
    "started_at": "2026-11-03T08:04:00+05:00",
    "started_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "completed_at": null,
    "completed_by": null,
    "reviewed_at": null,
    "reviewed_by": null,
    "review_note": null,
    "skip_reason": null,
    "parent_run_uuid": null,
    "updated_at": "2026-11-03T08:31:00+05:00",
    "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "description": "Everything that must be done before the first guest arrives.",
    "instruction": {
        "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "title": "Opening standard"
    },
    "options": {
        "sequential": false,
        "allow_na": true,
        "approval": "none",
        "photo_from_camera": true
    },
    "sections": [
        {
            "section_id": 31,
            "title": "Restrooms",
            "item": 0,
            "item_name": "Men",
            "can_fill": true,
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "conditional": false,
                    "instruction": null,
                    "mark": {
                        "status": "issue",
                        "value": "none",
                        "display": "Out of soap",
                        "flag": null,
                        "reason": null,
                        "comment": "Ordered from the warehouse",
                        "photos": [
                            {
                                "path": "routines/761/photos/2026/11/7f1e.jpg",
                                "url": "https://storage.example.com/routines/761/photos/2026/11/7f1e.jpg?X-Amz-Signature=…"
                            }
                        ],
                        "user": {
                            "id": 311,
                            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                            "name": "Aigerim Sadykova"
                        },
                        "marked_at": "2026-11-03T08:12:00+05:00",
                        "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
                        "spawned_run_uuid": null
                    }
                }
            ]
        }
    ],
    "missing_required": [
        "Restrooms (Women): Liquid soap"
    ],
    "children": [
        {
            "uuid": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
            "title": "Restock consumables",
            "status": "pending",
            "trigger_type": "on_issue"
        }
    ],
    "can": {
        "fill": true,
        "complete": true,
        "accept": false,
        "return": false,
        "skip": true
    }
}
```

### GET /routines/board — Get the day board

All runs of a day by location with totals (what the "Daily schedule" screen shows), plus routines the token may start manually.

MCP tool: `routines_board` · Permission: `routines.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_ids` | query | array<integer> | no | Only these locations (default — all visible) |
| `date` | query | string | no | Local day, default — today of the company |

Example response `data`:

```json
{
    "day": "2026-11-03",
    "summary": {
        "total": 12,
        "due": 8,
        "done": 6,
        "on_time": 5,
        "in_progress": 1,
        "review": 1,
        "failed": 1,
        "issues": 2,
        "steps_total": 96,
        "steps_done": 70,
        "skipped": 0
    },
    "locations": [
        {
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "timezone": "Asia/Almaty",
            "summary": {
                "total": 6,
                "due": 4,
                "done": 3,
                "on_time": 3,
                "in_progress": 1,
                "review": 0,
                "failed": 0,
                "issues": 1,
                "steps_total": 48,
                "steps_done": 35,
                "skipped": 0
            },
            "runs": [
                {
                    "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
                    "routine": {
                        "id": 12,
                        "title": "Opening the shift"
                    },
                    "category": "opening",
                    "location": {
                        "id": 4609,
                        "name": "Abay Ave"
                    },
                    "day": "2026-11-03",
                    "trigger_type": "schedule",
                    "label": "Morning",
                    "opens_at": "2026-11-03T08:00:00+05:00",
                    "due_at": "2026-11-03T09:00:00+05:00",
                    "status": "in_progress",
                    "late": false,
                    "progress": {
                        "done": 5,
                        "total": 8,
                        "issues": 1
                    },
                    "started_at": "2026-11-03T08:04:00+05:00",
                    "started_by": {
                        "id": 311,
                        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                        "name": "Aigerim Sadykova"
                    },
                    "completed_at": null,
                    "completed_by": null,
                    "reviewed_at": null,
                    "reviewed_by": null,
                    "review_note": null,
                    "skip_reason": null,
                    "parent_run_uuid": null,
                    "updated_at": "2026-11-03T08:31:00+05:00",
                    "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d"
                }
            ]
        }
    ],
    "startable": [
        {
            "routine_id": 15,
            "title": "Delivery acceptance",
            "category": "safety",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "manual": true
        }
    ]
}
```

### GET /routines/my — List my routine runs

Runs the acting user performs (permission `routines.fill` and the job position of the routine/section): today, yesterday's still workable and night windows opening within 2 hours; plus routines the user may start manually.

MCP tool: `routines_my` · Permission: `routines.fill`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | query | integer | no | Only this location |

Example response `data`:

```json
{
    "runs": [
        {
            "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
            "routine": {
                "id": 12,
                "title": "Opening the shift"
            },
            "category": "opening",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "day": "2026-11-03",
            "trigger_type": "schedule",
            "label": "Morning",
            "opens_at": "2026-11-03T08:00:00+05:00",
            "due_at": "2026-11-03T09:00:00+05:00",
            "status": "in_progress",
            "late": false,
            "progress": {
                "done": 5,
                "total": 8,
                "issues": 1
            },
            "started_at": "2026-11-03T08:04:00+05:00",
            "started_by": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "completed_at": null,
            "completed_by": null,
            "reviewed_at": null,
            "reviewed_by": null,
            "review_note": null,
            "skip_reason": null,
            "parent_run_uuid": null,
            "updated_at": "2026-11-03T08:31:00+05:00",
            "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d"
        }
    ],
    "startable": [
        {
            "routine_id": 15,
            "title": "Delivery acceptance",
            "category": "safety",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "manual": true
        }
    ]
}
```

### POST /routines/{id}/start — Start a routine run

Starts a run now at a location. Performers may start routines with manual start allowed; `routines.control` at the location may start any routine out of schedule. Performers are notified.

MCP tool: `routines_runs_start` · Permission: `routines.fill` or `routines.control`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Routine id |
| `location_id` | body | integer | yes | Location id |

Example response `data`:

```json
{
    "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "routine": {
        "id": 12,
        "title": "Opening the shift"
    },
    "category": "opening",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "day": "2026-11-03",
    "trigger_type": "schedule",
    "label": "Morning",
    "opens_at": "2026-11-03T08:00:00+05:00",
    "due_at": "2026-11-03T09:00:00+05:00",
    "status": "in_progress",
    "late": false,
    "progress": {
        "done": 5,
        "total": 8,
        "issues": 1
    },
    "started_at": "2026-11-03T08:04:00+05:00",
    "started_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "completed_at": null,
    "completed_by": null,
    "reviewed_at": null,
    "reviewed_by": null,
    "review_note": null,
    "skip_reason": null,
    "parent_run_uuid": null,
    "updated_at": "2026-11-03T08:31:00+05:00",
    "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "description": "Everything that must be done before the first guest arrives.",
    "instruction": {
        "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "title": "Opening standard"
    },
    "options": {
        "sequential": false,
        "allow_na": true,
        "approval": "none",
        "photo_from_camera": true
    },
    "sections": [
        {
            "section_id": 31,
            "title": "Restrooms",
            "item": 0,
            "item_name": "Men",
            "can_fill": true,
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "conditional": false,
                    "instruction": null,
                    "mark": {
                        "status": "issue",
                        "value": "none",
                        "display": "Out of soap",
                        "flag": null,
                        "reason": null,
                        "comment": "Ordered from the warehouse",
                        "photos": [
                            {
                                "path": "routines/761/photos/2026/11/7f1e.jpg",
                                "url": "https://storage.example.com/routines/761/photos/2026/11/7f1e.jpg?X-Amz-Signature=…"
                            }
                        ],
                        "user": {
                            "id": 311,
                            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                            "name": "Aigerim Sadykova"
                        },
                        "marked_at": "2026-11-03T08:12:00+05:00",
                        "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
                        "spawned_run_uuid": null
                    }
                }
            ]
        }
    ],
    "missing_required": [
        "Restrooms (Women): Liquid soap"
    ],
    "children": [
        {
            "uuid": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
            "title": "Restock consumables",
            "status": "pending",
            "trigger_type": "on_issue"
        }
    ],
    "can": {
        "fill": true,
        "complete": true,
        "accept": false,
        "return": false,
        "skip": true
    }
}
```

### POST /routines/runs/{uuid}/marks — Mark routine steps

Records step marks in a batch (offline-friendly, like the mobile app). Each mark: `step_id`, `item` (repeat object index, default 0), `status` (`done` default, `issue` — cannot do, reason required; `na`), `value` (string or number — measurement, choice key, text, scanned code), `reason`, `comment`, `photos` (paths from `routines.media.upload`), `marked_at` (device time), `client_uuid` — repeating the same `client_uuid` does not change the mark. Issues create tasks and chained runs as configured.

MCP tool: `routines_runs_mark` · Permission: `routines.fill` or `routines.control`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Run UUID |
| `marks` | body | array<object> | yes |  |

Example response `data`:

```json
{
    "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "routine": {
        "id": 12,
        "title": "Opening the shift"
    },
    "category": "opening",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "day": "2026-11-03",
    "trigger_type": "schedule",
    "label": "Morning",
    "opens_at": "2026-11-03T08:00:00+05:00",
    "due_at": "2026-11-03T09:00:00+05:00",
    "status": "in_progress",
    "late": false,
    "progress": {
        "done": 5,
        "total": 8,
        "issues": 1
    },
    "started_at": "2026-11-03T08:04:00+05:00",
    "started_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "completed_at": null,
    "completed_by": null,
    "reviewed_at": null,
    "reviewed_by": null,
    "review_note": null,
    "skip_reason": null,
    "parent_run_uuid": null,
    "updated_at": "2026-11-03T08:31:00+05:00",
    "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "description": "Everything that must be done before the first guest arrives.",
    "instruction": {
        "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "title": "Opening standard"
    },
    "options": {
        "sequential": false,
        "allow_na": true,
        "approval": "none",
        "photo_from_camera": true
    },
    "sections": [
        {
            "section_id": 31,
            "title": "Restrooms",
            "item": 0,
            "item_name": "Men",
            "can_fill": true,
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "conditional": false,
                    "instruction": null,
                    "mark": {
                        "status": "issue",
                        "value": "none",
                        "display": "Out of soap",
                        "flag": null,
                        "reason": null,
                        "comment": "Ordered from the warehouse",
                        "photos": [
                            {
                                "path": "routines/761/photos/2026/11/7f1e.jpg",
                                "url": "https://storage.example.com/routines/761/photos/2026/11/7f1e.jpg?X-Amz-Signature=…"
                            }
                        ],
                        "user": {
                            "id": 311,
                            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                            "name": "Aigerim Sadykova"
                        },
                        "marked_at": "2026-11-03T08:12:00+05:00",
                        "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
                        "spawned_run_uuid": null
                    }
                }
            ]
        }
    ],
    "missing_required": [
        "Restrooms (Women): Liquid soap"
    ],
    "children": [
        {
            "uuid": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
            "title": "Restock consumables",
            "status": "pending",
            "trigger_type": "on_issue"
        }
    ],
    "can": {
        "fill": true,
        "complete": true,
        "accept": false,
        "return": false,
        "skip": true
    }
}
```

### POST /routines/runs/{uuid}/complete — Complete a routine run

Completes the run when all visible required steps are marked. With manager approval enabled, the run goes to `review`. Optional `marks` are recorded first in the same call.

MCP tool: `routines_runs_complete` · Permission: `routines.fill` or `routines.control`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Run UUID |
| `marks` | body | array<object> | no | Last marks, same format as `routines.runs.mark` |

Example response `data`:

```json
{
    "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "routine": {
        "id": 12,
        "title": "Opening the shift"
    },
    "category": "opening",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "day": "2026-11-03",
    "trigger_type": "schedule",
    "label": "Morning",
    "opens_at": "2026-11-03T08:00:00+05:00",
    "due_at": "2026-11-03T09:00:00+05:00",
    "status": "in_progress",
    "late": false,
    "progress": {
        "done": 5,
        "total": 8,
        "issues": 1
    },
    "started_at": "2026-11-03T08:04:00+05:00",
    "started_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "completed_at": null,
    "completed_by": null,
    "reviewed_at": null,
    "reviewed_by": null,
    "review_note": null,
    "skip_reason": null,
    "parent_run_uuid": null,
    "updated_at": "2026-11-03T08:31:00+05:00",
    "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "description": "Everything that must be done before the first guest arrives.",
    "instruction": {
        "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "title": "Opening standard"
    },
    "options": {
        "sequential": false,
        "allow_na": true,
        "approval": "none",
        "photo_from_camera": true
    },
    "sections": [
        {
            "section_id": 31,
            "title": "Restrooms",
            "item": 0,
            "item_name": "Men",
            "can_fill": true,
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "conditional": false,
                    "instruction": null,
                    "mark": {
                        "status": "issue",
                        "value": "none",
                        "display": "Out of soap",
                        "flag": null,
                        "reason": null,
                        "comment": "Ordered from the warehouse",
                        "photos": [
                            {
                                "path": "routines/761/photos/2026/11/7f1e.jpg",
                                "url": "https://storage.example.com/routines/761/photos/2026/11/7f1e.jpg?X-Amz-Signature=…"
                            }
                        ],
                        "user": {
                            "id": 311,
                            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                            "name": "Aigerim Sadykova"
                        },
                        "marked_at": "2026-11-03T08:12:00+05:00",
                        "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
                        "spawned_run_uuid": null
                    }
                }
            ]
        }
    ],
    "missing_required": [
        "Restrooms (Women): Liquid soap"
    ],
    "children": [
        {
            "uuid": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
            "title": "Restock consumables",
            "status": "pending",
            "trigger_type": "on_issue"
        }
    ],
    "can": {
        "fill": true,
        "complete": true,
        "accept": false,
        "return": false,
        "skip": true
    }
}
```

### POST /routines/runs/{uuid}/accept — Accept a routine run

Manager approval of a run in `review` (`routines.control` at the location). Chained routines start.

MCP tool: `routines_runs_accept` · Permission: `routines.control`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Run UUID |

Example response `data`:

```json
{
    "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "routine": {
        "id": 12,
        "title": "Opening the shift"
    },
    "category": "opening",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "day": "2026-11-03",
    "trigger_type": "schedule",
    "label": "Morning",
    "opens_at": "2026-11-03T08:00:00+05:00",
    "due_at": "2026-11-03T09:00:00+05:00",
    "status": "in_progress",
    "late": false,
    "progress": {
        "done": 5,
        "total": 8,
        "issues": 1
    },
    "started_at": "2026-11-03T08:04:00+05:00",
    "started_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "completed_at": null,
    "completed_by": null,
    "reviewed_at": null,
    "reviewed_by": null,
    "review_note": null,
    "skip_reason": null,
    "parent_run_uuid": null,
    "updated_at": "2026-11-03T08:31:00+05:00",
    "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "description": "Everything that must be done before the first guest arrives.",
    "instruction": {
        "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "title": "Opening standard"
    },
    "options": {
        "sequential": false,
        "allow_na": true,
        "approval": "none",
        "photo_from_camera": true
    },
    "sections": [
        {
            "section_id": 31,
            "title": "Restrooms",
            "item": 0,
            "item_name": "Men",
            "can_fill": true,
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "conditional": false,
                    "instruction": null,
                    "mark": {
                        "status": "issue",
                        "value": "none",
                        "display": "Out of soap",
                        "flag": null,
                        "reason": null,
                        "comment": "Ordered from the warehouse",
                        "photos": [
                            {
                                "path": "routines/761/photos/2026/11/7f1e.jpg",
                                "url": "https://storage.example.com/routines/761/photos/2026/11/7f1e.jpg?X-Amz-Signature=…"
                            }
                        ],
                        "user": {
                            "id": 311,
                            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                            "name": "Aigerim Sadykova"
                        },
                        "marked_at": "2026-11-03T08:12:00+05:00",
                        "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
                        "spawned_run_uuid": null
                    }
                }
            ]
        }
    ],
    "missing_required": [
        "Restrooms (Women): Liquid soap"
    ],
    "children": [
        {
            "uuid": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
            "title": "Restock consumables",
            "status": "pending",
            "trigger_type": "on_issue"
        }
    ],
    "can": {
        "fill": true,
        "complete": true,
        "accept": false,
        "return": false,
        "skip": true
    }
}
```

### POST /routines/runs/{uuid}/return — Return a routine run for rework

Returns a completed run to `in_progress` with a note; the performer is notified.

MCP tool: `routines_runs_return` · Permission: `routines.control`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Run UUID |
| `note` | body | string | yes |  |

Example response `data`:

```json
{
    "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "routine": {
        "id": 12,
        "title": "Opening the shift"
    },
    "category": "opening",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "day": "2026-11-03",
    "trigger_type": "schedule",
    "label": "Morning",
    "opens_at": "2026-11-03T08:00:00+05:00",
    "due_at": "2026-11-03T09:00:00+05:00",
    "status": "in_progress",
    "late": false,
    "progress": {
        "done": 5,
        "total": 8,
        "issues": 1
    },
    "started_at": "2026-11-03T08:04:00+05:00",
    "started_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "completed_at": null,
    "completed_by": null,
    "reviewed_at": null,
    "reviewed_by": null,
    "review_note": null,
    "skip_reason": null,
    "parent_run_uuid": null,
    "updated_at": "2026-11-03T08:31:00+05:00",
    "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "description": "Everything that must be done before the first guest arrives.",
    "instruction": {
        "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "title": "Opening standard"
    },
    "options": {
        "sequential": false,
        "allow_na": true,
        "approval": "none",
        "photo_from_camera": true
    },
    "sections": [
        {
            "section_id": 31,
            "title": "Restrooms",
            "item": 0,
            "item_name": "Men",
            "can_fill": true,
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "conditional": false,
                    "instruction": null,
                    "mark": {
                        "status": "issue",
                        "value": "none",
                        "display": "Out of soap",
                        "flag": null,
                        "reason": null,
                        "comment": "Ordered from the warehouse",
                        "photos": [
                            {
                                "path": "routines/761/photos/2026/11/7f1e.jpg",
                                "url": "https://storage.example.com/routines/761/photos/2026/11/7f1e.jpg?X-Amz-Signature=…"
                            }
                        ],
                        "user": {
                            "id": 311,
                            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                            "name": "Aigerim Sadykova"
                        },
                        "marked_at": "2026-11-03T08:12:00+05:00",
                        "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
                        "spawned_run_uuid": null
                    }
                }
            ]
        }
    ],
    "missing_required": [
        "Restrooms (Women): Liquid soap"
    ],
    "children": [
        {
            "uuid": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
            "title": "Restock consumables",
            "status": "pending",
            "trigger_type": "on_issue"
        }
    ],
    "can": {
        "fill": true,
        "complete": true,
        "accept": false,
        "return": false,
        "skip": true
    }
}
```

### POST /routines/runs/{uuid}/skip — Skip a routine run

Cancels a run with a reason (location closed, sanitary day). Skipped runs are not counted in analytics.

MCP tool: `routines_runs_skip` · Permission: `routines.control`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Run UUID |
| `reason` | body | string | yes |  |

Example response `data`:

```json
{
    "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "routine": {
        "id": 12,
        "title": "Opening the shift"
    },
    "category": "opening",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "day": "2026-11-03",
    "trigger_type": "schedule",
    "label": "Morning",
    "opens_at": "2026-11-03T08:00:00+05:00",
    "due_at": "2026-11-03T09:00:00+05:00",
    "status": "in_progress",
    "late": false,
    "progress": {
        "done": 5,
        "total": 8,
        "issues": 1
    },
    "started_at": "2026-11-03T08:04:00+05:00",
    "started_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "completed_at": null,
    "completed_by": null,
    "reviewed_at": null,
    "reviewed_by": null,
    "review_note": null,
    "skip_reason": null,
    "parent_run_uuid": null,
    "updated_at": "2026-11-03T08:31:00+05:00",
    "url": "https://app.jecheck.io/ui/routines?run=9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
    "description": "Everything that must be done before the first guest arrives.",
    "instruction": {
        "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "title": "Opening standard"
    },
    "options": {
        "sequential": false,
        "allow_na": true,
        "approval": "none",
        "photo_from_camera": true
    },
    "sections": [
        {
            "section_id": 31,
            "title": "Restrooms",
            "item": 0,
            "item_name": "Men",
            "can_fill": true,
            "steps": [
                {
                    "id": 210,
                    "title": "Liquid soap",
                    "hint": "Refill if less than a third is left",
                    "type": "choice",
                    "settings": {
                        "options": [
                            {
                                "key": "ok",
                                "label": "Enough",
                                "issue": false
                            },
                            {
                                "key": "none",
                                "label": "Out of soap",
                                "issue": true
                            }
                        ]
                    },
                    "photo": "on_issue",
                    "required": true,
                    "critical": false,
                    "conditional": false,
                    "instruction": null,
                    "mark": {
                        "status": "issue",
                        "value": "none",
                        "display": "Out of soap",
                        "flag": null,
                        "reason": null,
                        "comment": "Ordered from the warehouse",
                        "photos": [
                            {
                                "path": "routines/761/photos/2026/11/7f1e.jpg",
                                "url": "https://storage.example.com/routines/761/photos/2026/11/7f1e.jpg?X-Amz-Signature=…"
                            }
                        ],
                        "user": {
                            "id": 311,
                            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                            "name": "Aigerim Sadykova"
                        },
                        "marked_at": "2026-11-03T08:12:00+05:00",
                        "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
                        "spawned_run_uuid": null
                    }
                }
            ]
        }
    ],
    "missing_required": [
        "Restrooms (Women): Liquid soap"
    ],
    "children": [
        {
            "uuid": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
            "title": "Restock consumables",
            "status": "pending",
            "trigger_type": "on_issue"
        }
    ],
    "can": {
        "fill": true,
        "complete": true,
        "accept": false,
        "return": false,
        "skip": true
    }
}
```

### POST /routines/media — Upload a routine photo or signature

Uploads an image (jpg, png, webp, heic, up to 15 MB) and returns its `path` to pass in `photos` of a step mark (or as `value` of a signature step).

MCP tool: `routines_media_upload` · Permission: `routines.fill`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `file` | body | file | yes | Image file (multipart; in MCP — object with `name` and `content_base64`) |
| `kind` | body | string (photo, signature) | no |  |

Example response `data`:

```json
{
    "path": "routines/761/photos/2026/11/7f1e.jpg",
    "url": "https://storage.example.com/routines/761/photos/2026/11/7f1e.jpg?X-Amz-Signature=…"
}
```

### GET /routines/issues — List routine step issues

Step marks with problems ("cannot do", out-of-range values, wrong code) at visible locations, newest first, with the follow-up task if one was created.

MCP tool: `routines_issues` · Permission: `routines.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `location_ids` | query | array<integer> | no | Only these locations |
| `date` | query | string | no | Only this local day |
| `date_from` | query | string | no | Local day from (inclusive) |
| `date_to` | query | string | no | Local day to (inclusive) |
| `routine_id` | query | integer | no | Only runs of this routine |
| `critical` | query | boolean | no | Only critical steps |

Example response `data`:

```json
[
    {
        "run_uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
        "routine": {
            "id": 12,
            "title": "Opening the shift"
        },
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "day": "2026-11-03",
        "step": {
            "id": 210,
            "title": "Liquid soap",
            "critical": false
        },
        "item": 0,
        "value": "none",
        "reason": null,
        "comment": "Ordered from the warehouse",
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "marked_at": "2026-11-03T08:12:00+05:00",
        "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
    }
]
```

### GET /routines/analytics — Get routine analytics

Discipline metrics for runs whose due time has passed (skipped runs excluded): totals vs previous period, trend, by location (with the average audit score), by routine, by employee, problem steps, start hours, recent failures and insights. Locations follow `analytics.view`.

MCP tool: `routines_analytics` · Permission: `analytics.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `period` | query | string (7d, 30d, 90d, 12m) | no |  |
| `date_from` | query | string | no | Custom period start (with `date_to`) |
| `date_to` | query | string | no | Custom period end |
| `location_ids` | query | array<integer> | no | Only these locations |
| `routine_id` | query | integer | no | Only this routine |

Example response `data`:

```json
{
    "period": {
        "from": "2026-10-05",
        "to": "2026-11-03"
    },
    "totals": {
        "due": 420,
        "on_time": 371,
        "late": 18,
        "failed": 31,
        "missed": 12,
        "review": 4,
        "issues": 57,
        "rate": 88,
        "done_rate": 93,
        "steps_rate": 95
    },
    "previous": {
        "due": 398,
        "on_time": 330,
        "late": 25,
        "failed": 43,
        "missed": 20,
        "review": 2,
        "issues": 61,
        "rate": 83,
        "done_rate": 89,
        "steps_rate": 92
    },
    "trend": [
        {
            "bucket": "2026-11-03",
            "due": 14,
            "on_time": 12,
            "late": 1,
            "failed": 1,
            "missed": 0,
            "review": 0,
            "issues": 2,
            "rate": 86,
            "done_rate": 93,
            "steps_rate": 96
        }
    ],
    "by_location": [
        {
            "id": 4609,
            "name": "Abay Ave",
            "due": 140,
            "on_time": 130,
            "late": 4,
            "failed": 6,
            "missed": 2,
            "review": 0,
            "issues": 12,
            "rate": 93,
            "done_rate": 96,
            "steps_rate": 97,
            "audit": {
                "avg": 64,
                "count": 5
            }
        }
    ],
    "by_routine": [
        {
            "id": 12,
            "name": "Opening the shift",
            "due": 60,
            "on_time": 55,
            "late": 3,
            "failed": 2,
            "missed": 1,
            "review": 0,
            "issues": 5,
            "rate": 92,
            "done_rate": 97,
            "steps_rate": 98
        }
    ],
    "by_employee": [
        {
            "id": 311,
            "name": "Aigerim Sadykova",
            "runs": 40,
            "on_time": 37,
            "rate": 93,
            "marks": 380,
            "issues": 6,
            "photos": 44
        }
    ],
    "problem_steps": [
        {
            "step_id": 210,
            "step": "Liquid soap",
            "routine": "Opening the shift",
            "critical": false,
            "count": 9,
            "points": 3,
            "tasks": 4,
            "reason": "Out of consumables"
        }
    ],
    "hours": [
        {
            "hour": 8,
            "due": 60,
            "rate": 92
        }
    ],
    "failed": [
        {
            "uuid": "9a8b7c6d-5e4f-4a3b-9c2d-1e0f9a8b7c6d",
            "date": "2026-11-02",
            "time": "23:00",
            "routine": "Closing",
            "location": "Abay Ave",
            "status": "missed",
            "progress": 0
        }
    ],
    "insights": [
        {
            "type": "gap",
            "location": "Abay Ave",
            "rate": 93,
            "audit": 64
        }
    ]
}
```

## HACCP

Food-safety journals (HACCP): today's checks per location, journal entries with automatic limit checks, deviations with corrective actions, registries (equipment, suppliers, verification, medical books) and the HACCP plan. Locations follow `haccp.view`; medical books require the sensitive `haccp.medical` permission.

### GET /haccp/sites — List HACCP locations

HACCP status of the company (enabled, country profile, timezone) and the HACCP locations visible to the token.

MCP tool: `haccp_sites` · Permission: `haccp.view`

Example response `data`:

```json
{
    "enabled": true,
    "profile": "kz",
    "timezone": "Asia/Almaty",
    "sites": [
        {
            "id": 4609,
            "name": "Abay Ave",
            "address": "10 Abay Ave",
            "kind": "cafe"
        }
    ]
}
```

### GET /haccp/today — Get today's HACCP checks

What must be recorded today: journals with time slots (status `pending`, `overdue`, `done`, `late`, `missed`, `skipped`), objects with limits, today's entries, staff on shift and open deviations. Without `location_id` — all HACCP locations visible to the token.

MCP tool: `haccp_today` · Permission: `haccp.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | query | integer | no | One location; omitted — all visible locations |

Example response `data`:

```json
{
    "day": "2026-11-03",
    "timezone": "Asia/Almaty",
    "late_minutes": 60,
    "sites": [
        {
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "can_resolve": false,
            "summary": {
                "total": 9,
                "done": 4,
                "pending": 5,
                "overdue": 1,
                "deviations": 1
            },
            "journals": [
                {
                    "id": 14,
                    "uuid": "4c3b2a1f-0e9d-4c8b-9a7f-6e5d4c3b2a1f",
                    "title": "Storage temperature",
                    "category": "monitoring",
                    "subject": "object",
                    "event_based": false,
                    "schedule_text": "Daily at 09:00, 17:00",
                    "fields": [
                        {
                            "key": "temperature",
                            "label": "Temperature",
                            "type": "temperature",
                            "unit": "°C",
                            "required": true,
                            "min": 2,
                            "max": 6
                        }
                    ],
                    "can_fill": true,
                    "objects": [
                        {
                            "id": 301,
                            "name": "Fridge #1",
                            "type": "fridge",
                            "type_label": "Fridge",
                            "limits": {
                                "temperature": {
                                    "min": 2,
                                    "max": 6
                                }
                            }
                        }
                    ],
                    "slots": [
                        {
                            "id": 99120,
                            "object_id": 301,
                            "opens_at": "2026-11-03T08:00:00+05:00",
                            "due_at": "2026-11-03T09:00:00+05:00",
                            "status": "done",
                            "open": true,
                            "entry_id": 88231,
                            "skip_reason": null
                        }
                    ],
                    "entries": [
                        {
                            "id": 88231,
                            "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
                            "object_id": 301,
                            "subject_user": null,
                            "recorded_at": "2026-11-03T09:02:00+05:00",
                            "values": {
                                "temperature": 8.4
                            },
                            "status": "deviation",
                            "late": false
                        }
                    ],
                    "done": 1,
                    "pending": 1,
                    "total": 2
                }
            ],
            "staff": [
                {
                    "id": 311,
                    "name": "Aigerim Sadykova",
                    "on_shift": true
                }
            ],
            "deviations": [
                {
                    "uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
                    "journal": {
                        "id": 14,
                        "title": "Storage temperature"
                    },
                    "object_id": 301,
                    "severity": "major",
                    "status": "open",
                    "due_at": "2026-11-04T09:02:00+05:00",
                    "overdue": false,
                    "recheck": false
                }
            ]
        }
    ]
}
```

### GET /haccp/journals — List HACCP journals

Journals of the company with their field structure and schedule.

MCP tool: `haccp_journals_list` · Permission: `haccp.view` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `active` | query | boolean | no | Only enabled (`true`) or disabled (`false`) journals |
| `location_id` | query | integer | no | Only journals kept at this location |
| `category` | query | string (monitoring, prp, receiving, personnel, cleaning, other) | no |  |

Example response `data`:

```json
[
    {
        "id": 14,
        "uuid": "4c3b2a1f-0e9d-4c8b-9a7f-6e5d4c3b2a1f",
        "title": "Storage temperature",
        "description": "Fridges and freezers, twice per shift",
        "category": "monitoring",
        "icon": "kitchen",
        "color": "#0e9aa7",
        "active": true,
        "subject": "object",
        "event_based": false,
        "schedule": {
            "type": "times",
            "times": [
                "09:00",
                "17:00"
            ]
        },
        "schedule_text": "Daily at 09:00, 17:00",
        "fields": [
            {
                "key": "temperature",
                "label": "Temperature",
                "type": "temperature",
                "unit": "°C",
                "required": true,
                "min": 2,
                "max": 6
            }
        ],
        "object_types": [
            "fridge",
            "freezer"
        ],
        "location_ids": null,
        "position_ids": [],
        "ccp_id": null,
        "requires_photo_on_deviation": false,
        "template_key": "storage_temperature",
        "updated_at": "2026-10-20T10:00:00+05:00"
    }
]
```

### GET /haccp/journals/{id} — Retrieve a HACCP journal

Journal structure: fields (types, units, limits), schedule, subject (`site`, `object`, `employee`), object types, locations and positions, plus objects at the visible locations.

MCP tool: `haccp_journals_get` · Permission: `haccp.view` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Journal id |

Example response `data`:

```json
{
    "id": 14,
    "uuid": "4c3b2a1f-0e9d-4c8b-9a7f-6e5d4c3b2a1f",
    "title": "Storage temperature",
    "description": "Fridges and freezers, twice per shift",
    "category": "monitoring",
    "icon": "kitchen",
    "color": "#0e9aa7",
    "active": true,
    "subject": "object",
    "event_based": false,
    "schedule": {
        "type": "times",
        "times": [
            "09:00",
            "17:00"
        ]
    },
    "schedule_text": "Daily at 09:00, 17:00",
    "fields": [
        {
            "key": "temperature",
            "label": "Temperature",
            "type": "temperature",
            "unit": "°C",
            "required": true,
            "min": 2,
            "max": 6
        }
    ],
    "object_types": [
        "fridge",
        "freezer"
    ],
    "location_ids": null,
    "position_ids": [],
    "ccp_id": null,
    "requires_photo_on_deviation": false,
    "template_key": "storage_temperature",
    "updated_at": "2026-10-20T10:00:00+05:00",
    "objects": [
        {
            "id": 301,
            "name": "Fridge #1",
            "type": "fridge",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "limits": {
                "temperature": {
                    "min": 2,
                    "max": 6
                }
            },
            "active": true
        }
    ]
}
```

### GET /haccp/entries — List HACCP journal entries

Entries at the visible locations, newest first. Archived entries (past the retention period) are not returned; voided entries are included only with `voided=true`.

MCP tool: `haccp_entries_list` · Permission: `haccp.view` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `journal_id` | query | integer | no | Only this journal |
| `location_ids` | query | array<integer> | no | Only these locations |
| `object_id` | query | integer | no | Only this object (fridge, freezer, …) |
| `status` | query | string (ok, warning, deviation) | no |  |
| `late` | query | boolean | no | Only entries recorded late |
| `voided` | query | boolean | no | `true` — only voided entries |
| `recorded_from` | query | string | no | Measurement time from |
| `recorded_to` | query | string | no | Measurement time to |
| `updated_since` | query | string | no | Only entries changed after this moment — use for incremental sync |

Example response `data`:

```json
[
    {
        "id": 88231,
        "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
        "journal_id": 14,
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "object_id": 301,
        "subject_user": null,
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "recorded_at": "2026-11-03T09:02:00+05:00",
        "values": {
            "temperature": 8.4
        },
        "status": "deviation",
        "flags": {
            "breaches": [
                {
                    "key": "temperature",
                    "label": "Temperature",
                    "value": 8.4,
                    "unit": "°C",
                    "min": 2,
                    "max": 6
                }
            ],
            "warnings": []
        },
        "late": false,
        "late_reason": null,
        "comment": "Door was left open",
        "photos": [
            {
                "path": "haccp/761/photos/2026/11/a1b2.jpg",
                "url": "https://storage.example.com/haccp/761/photos/2026/11/a1b2.jpg?X-Amz-Signature=…"
            }
        ],
        "deviation_uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
        "source": "app",
        "revision": 0,
        "voided": false,
        "void_reason": null,
        "updated_at": "2026-11-03T09:02:05+05:00"
    }
]
```

### POST /haccp/journals/{id}/entries — Record a HACCP journal entry

Records a measurement/check. Values are checked against the field and object limits: out of limits — the entry gets status `deviation` and a deviation is opened (notifications as in the app). Idempotent with `uuid`: repeating the same `uuid` returns the existing entry. With `deviation_id` — a re-check after correction.

MCP tool: `haccp_entries_create` · Permission: `haccp.fill`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Journal id |
| `location_id` | body | integer | yes | Location id |
| `values` | body | object | yes | Field values by field `key`, e.g. `{"temperature": 4.2}` |
| `object_id` | body | integer | no | Object (journals with subject `object`) |
| `subject_user_id` | body | integer | no | Employee the entry is about (journals with subject `employee`, e.g. health check) |
| `recorded_at` | body | string | no | Measurement time (device time for offline sync), default — now |
| `uuid` | body | string | no | Client-generated UUID for safe retries |
| `comment` | body | string | no |  |
| `photos` | body | array<string> | no | Paths from `haccp.media.upload` |
| `deviation_id` | body | integer | no | Re-check measurement for this deviation |
| `late_reason` | body | string | no | Why the entry is recorded later than the measurement |
| `latitude` | body | number | no |  |
| `longitude` | body | number | no |  |

Example response `data`:

```json
{
    "id": 88231,
    "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
    "journal_id": 14,
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object_id": 301,
    "subject_user": null,
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "recorded_at": "2026-11-03T09:02:00+05:00",
    "values": {
        "temperature": 8.4
    },
    "status": "deviation",
    "flags": {
        "breaches": [
            {
                "key": "temperature",
                "label": "Temperature",
                "value": 8.4,
                "unit": "°C",
                "min": 2,
                "max": 6
            }
        ],
        "warnings": []
    },
    "late": false,
    "late_reason": null,
    "comment": "Door was left open",
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/a1b2.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/a1b2.jpg?X-Amz-Signature=…"
        }
    ],
    "deviation_uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "source": "app",
    "revision": 0,
    "voided": false,
    "void_reason": null,
    "updated_at": "2026-11-03T09:02:05+05:00"
}
```

### POST /haccp/entries/{id}/revise — Correct a HACCP journal entry

Corrects values of an entry with a reason; previous values stay in the revision history. Allowed to the author or with `haccp.resolve` at the location.

MCP tool: `haccp_entries_revise` · Permission: `haccp.fill` or `haccp.resolve` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Entry id |
| `values` | body | object | yes |  |
| `reason` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 88231,
    "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
    "journal_id": 14,
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object_id": 301,
    "subject_user": null,
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "recorded_at": "2026-11-03T09:02:00+05:00",
    "values": {
        "temperature": 8.4
    },
    "status": "deviation",
    "flags": {
        "breaches": [
            {
                "key": "temperature",
                "label": "Temperature",
                "value": 8.4,
                "unit": "°C",
                "min": 2,
                "max": 6
            }
        ],
        "warnings": []
    },
    "late": false,
    "late_reason": null,
    "comment": "Door was left open",
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/a1b2.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/a1b2.jpg?X-Amz-Signature=…"
        }
    ],
    "deviation_uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "source": "app",
    "revision": 0,
    "voided": false,
    "void_reason": null,
    "updated_at": "2026-11-03T09:02:05+05:00"
}
```

### POST /haccp/entries/{id}/void — Void a HACCP journal entry

Marks an entry as void with a reason (`haccp.resolve` at the location). The entry stays in the journal for the record.

MCP tool: `haccp_entries_void` · Permission: `haccp.resolve` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Entry id |
| `reason` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 88231,
    "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
    "journal_id": 14,
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object_id": 301,
    "subject_user": null,
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "recorded_at": "2026-11-03T09:02:00+05:00",
    "values": {
        "temperature": 8.4
    },
    "status": "deviation",
    "flags": {
        "breaches": [
            {
                "key": "temperature",
                "label": "Temperature",
                "value": 8.4,
                "unit": "°C",
                "min": 2,
                "max": 6
            }
        ],
        "warnings": []
    },
    "late": false,
    "late_reason": null,
    "comment": "Door was left open",
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/a1b2.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/a1b2.jpg?X-Amz-Signature=…"
        }
    ],
    "deviation_uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "source": "app",
    "revision": 0,
    "voided": false,
    "void_reason": null,
    "updated_at": "2026-11-03T09:02:05+05:00"
}
```

### POST /haccp/slots/{id}/skip — Skip a scheduled HACCP check

Marks a time slot as not required today (location closed, equipment off) with a reason. Requires `haccp.resolve` at the location.

MCP tool: `haccp_slots_skip` · Permission: `haccp.resolve` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Slot id from `haccp.today` |
| `reason` | body | string | yes |  |

Example response `data`:

```json
{
    "skipped": true
}
```

### POST /haccp/media — Upload a HACCP photo or document

Uploads an image (jpg, png, webp, heic) or PDF and returns its `path` for `photos` of an entry or deviation, photo fields, staff documents and verification records.

MCP tool: `haccp_media_upload` · Permission: `haccp.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `file` | body | file | yes | File (multipart; in MCP — object with `name` and `content_base64`) |
| `folder` | body | string (photos, docs) | no |  |

Example response `data`:

```json
{
    "path": "haccp/761/photos/2026/11/a1b2.jpg",
    "url": "https://storage.example.com/haccp/761/photos/2026/11/a1b2.jpg?X-Amz-Signature=…"
}
```

### GET /haccp/deviations — List HACCP deviations

Deviations at the visible locations: reviews first, then newest. `status=open` (default) — all unresolved (stages `open`, `correcting`, `review`); `closed`; `all`. Use `stage` for an exact stage.

MCP tool: `haccp_deviations_list` · Permission: `haccp.view` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `status` | query | string (open, closed, all) | no |  |
| `stage` | query | string (open, correcting, review, closed) | no | Exact stage |
| `severity` | query | string (critical, major, minor) | no |  |
| `journal_id` | query | integer | no |  |
| `location_ids` | query | array<integer> | no | Only these locations |
| `overdue` | query | boolean | no | Only unresolved deviations past their due time |
| `date_from` | query | string | no | Opened from (inclusive) |
| `date_to` | query | string | no | Opened to (inclusive) |
| `updated_since` | query | string | no | Only deviations changed after this moment |

Example response `data`:

```json
[
    {
        "id": 5120,
        "uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
        "journal": {
            "id": 14,
            "title": "Storage temperature"
        },
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "object": {
            "id": 301,
            "name": "Fridge #1"
        },
        "severity": "major",
        "status": "correcting",
        "breaches": [
            {
                "key": "temperature",
                "label": "Temperature",
                "value": 8.4,
                "unit": "°C",
                "min": 2,
                "max": 6
            }
        ],
        "responsible": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "due_at": "2026-11-04T09:02:00+05:00",
        "overdue": false,
        "created_at": "2026-11-03T09:02:05+05:00",
        "closed_at": null,
        "updated_at": "2026-11-03T09:40:00+05:00",
        "url": "https://app.jecheck.io/ui/haccp/deviations?open=0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d"
    }
]
```

### GET /haccp/deviations/{uuid} — Retrieve a HACCP deviation

Deviation card: breached limits, the original entry and the re-check, corrective actions, required fields before submitting, the follow-up task and whether the token can work on / verify it.

MCP tool: `haccp_deviations_get` · Permission: `haccp.view` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Deviation UUID |

Example response `data`:

```json
{
    "id": 5120,
    "uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "journal": {
        "id": 14,
        "title": "Storage temperature"
    },
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object": {
        "id": 301,
        "name": "Fridge #1"
    },
    "severity": "major",
    "status": "correcting",
    "breaches": [
        {
            "key": "temperature",
            "label": "Temperature",
            "value": 8.4,
            "unit": "°C",
            "min": 2,
            "max": 6
        }
    ],
    "responsible": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "due_at": "2026-11-04T09:02:00+05:00",
    "overdue": false,
    "created_at": "2026-11-03T09:02:05+05:00",
    "closed_at": null,
    "updated_at": "2026-11-03T09:40:00+05:00",
    "url": "https://app.jecheck.io/ui/haccp/deviations?open=0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "correction": "Products moved to the backup fridge, door seal replaced",
    "product_disposition": "released",
    "product_note": null,
    "root_cause": null,
    "preventive_action": null,
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/c3d4.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/c3d4.jpg?X-Amz-Signature=…"
        }
    ],
    "required": [
        "correction",
        "product_disposition"
    ],
    "entry": {
        "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
        "values": {
            "temperature": 8.4
        },
        "recorded_at": "2026-11-03T09:02:00+05:00",
        "comment": "Door was left open"
    },
    "recheck": {
        "uuid": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b",
        "status": "ok",
        "values": {
            "temperature": 4.1
        },
        "recorded_at": "2026-11-03T11:00:00+05:00"
    },
    "task": {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "title": "Fix the deviation: Storage temperature",
        "status": "in_progress"
    },
    "closed_by": null,
    "verified_by": null,
    "verified_at": null,
    "verify_note": null,
    "can_work": true,
    "can_verify": false
}
```

### PATCH /haccp/deviations/{uuid} — Update corrective actions

Saves corrective actions as a draft (stage moves to `correcting`). Allowed to the responsible person or with `haccp.resolve` at the location; `responsible_user_id` — with `haccp.resolve` only.

MCP tool: `haccp_deviations_update` · Permission: `haccp.view` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Deviation UUID |
| `correction` | body | string | no | What was done to fix it |
| `product_disposition` | body | string (released, reworked, destroyed, returned, quarantine, not_applicable) | no | What happened to the product |
| `product_note` | body | string | no |  |
| `root_cause` | body | string | no | Root cause (required for critical deviations) |
| `preventive_action` | body | string | no | Preventive action (required for critical deviations) |
| `photos` | body | array<string> | no | Paths from `haccp.media.upload` |
| `responsible_user_id` | body | integer | no |  |

Example response `data`:

```json
{
    "id": 5120,
    "uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "journal": {
        "id": 14,
        "title": "Storage temperature"
    },
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object": {
        "id": 301,
        "name": "Fridge #1"
    },
    "severity": "major",
    "status": "correcting",
    "breaches": [
        {
            "key": "temperature",
            "label": "Temperature",
            "value": 8.4,
            "unit": "°C",
            "min": 2,
            "max": 6
        }
    ],
    "responsible": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "due_at": "2026-11-04T09:02:00+05:00",
    "overdue": false,
    "created_at": "2026-11-03T09:02:05+05:00",
    "closed_at": null,
    "updated_at": "2026-11-03T09:40:00+05:00",
    "url": "https://app.jecheck.io/ui/haccp/deviations?open=0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "correction": "Products moved to the backup fridge, door seal replaced",
    "product_disposition": "released",
    "product_note": null,
    "root_cause": null,
    "preventive_action": null,
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/c3d4.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/c3d4.jpg?X-Amz-Signature=…"
        }
    ],
    "required": [
        "correction",
        "product_disposition"
    ],
    "entry": {
        "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
        "values": {
            "temperature": 8.4
        },
        "recorded_at": "2026-11-03T09:02:00+05:00",
        "comment": "Door was left open"
    },
    "recheck": {
        "uuid": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b",
        "status": "ok",
        "values": {
            "temperature": 4.1
        },
        "recorded_at": "2026-11-03T11:00:00+05:00"
    },
    "task": {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "title": "Fix the deviation: Storage temperature",
        "status": "in_progress"
    },
    "closed_by": null,
    "verified_by": null,
    "verified_at": null,
    "verify_note": null,
    "can_work": true,
    "can_verify": false
}
```

### POST /haccp/deviations/{uuid}/submit — Submit corrective actions for review

Sends the deviation to review when the required fields are filled. If the token itself may close it (`haccp.resolve`, or `haccp.close_critical` for critical ones), it is closed right away.

MCP tool: `haccp_deviations_submit` · Permission: `haccp.view` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Deviation UUID |

Example response `data`:

```json
{
    "id": 5120,
    "uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "journal": {
        "id": 14,
        "title": "Storage temperature"
    },
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object": {
        "id": 301,
        "name": "Fridge #1"
    },
    "severity": "major",
    "status": "correcting",
    "breaches": [
        {
            "key": "temperature",
            "label": "Temperature",
            "value": 8.4,
            "unit": "°C",
            "min": 2,
            "max": 6
        }
    ],
    "responsible": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "due_at": "2026-11-04T09:02:00+05:00",
    "overdue": false,
    "created_at": "2026-11-03T09:02:05+05:00",
    "closed_at": null,
    "updated_at": "2026-11-03T09:40:00+05:00",
    "url": "https://app.jecheck.io/ui/haccp/deviations?open=0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "correction": "Products moved to the backup fridge, door seal replaced",
    "product_disposition": "released",
    "product_note": null,
    "root_cause": null,
    "preventive_action": null,
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/c3d4.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/c3d4.jpg?X-Amz-Signature=…"
        }
    ],
    "required": [
        "correction",
        "product_disposition"
    ],
    "entry": {
        "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
        "values": {
            "temperature": 8.4
        },
        "recorded_at": "2026-11-03T09:02:00+05:00",
        "comment": "Door was left open"
    },
    "recheck": {
        "uuid": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b",
        "status": "ok",
        "values": {
            "temperature": 4.1
        },
        "recorded_at": "2026-11-03T11:00:00+05:00"
    },
    "task": {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "title": "Fix the deviation: Storage temperature",
        "status": "in_progress"
    },
    "closed_by": null,
    "verified_by": null,
    "verified_at": null,
    "verify_note": null,
    "can_work": true,
    "can_verify": false
}
```

### POST /haccp/deviations/{uuid}/close — Close a HACCP deviation

Verifies corrective actions and closes the deviation. Requires `haccp.resolve` at the location; critical deviations — `haccp.close_critical`.

MCP tool: `haccp_deviations_close` · Permission: `haccp.resolve` or `haccp.close_critical` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Deviation UUID |
| `note` | body | string | no | Verification note |

Example response `data`:

```json
{
    "id": 5120,
    "uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "journal": {
        "id": 14,
        "title": "Storage temperature"
    },
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object": {
        "id": 301,
        "name": "Fridge #1"
    },
    "severity": "major",
    "status": "correcting",
    "breaches": [
        {
            "key": "temperature",
            "label": "Temperature",
            "value": 8.4,
            "unit": "°C",
            "min": 2,
            "max": 6
        }
    ],
    "responsible": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "due_at": "2026-11-04T09:02:00+05:00",
    "overdue": false,
    "created_at": "2026-11-03T09:02:05+05:00",
    "closed_at": null,
    "updated_at": "2026-11-03T09:40:00+05:00",
    "url": "https://app.jecheck.io/ui/haccp/deviations?open=0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "correction": "Products moved to the backup fridge, door seal replaced",
    "product_disposition": "released",
    "product_note": null,
    "root_cause": null,
    "preventive_action": null,
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/c3d4.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/c3d4.jpg?X-Amz-Signature=…"
        }
    ],
    "required": [
        "correction",
        "product_disposition"
    ],
    "entry": {
        "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
        "values": {
            "temperature": 8.4
        },
        "recorded_at": "2026-11-03T09:02:00+05:00",
        "comment": "Door was left open"
    },
    "recheck": {
        "uuid": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b",
        "status": "ok",
        "values": {
            "temperature": 4.1
        },
        "recorded_at": "2026-11-03T11:00:00+05:00"
    },
    "task": {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "title": "Fix the deviation: Storage temperature",
        "status": "in_progress"
    },
    "closed_by": null,
    "verified_by": null,
    "verified_at": null,
    "verify_note": null,
    "can_work": true,
    "can_verify": false
}
```

### POST /haccp/deviations/{uuid}/reject — Return a HACCP deviation for rework

Returns a deviation in `review` back to correction with a note. Same permissions as closing.

MCP tool: `haccp_deviations_reject` · Permission: `haccp.resolve` or `haccp.close_critical` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Deviation UUID |
| `note` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 5120,
    "uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "journal": {
        "id": 14,
        "title": "Storage temperature"
    },
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object": {
        "id": 301,
        "name": "Fridge #1"
    },
    "severity": "major",
    "status": "correcting",
    "breaches": [
        {
            "key": "temperature",
            "label": "Temperature",
            "value": 8.4,
            "unit": "°C",
            "min": 2,
            "max": 6
        }
    ],
    "responsible": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "due_at": "2026-11-04T09:02:00+05:00",
    "overdue": false,
    "created_at": "2026-11-03T09:02:05+05:00",
    "closed_at": null,
    "updated_at": "2026-11-03T09:40:00+05:00",
    "url": "https://app.jecheck.io/ui/haccp/deviations?open=0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "correction": "Products moved to the backup fridge, door seal replaced",
    "product_disposition": "released",
    "product_note": null,
    "root_cause": null,
    "preventive_action": null,
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/c3d4.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/c3d4.jpg?X-Amz-Signature=…"
        }
    ],
    "required": [
        "correction",
        "product_disposition"
    ],
    "entry": {
        "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
        "values": {
            "temperature": 8.4
        },
        "recorded_at": "2026-11-03T09:02:00+05:00",
        "comment": "Door was left open"
    },
    "recheck": {
        "uuid": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b",
        "status": "ok",
        "values": {
            "temperature": 4.1
        },
        "recorded_at": "2026-11-03T11:00:00+05:00"
    },
    "task": {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "title": "Fix the deviation: Storage temperature",
        "status": "in_progress"
    },
    "closed_by": null,
    "verified_by": null,
    "verified_at": null,
    "verify_note": null,
    "can_work": true,
    "can_verify": false
}
```

### POST /haccp/deviations/{uuid}/task — Create a task for a HACCP deviation

Creates a task when the fix needs work (repair, deep cleaning). Assign to a user (`assignee_id`) or a team queue (`team_id`). Requires `haccp.resolve` at the location.

MCP tool: `haccp_deviations_task` · Permission: `haccp.resolve` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Deviation UUID |
| `assignee_id` | body | integer | no | Executor (user id) |
| `team_id` | body | integer | no | Team id instead of a user |
| `title` | body | string | no | Default: "Fix the deviation: <journal>" |
| `description` | body | string | no |  |
| `due_at` | body | string | no | Default: +4 h for critical, +24 h otherwise |

Example response `data`:

```json
{
    "id": 5120,
    "uuid": "0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "journal": {
        "id": 14,
        "title": "Storage temperature"
    },
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "object": {
        "id": 301,
        "name": "Fridge #1"
    },
    "severity": "major",
    "status": "correcting",
    "breaches": [
        {
            "key": "temperature",
            "label": "Temperature",
            "value": 8.4,
            "unit": "°C",
            "min": 2,
            "max": 6
        }
    ],
    "responsible": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "due_at": "2026-11-04T09:02:00+05:00",
    "overdue": false,
    "created_at": "2026-11-03T09:02:05+05:00",
    "closed_at": null,
    "updated_at": "2026-11-03T09:40:00+05:00",
    "url": "https://app.jecheck.io/ui/haccp/deviations?open=0a9b8c7d-6e5f-4a3b-8c2d-1e0f9a8b7c6d",
    "correction": "Products moved to the backup fridge, door seal replaced",
    "product_disposition": "released",
    "product_note": null,
    "root_cause": null,
    "preventive_action": null,
    "photos": [
        {
            "path": "haccp/761/photos/2026/11/c3d4.jpg",
            "url": "https://storage.example.com/haccp/761/photos/2026/11/c3d4.jpg?X-Amz-Signature=…"
        }
    ],
    "required": [
        "correction",
        "product_disposition"
    ],
    "entry": {
        "uuid": "6d5c4b3a-2f1e-4d0c-9b8a-7f6e5d4c3b2a",
        "values": {
            "temperature": 8.4
        },
        "recorded_at": "2026-11-03T09:02:00+05:00",
        "comment": "Door was left open"
    },
    "recheck": {
        "uuid": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b",
        "status": "ok",
        "values": {
            "temperature": 4.1
        },
        "recorded_at": "2026-11-03T11:00:00+05:00"
    },
    "task": {
        "uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "title": "Fix the deviation: Storage temperature",
        "status": "in_progress"
    },
    "closed_by": null,
    "verified_by": null,
    "verified_at": null,
    "verify_note": null,
    "can_work": true,
    "can_verify": false
}
```

### GET /haccp/plans — List HACCP plans

HACCP plans with status, version, review date and completion of the 12 Codex steps.

MCP tool: `haccp_plans_list` · Permission: `haccp.plan` · Plan feature: `haccp`

Example response `data`:

```json
[
    {
        "id": 3,
        "uuid": "8b7a6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
        "title": "Hot kitchen",
        "status": "approved",
        "version": 2,
        "review_due_at": "2027-05-01",
        "location_ids": [
            4609
        ],
        "progress": {
            "1": true,
            "2": true,
            "3": true,
            "4": true,
            "5": true,
            "6": true,
            "7": true,
            "8": true,
            "9": true,
            "10": true,
            "11": false,
            "12": true
        },
        "problems": 1,
        "has_changes": false,
        "counts": {
            "steps": 9,
            "hazards": 14,
            "ccps": 2
        },
        "updated_at": "2026-10-28T15:00:00+05:00"
    }
]
```

### GET /haccp/plans/{id} — Retrieve a HACCP plan

Full plan by the 12 Codex steps: team, products, process flow steps, hazard analysis, critical control points with limits, monitoring and corrective actions; open problems per step.

MCP tool: `haccp_plans_get` · Permission: `haccp.plan` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Plan id |

Example response `data`:

```json
{
    "id": 3,
    "uuid": "8b7a6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
    "title": "Hot kitchen",
    "scope": "Hot dishes of the main menu",
    "location_ids": [
        4609
    ],
    "status": "approved",
    "intended_use": "Served hot within 2 hours",
    "consumer_groups": [
        "general"
    ],
    "prerequisites": [],
    "flow_confirmed_at": "2026-10-10",
    "flow_confirmed_by": 120,
    "documentation": {
        "records": []
    },
    "validation": null,
    "risk_threshold": 6,
    "version": 2,
    "review_due_at": "2027-05-01",
    "updated_at": "2026-10-28T15:00:00+05:00",
    "team": [
        {
            "id": 1,
            "user_id": 120,
            "name": "Daniyar Omarov",
            "role": "leader",
            "job_title": "Chef",
            "competence": "HACCP course 2025",
            "responsibilities": "Plan owner"
        }
    ],
    "products": [
        {
            "id": 4,
            "name": "Chicken soup",
            "group": "hot",
            "composition": "Chicken, vegetables",
            "characteristics": null,
            "processing": "Boiling",
            "packaging": null,
            "storage": "+2…+6 °C",
            "shelf_life": "24 h",
            "distribution": null,
            "allergens": [
                "celery"
            ],
            "labeling": null,
            "intended_use": null
        }
    ],
    "steps": [
        {
            "id": 21,
            "name": "Cooking",
            "type": "process",
            "description": null,
            "parameters": "≥ 75 °C in the core",
            "inputs": null,
            "outsourced": false,
            "rework": false,
            "analyzed": true,
            "next": [
                22
            ],
            "products": [
                4
            ],
            "position": 3
        }
    ],
    "hazards": [
        {
            "id": 51,
            "step_id": 21,
            "type": "biological",
            "name": "Salmonella survival",
            "source": "Raw chicken",
            "likelihood": 3,
            "severity": 3,
            "significant": true,
            "justification": null,
            "control_measures": "Core temperature control",
            "decision": {
                "q1": true
            },
            "outcome": "ccp"
        }
    ],
    "ccps": [
        {
            "id": 7,
            "step_id": 21,
            "hazard_id": 51,
            "kind": "ccp",
            "code": "CCP-1",
            "name": "Core temperature",
            "limits": [
                {
                    "field": "temperature",
                    "min": 75
                }
            ],
            "limits_justification": null,
            "monitoring_what": "Core temperature",
            "monitoring_how": "Probe thermometer",
            "monitoring_frequency": "Each batch",
            "monitoring_positions": [],
            "journal_id": 15,
            "corrective_actions": "Continue cooking",
            "corrective_positions": [],
            "verification": "Weekly journal review",
            "verification_frequency": "weekly",
            "validation": null,
            "records": null
        }
    ],
    "problems": [
        {
            "step": 11,
            "message": "Describe verification procedures"
        }
    ]
}
```

### GET /haccp/equipment — List HACCP equipment

Controlled objects (fridges, freezers, thermometers, …) at the visible locations with limits, calibration dates and the last measurement.

MCP tool: `haccp_equipment_list` · Permission: `haccp.view` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | query | integer | no | Only this location |
| `type` | query | string | no | Object type key, e.g. `fridge` |

Example response `data`:

```json
[
    {
        "id": 301,
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "type": "fridge",
        "type_label": "Fridge",
        "name": "Fridge #1",
        "serial": "LG-2231",
        "limits": {
            "temperature": {
                "min": 2,
                "max": 6
            }
        },
        "calibrated_at": null,
        "calibration_due_at": null,
        "calibration_overdue": false,
        "note": null,
        "active": true,
        "last": {
            "values": {
                "temperature": 4.1
            },
            "status": "ok",
            "recorded_at": "2026-11-03T11:00:00+05:00"
        }
    }
]
```

### POST /haccp/equipment — Add HACCP equipment

Adds a controlled object at a HACCP location. Without `limits` the country profile defaults for the type are used. Requires `haccp.registries`.

MCP tool: `haccp_equipment_create` · Permission: `haccp.registries` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | body | integer | yes | HACCP location id |
| `type` | body | string | no | Object type key of the country profile (`fridge`, `freezer`, …); unknown — `other` |
| `name` | body | string | yes |  |
| `serial` | body | string | no |  |
| `limits` | body | object | no | `{"temperature": {"min": 2, "max": 6}, "humidity": {"min": 40, "max": 70}}` |
| `calibrated_at` | body | string | no |  |
| `calibration_due_at` | body | string | no |  |
| `note` | body | string | no |  |
| `active` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 301,
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "type": "fridge",
    "type_label": "Fridge",
    "name": "Fridge #1",
    "serial": "LG-2231",
    "limits": {
        "temperature": {
            "min": 2,
            "max": 6
        }
    },
    "calibrated_at": null,
    "calibration_due_at": null,
    "calibration_overdue": false,
    "note": null,
    "active": true,
    "last": {
        "values": {
            "temperature": 4.1
        },
        "status": "ok",
        "recorded_at": "2026-11-03T11:00:00+05:00"
    }
}
```

### PATCH /haccp/equipment/{id} — Update HACCP equipment

Partial update of a controlled object. Requires `haccp.registries`.

MCP tool: `haccp_equipment_update` · Permission: `haccp.registries` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Object id |
| `location_id` | body | integer | no | HACCP location id |
| `type` | body | string | no | Object type key of the country profile (`fridge`, `freezer`, …); unknown — `other` |
| `name` | body | string | no |  |
| `serial` | body | string | no |  |
| `limits` | body | object | no | `{"temperature": {"min": 2, "max": 6}, "humidity": {"min": 40, "max": 70}}` |
| `calibrated_at` | body | string | no |  |
| `calibration_due_at` | body | string | no |  |
| `note` | body | string | no |  |
| `active` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 301,
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "type": "fridge",
    "type_label": "Fridge",
    "name": "Fridge #1",
    "serial": "LG-2231",
    "limits": {
        "temperature": {
            "min": 2,
            "max": 6
        }
    },
    "calibrated_at": null,
    "calibration_due_at": null,
    "calibration_overdue": false,
    "note": null,
    "active": true,
    "last": {
        "values": {
            "temperature": 4.1
        },
        "status": "ok",
        "recorded_at": "2026-11-03T11:00:00+05:00"
    }
}
```

### DELETE /haccp/equipment/{id} — Delete HACCP equipment

Removes a controlled object; its entries are kept. Requires `haccp.registries`.

MCP tool: `haccp_equipment_delete` · Permission: `haccp.registries` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Object id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /haccp/medical-books — List staff medical books

Employees of locations where the token has the sensitive `haccp.medical` permission, with the latest document of each type (medical book, hygiene training, …) and its status (`valid`, `expiring`, `expired`, `missing`). Each call is written to the access log.

MCP tool: `haccp_medical_list` · Permission: `haccp.medical` · Plan feature: `haccp`

Example response `data`:

```json
[
    {
        "id": 311,
        "name": "Aigerim Sadykova",
        "position": "Cook",
        "location_ids": [
            4609
        ],
        "problems": 0,
        "docs": {
            "medical_book": {
                "doc_id": 77,
                "number": "MB-1022",
                "issued_at": "2026-03-01",
                "expires_at": "2027-03-01",
                "status": "valid"
            }
        },
        "history": [
            {
                "id": 77,
                "type": "medical_book",
                "number": "MB-1022",
                "issued_at": "2026-03-01",
                "expires_at": "2027-03-01",
                "issuer": "City clinic #4",
                "note": null,
                "file": {
                    "path": "haccp/761/docs/2026/03/mb.pdf",
                    "url": "https://storage.example.com/haccp/761/docs/2026/03/mb.pdf?X-Amz-Signature=…"
                }
            }
        ]
    }
]
```

### POST /haccp/medical-books — Add a staff medical document

Records a medical book / health document of an employee. Requires `haccp.registries` and `haccp.medical` for the employee's location. `expires_at` defaults from the document type validity.

MCP tool: `haccp_medical_create` · Permission: `haccp.registries` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `user_id` | body | integer | yes |  |
| `type` | body | string | yes | Document type key of the country profile, e.g. `medical_book` |
| `number` | body | string | no |  |
| `issued_at` | body | string | no |  |
| `expires_at` | body | string | no |  |
| `issuer` | body | string | no |  |
| `note` | body | string | no |  |
| `file` | body | string | no | Scan path from `haccp.media.upload` (folder `docs`) |

Example response `data`:

```json
{
    "id": 77,
    "user_id": 311,
    "type": "medical_book",
    "number": "MB-1022",
    "issued_at": "2026-03-01",
    "expires_at": "2027-03-01",
    "issuer": "City clinic #4",
    "note": null
}
```

### GET /haccp/suppliers — List HACCP suppliers

Approved suppliers with their documents (certificates, declarations) and expiry status.

MCP tool: `haccp_suppliers_list` · Permission: `haccp.view` · Plan feature: `haccp`

Example response `data`:

```json
[
    {
        "id": 12,
        "name": "Fresh Farm LLP",
        "tax_id": "123456789012",
        "contact": "Arman",
        "phone": "+7 701 000 00 00",
        "email": "sales@freshfarm.kz",
        "products": "Poultry",
        "status": "approved",
        "rating": 5,
        "note": null,
        "evaluated_at": "2026-09-01",
        "expired": 0,
        "expiring": 1,
        "docs": [
            {
                "id": 30,
                "type": "certificate",
                "number": "KZ-77",
                "product": "Chicken",
                "issued_at": "2026-01-10",
                "valid_until": "2026-11-20",
                "status": "expiring",
                "file": null
            }
        ]
    }
]
```

### GET /haccp/verifications — List HACCP verification activities

Scheduled verification activities (internal audits, lab tests, swabs, calibration) with next due date and the latest records.

MCP tool: `haccp_verifications_list` · Permission: `haccp.view` · Plan feature: `haccp`

Example response `data`:

```json
[
    {
        "id": 5,
        "type": "internal_audit",
        "title": "Quarterly HACCP audit",
        "description": null,
        "every_days": 90,
        "next_due_at": "2026-12-01",
        "last_done_at": "2026-09-01",
        "responsible": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "plan_id": 3,
        "active": true,
        "overdue": false,
        "records": [
            {
                "id": 41,
                "done_at": "2026-09-01",
                "user": "Daniyar Omarov",
                "result": "ok",
                "findings": null,
                "actions": null,
                "files": []
            }
        ]
    }
]
```

### POST /haccp/verifications/{id}/records — Record a verification result

Records that a verification activity was done; the next due date moves by its interval. Findings are required when `result` is `issues`. Requires `haccp.registries`.

MCP tool: `haccp_verifications_record` · Permission: `haccp.registries` · Plan feature: `haccp`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Verification activity id |
| `done_at` | body | string | no | Default — today |
| `result` | body | string (ok, issues) | no |  |
| `findings` | body | string | no |  |
| `actions` | body | string | no |  |
| `files` | body | array<string> | no | Paths from `haccp.media.upload` |

Example response `data`:

```json
{
    "id": 42,
    "verification_id": 5,
    "done_at": "2026-11-03",
    "result": "ok",
    "findings": null,
    "actions": null
}
```

### GET /haccp/settings — Get HACCP settings

Module settings: country profile of norms, timezone, shift start, HACCP leader, retention and grace periods, options and HACCP locations. Requires `haccp.settings`.

MCP tool: `haccp_settings_get` · Permission: `haccp.settings` · Plan feature: `haccp`

Example response `data`:

```json
{
    "profile": "kz",
    "profile_name": "Kazakhstan",
    "timezone": "Asia/Almaty",
    "shift_start": "08:00",
    "leader": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "retention_months": 12,
    "grace_minutes": 30,
    "options": {
        "late_minutes": 60,
        "expiry_warning_days": 30
    },
    "onboarded_at": "2026-10-17T12:00:00+05:00",
    "sites": [
        {
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "kind": "cafe",
            "seats": 40,
            "shift_start": null,
            "work_days": [
                1,
                2,
                3,
                4,
                5,
                6,
                7
            ],
            "active": true
        }
    ]
}
```

## Schedule & attendance

Staff schedule: shifts, publishing, open shifts, clock-in/out with breaks, who is on shift now, attendance records, timesheet, absences and shift swaps, availability, KPI and payroll. Location permissions follow `schedule.*`; pay rates and payroll require `pay.*` and are omitted otherwise.

### GET /schedule/shifts — List shifts

Shifts at locations where the token has `schedule.view`, by date. Drafts and unpublished changes are included (`status`: `draft`, `published`, `changed`; `removed` — deleted but not yet published). Default period — the current week, max 62 days.

MCP tool: `schedule_shifts_list` · Permission: `schedule.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `date_from` | query | string | no | From (inclusive) |
| `date_to` | query | string | no | To (inclusive) |
| `location_ids` | query | array<integer> | no | Only these locations |
| `user_ids` | query | array<integer> | no | Only these employees |
| `status` | query | string (draft, published, changed) | no |  |
| `open` | query | boolean | no | `true` — only open (unassigned) shifts |
| `include_removed` | query | boolean | no | Include removed shifts awaiting publication |
| `updated_since` | query | string | no | Only shifts changed after this moment — use for incremental sync |

Example response `data`:

```json
[
    {
        "id": 70211,
        "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "position_id": 12,
        "template_id": 3,
        "date": "2026-11-04",
        "start": "09:00",
        "end": "18:00",
        "starts_at": "2026-11-04T09:00:00+05:00",
        "ends_at": "2026-11-04T18:00:00+05:00",
        "overnight": false,
        "break_minutes": 60,
        "minutes": 480,
        "status": "published",
        "published": true,
        "removed": false,
        "open": false,
        "note": null,
        "color": null,
        "updated_at": "2026-11-01T12:00:00+05:00"
    }
]
```

### GET /schedule/board — Get the schedule board of a location

What the schedule grid shows for one location: employees with hour totals, shifts with warnings and attendance, absences, availability, demand coverage and totals. Shift cost, employee rates and the budget are included only with `pay.rates.view` at the location.

MCP tool: `schedule_board` · Permission: `schedule.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | query | integer | yes | Location id |
| `date_from` | query | string | no |  |
| `date_to` | query | string | no |  |

Example response `data`:

```json
{
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "date_from": "2026-11-02",
    "date_to": "2026-11-08",
    "days": [
        {
            "date": "2026-11-02",
            "weekday": 1,
            "holiday": null
        }
    ],
    "employees": [
        {
            "id": 311,
            "name": "Aigerim Sadykova",
            "position_id": 12,
            "position": "Barista",
            "pay_type": "hourly",
            "rate": 1500,
            "totals": {
                "minutes": 2400,
                "shifts": 5,
                "cost": 60000
            }
        }
    ],
    "shifts": [
        {
            "id": 70211,
            "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "position_id": 12,
            "template_id": 3,
            "date": "2026-11-04",
            "start": "09:00",
            "end": "18:00",
            "starts_at": "2026-11-04T09:00:00+05:00",
            "ends_at": "2026-11-04T18:00:00+05:00",
            "overnight": false,
            "break_minutes": 60,
            "minutes": 480,
            "status": "published",
            "published": true,
            "removed": false,
            "open": false,
            "note": null,
            "color": null,
            "updated_at": "2026-11-01T12:00:00+05:00",
            "warnings": [],
            "cost": 12000,
            "attendance": null
        }
    ],
    "absences": [
        {
            "id": 912,
            "user_id": 311,
            "type": "vacation",
            "from": "2026-12-01",
            "to": "2026-12-14",
            "status": "approved"
        }
    ],
    "availability": [
        {
            "user_id": 311,
            "date": "2026-11-06",
            "kind": "unavailable",
            "start": null,
            "end": null,
            "note": "University"
        }
    ],
    "coverage": {
        "2026-11-02": [
            {
                "position_id": 12,
                "start": "08:00",
                "end": "12:00",
                "need": 2,
                "have": 1
            }
        ]
    },
    "totals": {
        "minutes": 9600,
        "cost": 240000,
        "budget": 300000
    },
    "unpublished": 3,
    "warnings_count": 1,
    "can_pay": true,
    "can_edit": true,
    "can_publish": true,
    "currency": "KZT"
}
```

### POST /schedule/shifts — Create a shift

Creates a draft shift (employees see it after `schedule.publish`). Rejected in a closed month.

MCP tool: `schedule_shifts_create` · Permission: `schedule.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | body | integer | yes | Location id (`schedule.edit` there) |
| `user_id` | body | integer | no | Employee; `null` — open shift anyone eligible can claim |
| `position_id` | body | integer | no | Job position; default — the employee's position |
| `template_id` | body | integer | no | Shift template: fills start, end, break and position |
| `date` | body | string | yes | Shift date (local) |
| `start` | body | string | no | Start time `HH:MM` (local). Required without a template |
| `end` | body | string | no | End time `HH:MM`; earlier than start — ends next day |
| `break_minutes` | body | integer | no |  |
| `note` | body | string | no |  |
| `color` | body | string | no |  |

Example response `data`:

```json
{
    "id": 70211,
    "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "position_id": 12,
    "template_id": 3,
    "date": "2026-11-04",
    "start": "09:00",
    "end": "18:00",
    "starts_at": "2026-11-04T09:00:00+05:00",
    "ends_at": "2026-11-04T18:00:00+05:00",
    "overnight": false,
    "break_minutes": 60,
    "minutes": 480,
    "status": "published",
    "published": true,
    "removed": false,
    "open": false,
    "note": null,
    "color": null,
    "updated_at": "2026-11-01T12:00:00+05:00"
}
```

### PATCH /schedule/shifts/{id} — Update a shift

Partial update. A published shift becomes `changed` until published again.

MCP tool: `schedule_shifts_update` · Permission: `schedule.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Shift id |
| `location_id` | body | integer | no | Location id (`schedule.edit` there) |
| `user_id` | body | integer | no | Employee; `null` — open shift anyone eligible can claim |
| `position_id` | body | integer | no | Job position; default — the employee's position |
| `template_id` | body | integer | no | Shift template: fills start, end, break and position |
| `date` | body | string | no | Shift date (local) |
| `start` | body | string | no | Start time `HH:MM` (local). Required without a template |
| `end` | body | string | no | End time `HH:MM`; earlier than start — ends next day |
| `break_minutes` | body | integer | no |  |
| `note` | body | string | no |  |
| `color` | body | string | no |  |

Example response `data`:

```json
{
    "id": 70211,
    "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
    "location": {
        "id": 4609,
        "name": "Abay Ave"
    },
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "position_id": 12,
    "template_id": 3,
    "date": "2026-11-04",
    "start": "09:00",
    "end": "18:00",
    "starts_at": "2026-11-04T09:00:00+05:00",
    "ends_at": "2026-11-04T18:00:00+05:00",
    "overnight": false,
    "break_minutes": 60,
    "minutes": 480,
    "status": "published",
    "published": true,
    "removed": false,
    "open": false,
    "note": null,
    "color": null,
    "updated_at": "2026-11-01T12:00:00+05:00"
}
```

### DELETE /schedule/shifts/{id} — Delete a shift

Deletes a draft shift; a published one is marked removed until the schedule is published again.

MCP tool: `schedule_shifts_delete` · Permission: `schedule.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Shift id |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /schedule/publish — Publish the schedule

Publishes drafts and changes of a location for a period; affected employees are notified (unless `notify=false`).

MCP tool: `schedule_publish` · Permission: `schedule.publish`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | body | integer | yes |  |
| `date_from` | body | string | yes |  |
| `date_to` | body | string | yes |  |
| `notify` | body | boolean | no | Notify employees, default `true` |

Example response `data`:

```json
{
    "published": 24,
    "removed": 1,
    "notified": 9
}
```

### POST /schedule/copy — Copy shifts to another period

Copies the shifts of `days` days starting at `date_from` to `target_date`, optionally repeated. Absent employees are skipped. Copies are drafts.

MCP tool: `schedule_copy` · Permission: `schedule.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | body | integer | yes |  |
| `date_from` | body | string | yes |  |
| `days` | body | integer | yes |  |
| `target_date` | body | string | yes |  |
| `repeat` | body | integer | no | How many times to repeat, default 1 |
| `replace` | body | boolean | no | Replace existing shifts in the target period |

Example response `data`:

```json
{
    "created": 35
}
```

### POST /schedule/autofill — Auto-fill the schedule

Assigns employees to shifts by demand, availability, absences and rules; unfilled demand becomes open shifts (unless `create_open=false`).

MCP tool: `schedule_autofill` · Permission: `schedule.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | body | integer | yes |  |
| `date_from` | body | string | yes |  |
| `date_to` | body | string | yes |  |
| `create_open` | body | boolean | no |  |

Example response `data`:

```json
{
    "assigned": 18,
    "open": 2,
    "unfilled": 1
}
```

### POST /schedule/clear — Clear shifts of a period

Deletes the shifts of a location for a period (published ones are marked removed until publishing).

MCP tool: `schedule_clear` · Permission: `schedule.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | body | integer | yes |  |
| `date_from` | body | string | yes |  |
| `date_to` | body | string | yes |  |

Example response `data`:

```json
{
    "cleared": 12
}
```

### GET /schedule/templates — List shift templates

Company-wide shift templates and templates of the location.

MCP tool: `schedule_templates_list` · Permission: `schedule.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_id` | query | integer | no | Include templates of this location |

Example response `data`:

```json
[
    {
        "id": 3,
        "location_id": null,
        "name": "Morning",
        "code": "M",
        "start": "08:00",
        "end": "16:00",
        "break_minutes": 30,
        "position_id": null,
        "color": "#fab005"
    }
]
```

### GET /schedule/my/shifts — List my shifts

Published shifts of the acting user with attendance and (if enabled by the company) colleagues on the same shift. Default — current week, max 62 days.

MCP tool: `schedule_my_shifts`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `date_from` | query | string | no |  |
| `date_to` | query | string | no |  |

Example response `data`:

```json
[
    {
        "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "date": "2026-11-04",
        "start": "09:00",
        "end": "18:00",
        "starts_at": "2026-11-04T09:00:00+05:00",
        "ends_at": "2026-11-04T18:00:00+05:00",
        "break_minutes": 60,
        "hours": 8,
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "position_id": 12,
        "position": "Barista",
        "note": null,
        "open": false,
        "pending_change": false,
        "attendance": null,
        "colleagues": [
            {
                "user_id": 312,
                "name": "Arman Bekov",
                "start": "09:00",
                "end": "18:00",
                "position": "Cashier",
                "shift_uuid": "3d4e5f6a-7b8c-4d9e-8f0a-1b2c3d4e5f6a"
            }
        ]
    }
]
```

### GET /schedule/my/month — Get my hours for a month

Timesheet of the acting user for a month: totals and day codes; accrued pay only if the company shows pay to staff.

MCP tool: `schedule_my_month`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `month` | query | string | no | Month `YYYY-MM`, default — current |

Example response `data`:

```json
{
    "month": "2026-11",
    "totals": {
        "planned_hours": 168,
        "hours": 52.5,
        "night_hours": 0,
        "holiday_hours": 0,
        "overtime_hours": 0,
        "days": 7,
        "late_count": 1,
        "late_minutes": 4,
        "absences": []
    },
    "days": [
        {
            "date": "2026-11-03",
            "code": "Я",
            "planned": 480,
            "hours": 7.5,
            "late": 4
        }
    ],
    "pay": null
}
```

### GET /schedule/open-shifts — List open shifts I can take

Published unassigned future shifts at locations where the acting user may work, matching the position and without overlaps.

MCP tool: `schedule_open_shifts_list`

Example response `data`:

```json
[
    {
        "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "date": "2026-11-04",
        "start": "09:00",
        "end": "18:00",
        "starts_at": "2026-11-04T09:00:00+05:00",
        "ends_at": "2026-11-04T18:00:00+05:00",
        "break_minutes": 60,
        "hours": 8,
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "position_id": 12,
        "position": "Barista",
        "note": null,
        "open": false,
        "requested": false,
        "taken": false
    }
]
```

### POST /schedule/open-shifts/{uuid}/claim — Claim an open shift

Takes an open shift: assigned right away or sent to the manager, depending on company rules.

MCP tool: `schedule_open_shifts_claim`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Shift UUID |
| `note` | body | string | no |  |

Example response `data`:

```json
{
    "status": "approved"
}
```

### GET /schedule/clock — Get my clock state

Whether the acting user is on shift / on a break, the open attendance, current and next shift, clock-in locations with geofence and the company clock rules.

MCP tool: `schedule_clock_state`

Example response `data`:

```json
{
    "on_shift": true,
    "on_break": false,
    "attendance": {
        "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
        "date": "2026-11-03",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "clock_in_at": "2026-11-03T09:04:00+05:00",
        "clock_out_at": null,
        "breaks": [
            {
                "start": "13:00",
                "end": "13:30"
            }
        ],
        "status": "open",
        "late_minutes": 4,
        "early_minutes": 0,
        "worked_minutes": 0,
        "approved_minutes": null,
        "flags": [
            "late"
        ]
    },
    "current_shift": {
        "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "date": "2026-11-04",
        "start": "09:00",
        "end": "18:00",
        "starts_at": "2026-11-04T09:00:00+05:00",
        "ends_at": "2026-11-04T18:00:00+05:00",
        "break_minutes": 60,
        "hours": 8,
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "position_id": 12,
        "position": "Barista",
        "note": null,
        "open": false
    },
    "next_shift": null,
    "sites": [
        {
            "id": 4609,
            "name": "Abay Ave",
            "latitude": 43.2389,
            "longitude": 76.8897,
            "radius": 150
        }
    ],
    "rules": {
        "require_geo": true,
        "require_photo": false,
        "block_out_of_zone": false,
        "allow_without_shift": true,
        "early_clock_in_minutes": 30,
        "allow_web_clock": false
    },
    "server_time": "2026-11-03T09:05:00+05:00"
}
```

### POST /schedule/clock/in — Clock in

Starts attendance for the acting user (same rules as the mobile app: geofence, photo, early clock-in limit, matching shift). Repeating the same `uuid` returns the existing record. Errors carry a reason code: `already_in`, `no_shift`, `too_early`, `geo_required`, `out_of_zone`, `photo_required`, `qr_invalid`, `department_required`, `bad_time`.

MCP tool: `schedule_clock_in`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | body | string | no | Client UUID of the clock-in, for safe retries |
| `attendance_uuid` | body | string | no | Attendance to close (clock-out); default — the open one |
| `location_id` | body | integer | no | Location; default — from the shift or geolocation |
| `shift_uuid` | body | string | no | Shift being worked |
| `qr` | body | string | no | Scanned QR content (link `/clock/{token}` or the token) |
| `lat` | body | number | no |  |
| `lng` | body | number | no |  |
| `photo` | body | string | no | Photo path from `schedule.media.upload` |
| `recorded_at` | body | string | no | Device time for offline sync |
| `note` | body | string | no |  |

Example response `data`:

```json
{
    "attendance": {
        "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
        "date": "2026-11-03",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "clock_in_at": "2026-11-03T09:04:00+05:00",
        "clock_out_at": null,
        "breaks": [
            {
                "start": "13:00",
                "end": "13:30"
            }
        ],
        "status": "open",
        "late_minutes": 4,
        "early_minutes": 0,
        "worked_minutes": 0,
        "approved_minutes": null,
        "flags": [
            "late"
        ]
    },
    "clock": {
        "on_shift": true,
        "on_break": false,
        "attendance": {
            "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
            "date": "2026-11-03",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "clock_in_at": "2026-11-03T09:04:00+05:00",
            "clock_out_at": null,
            "breaks": [
                {
                    "start": "13:00",
                    "end": "13:30"
                }
            ],
            "status": "open",
            "late_minutes": 4,
            "early_minutes": 0,
            "worked_minutes": 0,
            "approved_minutes": null,
            "flags": [
                "late"
            ]
        },
        "current_shift": {
            "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
            "date": "2026-11-04",
            "start": "09:00",
            "end": "18:00",
            "starts_at": "2026-11-04T09:00:00+05:00",
            "ends_at": "2026-11-04T18:00:00+05:00",
            "break_minutes": 60,
            "hours": 8,
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "position_id": 12,
            "position": "Barista",
            "note": null,
            "open": false
        },
        "next_shift": null,
        "sites": [
            {
                "id": 4609,
                "name": "Abay Ave",
                "latitude": 43.2389,
                "longitude": 76.8897,
                "radius": 150
            }
        ],
        "rules": {
            "require_geo": true,
            "require_photo": false,
            "block_out_of_zone": false,
            "allow_without_shift": true,
            "early_clock_in_minutes": 30,
            "allow_web_clock": false
        },
        "server_time": "2026-11-03T09:05:00+05:00"
    }
}
```

### POST /schedule/clock/out — Clock out

Closes the open attendance of the acting user and computes worked minutes. Errors: `not_in`, `break_open`, `bad_time`.

MCP tool: `schedule_clock_out`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | body | string | no | Client UUID of the clock-in, for safe retries |
| `attendance_uuid` | body | string | no | Attendance to close (clock-out); default — the open one |
| `location_id` | body | integer | no | Location; default — from the shift or geolocation |
| `shift_uuid` | body | string | no | Shift being worked |
| `qr` | body | string | no | Scanned QR content (link `/clock/{token}` or the token) |
| `lat` | body | number | no |  |
| `lng` | body | number | no |  |
| `photo` | body | string | no | Photo path from `schedule.media.upload` |
| `recorded_at` | body | string | no | Device time for offline sync |
| `note` | body | string | no |  |

Example response `data`:

```json
{
    "attendance": {
        "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
        "date": "2026-11-03",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "clock_in_at": "2026-11-03T09:04:00+05:00",
        "clock_out_at": null,
        "breaks": [
            {
                "start": "13:00",
                "end": "13:30"
            }
        ],
        "status": "open",
        "late_minutes": 4,
        "early_minutes": 0,
        "worked_minutes": 0,
        "approved_minutes": null,
        "flags": [
            "late"
        ]
    },
    "clock": {
        "on_shift": true,
        "on_break": false,
        "attendance": {
            "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
            "date": "2026-11-03",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "clock_in_at": "2026-11-03T09:04:00+05:00",
            "clock_out_at": null,
            "breaks": [
                {
                    "start": "13:00",
                    "end": "13:30"
                }
            ],
            "status": "open",
            "late_minutes": 4,
            "early_minutes": 0,
            "worked_minutes": 0,
            "approved_minutes": null,
            "flags": [
                "late"
            ]
        },
        "current_shift": {
            "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
            "date": "2026-11-04",
            "start": "09:00",
            "end": "18:00",
            "starts_at": "2026-11-04T09:00:00+05:00",
            "ends_at": "2026-11-04T18:00:00+05:00",
            "break_minutes": 60,
            "hours": 8,
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "position_id": 12,
            "position": "Barista",
            "note": null,
            "open": false
        },
        "next_shift": null,
        "sites": [
            {
                "id": 4609,
                "name": "Abay Ave",
                "latitude": 43.2389,
                "longitude": 76.8897,
                "radius": 150
            }
        ],
        "rules": {
            "require_geo": true,
            "require_photo": false,
            "block_out_of_zone": false,
            "allow_without_shift": true,
            "early_clock_in_minutes": 30,
            "allow_web_clock": false
        },
        "server_time": "2026-11-03T09:05:00+05:00"
    }
}
```

### POST /schedule/clock/break-start — Start a break

Starts a break in the open attendance. Errors: `not_in`, `break_open`.

MCP tool: `schedule_clock_break_start`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | body | string | no | Client UUID of the clock-in, for safe retries |
| `attendance_uuid` | body | string | no | Attendance to close (clock-out); default — the open one |
| `location_id` | body | integer | no | Location; default — from the shift or geolocation |
| `shift_uuid` | body | string | no | Shift being worked |
| `qr` | body | string | no | Scanned QR content (link `/clock/{token}` or the token) |
| `lat` | body | number | no |  |
| `lng` | body | number | no |  |
| `photo` | body | string | no | Photo path from `schedule.media.upload` |
| `recorded_at` | body | string | no | Device time for offline sync |
| `note` | body | string | no |  |

Example response `data`:

```json
{
    "attendance": {
        "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
        "date": "2026-11-03",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "clock_in_at": "2026-11-03T09:04:00+05:00",
        "clock_out_at": null,
        "breaks": [
            {
                "start": "13:00",
                "end": "13:30"
            }
        ],
        "status": "open",
        "late_minutes": 4,
        "early_minutes": 0,
        "worked_minutes": 0,
        "approved_minutes": null,
        "flags": [
            "late"
        ]
    },
    "clock": {
        "on_shift": true,
        "on_break": false,
        "attendance": {
            "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
            "date": "2026-11-03",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "clock_in_at": "2026-11-03T09:04:00+05:00",
            "clock_out_at": null,
            "breaks": [
                {
                    "start": "13:00",
                    "end": "13:30"
                }
            ],
            "status": "open",
            "late_minutes": 4,
            "early_minutes": 0,
            "worked_minutes": 0,
            "approved_minutes": null,
            "flags": [
                "late"
            ]
        },
        "current_shift": {
            "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
            "date": "2026-11-04",
            "start": "09:00",
            "end": "18:00",
            "starts_at": "2026-11-04T09:00:00+05:00",
            "ends_at": "2026-11-04T18:00:00+05:00",
            "break_minutes": 60,
            "hours": 8,
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "position_id": 12,
            "position": "Barista",
            "note": null,
            "open": false
        },
        "next_shift": null,
        "sites": [
            {
                "id": 4609,
                "name": "Abay Ave",
                "latitude": 43.2389,
                "longitude": 76.8897,
                "radius": 150
            }
        ],
        "rules": {
            "require_geo": true,
            "require_photo": false,
            "block_out_of_zone": false,
            "allow_without_shift": true,
            "early_clock_in_minutes": 30,
            "allow_web_clock": false
        },
        "server_time": "2026-11-03T09:05:00+05:00"
    }
}
```

### POST /schedule/clock/break-end — End a break

Ends the current break. Errors: `not_in`, `no_break`.

MCP tool: `schedule_clock_break_end`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | body | string | no | Client UUID of the clock-in, for safe retries |
| `attendance_uuid` | body | string | no | Attendance to close (clock-out); default — the open one |
| `location_id` | body | integer | no | Location; default — from the shift or geolocation |
| `shift_uuid` | body | string | no | Shift being worked |
| `qr` | body | string | no | Scanned QR content (link `/clock/{token}` or the token) |
| `lat` | body | number | no |  |
| `lng` | body | number | no |  |
| `photo` | body | string | no | Photo path from `schedule.media.upload` |
| `recorded_at` | body | string | no | Device time for offline sync |
| `note` | body | string | no |  |

Example response `data`:

```json
{
    "attendance": {
        "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
        "date": "2026-11-03",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "clock_in_at": "2026-11-03T09:04:00+05:00",
        "clock_out_at": null,
        "breaks": [
            {
                "start": "13:00",
                "end": "13:30"
            }
        ],
        "status": "open",
        "late_minutes": 4,
        "early_minutes": 0,
        "worked_minutes": 0,
        "approved_minutes": null,
        "flags": [
            "late"
        ]
    },
    "clock": {
        "on_shift": true,
        "on_break": false,
        "attendance": {
            "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
            "date": "2026-11-03",
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "clock_in_at": "2026-11-03T09:04:00+05:00",
            "clock_out_at": null,
            "breaks": [
                {
                    "start": "13:00",
                    "end": "13:30"
                }
            ],
            "status": "open",
            "late_minutes": 4,
            "early_minutes": 0,
            "worked_minutes": 0,
            "approved_minutes": null,
            "flags": [
                "late"
            ]
        },
        "current_shift": {
            "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
            "date": "2026-11-04",
            "start": "09:00",
            "end": "18:00",
            "starts_at": "2026-11-04T09:00:00+05:00",
            "ends_at": "2026-11-04T18:00:00+05:00",
            "break_minutes": 60,
            "hours": 8,
            "location": {
                "id": 4609,
                "name": "Abay Ave"
            },
            "position_id": 12,
            "position": "Barista",
            "note": null,
            "open": false
        },
        "next_shift": null,
        "sites": [
            {
                "id": 4609,
                "name": "Abay Ave",
                "latitude": 43.2389,
                "longitude": 76.8897,
                "radius": 150
            }
        ],
        "rules": {
            "require_geo": true,
            "require_photo": false,
            "block_out_of_zone": false,
            "allow_without_shift": true,
            "early_clock_in_minutes": 30,
            "allow_web_clock": false
        },
        "server_time": "2026-11-03T09:05:00+05:00"
    }
}
```

### POST /schedule/media — Upload a clock photo or request file

`kind=clock` — selfie for clock-in/out (image up to 8 MB); `kind=absence` — document for an absence request (image or PDF up to 10 MB). Returns the `path` to pass as `photo` / `file`.

MCP tool: `schedule_media_upload`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `file` | body | file | yes | File (multipart; in MCP — object with `name` and `content_base64`) |
| `kind` | body | string (clock, absence) | no |  |

Example response `data`:

```json
{
    "path": "staff/761/clock/2026/11/0f1e.jpg"
}
```

### GET /schedule/attendance — Get who is on shift

Attendance board by location: every published shift of the day with status (`upcoming`, `late`, `working`, `break`, `done`, `absent`, `missed`) and clock-ins without a shift. Phones only with `users.contacts`. Default — today at each location.

MCP tool: `schedule_attendance` · Permission: `schedule.attendance`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_ids` | query | array<integer> | no | Only these locations (default — all with `schedule.attendance`) |
| `date` | query | string | no | Day, default — today |

Example response `data`:

```json
[
    {
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "date": "2026-11-03",
        "now": "09:15",
        "counts": {
            "working": 3,
            "late": 1,
            "upcoming": 2,
            "total": 6
        },
        "people": [
            {
                "user": {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                },
                "position": "Barista",
                "phone": null,
                "status": "working",
                "shift": {
                    "id": 70211,
                    "start": "09:00",
                    "end": "18:00"
                },
                "attendance": {
                    "id": 55120,
                    "in": "09:04",
                    "out": null,
                    "late_minutes": 4,
                    "worked_minutes": 11,
                    "method": "app",
                    "flags": [
                        "late"
                    ]
                },
                "late_now_minutes": null,
                "absence": null
            }
        ]
    }
]
```

### GET /schedule/attendance/records — List attendance records

Clock-in/out journal at locations with `schedule.attendance`, newest first (max 1000 per period). `flag`: `late`, `early`, `out_of_zone`, `manual`, `offline`, `auto_closed`, `unapproved`. Default period — last 7 days.

MCP tool: `schedule_attendance_list` · Permission: `schedule.attendance`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `date_from` | query | string | no |  |
| `date_to` | query | string | no |  |
| `location_ids` | query | array<integer> | no |  |
| `user_id` | query | integer | no |  |
| `flag` | query | string | no |  |

Example response `data`:

```json
[
    {
        "id": 55120,
        "uuid": "5f6a7b8c-9d0e-4f1a-8b2c-3d4e5f6a7b8c",
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        },
        "date": "2026-11-03",
        "clock_in": "2026-11-03 09:04",
        "clock_out": "2026-11-03 18:02",
        "shift": {
            "id": 70211,
            "start": "09:00",
            "end": "18:00",
            "minutes": 480
        },
        "break_minutes": 30,
        "worked_minutes": 508,
        "approved_minutes": null,
        "approved": false,
        "status": "closed",
        "in_method": "app",
        "out_method": "app",
        "in_distance": 12,
        "out_distance": 20,
        "in_photo": null,
        "out_photo": null,
        "late_minutes": 4,
        "early_minutes": 0,
        "flags": [
            "late"
        ],
        "note": null,
        "edits": 0
    }
]
```

### POST /schedule/attendance/records — Add an attendance record

Manual attendance for an employee (forgot to clock in) with a reason; written to the edit history. Times are local wall time `YYYY-MM-DD HH:MM`.

MCP tool: `schedule_attendance_create` · Permission: `schedule.attendance`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `user_id` | body | integer | yes |  |
| `location_id` | body | integer | yes |  |
| `shift_id` | body | integer | no |  |
| `clock_in` | body | string | yes | `YYYY-MM-DD HH:MM`, local |
| `clock_out` | body | string | no | `YYYY-MM-DD HH:MM`, local |
| `break_minutes` | body | integer | no |  |
| `approved_minutes` | body | integer | no |  |
| `note` | body | string | no |  |
| `reason` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 55121
}
```

### PATCH /schedule/attendance/records/{id} — Correct an attendance record

Corrects times of an attendance record with a reason (kept in history). Rejected in a closed month.

MCP tool: `schedule_attendance_update` · Permission: `schedule.attendance`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Attendance id |
| `clock_in` | body | string | no | `YYYY-MM-DD HH:MM`, local |
| `clock_out` | body | string | no |  |
| `break_minutes` | body | integer | no |  |
| `approved_minutes` | body | integer | no |  |
| `note` | body | string | no |  |
| `reason` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 55120,
    "updated": true
}
```

### DELETE /schedule/attendance/records/{id} — Delete an attendance record

MCP tool: `schedule_attendance_delete` · Permission: `schedule.attendance`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Attendance id |
| `reason` | query | string | yes | Why the record is deleted |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /schedule/attendance/records/approve — Approve attendance records

Approves worked minutes of closed records (they go to the timesheet as approved). `minutes` overrides the value when approving one record.

MCP tool: `schedule_attendance_approve` · Permission: `schedule.attendance`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `ids` | body | array<integer> | yes |  |
| `minutes` | body | integer | no |  |

Example response `data`:

```json
{
    "approved": 12
}
```

### GET /schedule/timesheet — Get the timesheet

Planned and payable hours by employee and day (minutes), night/holiday/overtime, lateness, absences and day codes. A month, or `date_from`/`date_to` up to 62 days. `closed` — the month is locked.

MCP tool: `schedule_timesheet` · Permission: `schedule.timesheet`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `month` | query | string | no | Month `YYYY-MM`, default — current |
| `date_from` | query | string | no |  |
| `date_to` | query | string | no |  |
| `location_ids` | query | array<integer> | no |  |

Example response `data`:

```json
{
    "date_from": "2026-11-01",
    "date_to": "2026-11-30",
    "closed": false,
    "holidays": [],
    "rows": [
        {
            "user": {
                "id": 311,
                "name": "Aigerim Sadykova",
                "position": "Barista",
                "personnel_no": "A-17",
                "location_ids": [
                    4609
                ]
            },
            "totals": {
                "planned": 10080,
                "worked": 3200,
                "payable": 3150,
                "night": 0,
                "holiday": 0,
                "overtime": 0,
                "late_count": 1,
                "late": 4,
                "days": 7,
                "shifts": 7,
                "no_show": 0,
                "absences": [],
                "paid_absence_days": 0
            },
            "days": [
                {
                    "date": "2026-11-03",
                    "code": "Я",
                    "planned": 480,
                    "payable": 450,
                    "night": 0,
                    "holiday": 0,
                    "overtime": 0,
                    "late": 4,
                    "absence": null,
                    "flags": [
                        "late"
                    ],
                    "open": false
                }
            ]
        }
    ]
}
```

### GET /schedule/payroll — Get the payroll

Payroll for a month by employee: hours, amounts (base, night, holiday, overtime, KPI, adjustments, deductions, payable) and status (`draft`, `approved`, `paid`). Sensitive: requires `pay.payroll.view` (locations of the employees); each call is written to the access log. `rate` / `pay_type` are omitted for employees of locations without `pay.rates.view`.

MCP tool: `schedule_payroll` · Permission: `pay.payroll.view` · Plan feature: `payroll`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `month` | query | string | no | Month `YYYY-MM`, default — current |
| `location_ids` | query | array<integer> | no |  |

Example response `data`:

```json
{
    "month": "2026-10",
    "currency": "KZT",
    "status": "approved",
    "approved_at": "2026-11-02T10:00:00+05:00",
    "paid_at": null,
    "lines": [
        {
            "user": {
                "id": 311,
                "name": "Aigerim Sadykova",
                "position": "Barista",
                "personnel_no": "A-17",
                "location_ids": [
                    4609
                ]
            },
            "pay_type": "hourly",
            "rate": 1500,
            "hours": {
                "planned": 168,
                "payable": 170.5,
                "night": 0,
                "holiday": 8,
                "overtime": 2.5
            },
            "days": 21,
            "shifts": 21,
            "late_count": 2,
            "no_show": 0,
            "paid_absence_days": 0,
            "absences": [],
            "kpi_score": 96.5,
            "amounts": {
                "base": 255750,
                "night": 0,
                "holiday": 12000,
                "overtime": 5625,
                "absence": 0,
                "kpi": 20000,
                "bonus": 0,
                "compensation": 0,
                "accrued": 293375,
                "penalty": 0,
                "deduction": 0,
                "advance": 50000,
                "percent": [
                    {
                        "name": "IIT",
                        "percent": 10,
                        "amount": 29337.5
                    }
                ],
                "deducted": 79337.5,
                "payable": 214037.5
            },
            "adjustments": [
                {
                    "id": 81,
                    "kind": "advance",
                    "amount": 50000,
                    "comment": "Mid-month advance"
                }
            ],
            "norm_hours": 168
        }
    ],
    "totals": {
        "base": 255750,
        "night": 0,
        "holiday": 12000,
        "overtime": 5625,
        "absence": 0,
        "kpi": 20000,
        "bonus": 0,
        "compensation": 0,
        "accrued": 293375,
        "penalty": 0,
        "deduction": 0,
        "advance": 50000,
        "deducted": 79337.5,
        "payable": 214037.5,
        "hours": 170.5,
        "people": 1
    }
}
```

### POST /schedule/payroll/approve — Approve the payroll

Freezes the payroll of a month and closes the month for changes of shifts, attendance and absences. Requires `pay.payroll.approve` at all locations.

MCP tool: `schedule_payroll_approve` · Permission: `pay.payroll.approve` · Plan feature: `payroll`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `month` | body | string | yes | `YYYY-MM` |

Example response `data`:

```json
{
    "month": "2026-10",
    "status": "approved"
}
```

### POST /schedule/payroll/reopen — Reopen the payroll

Reopens an approved month for corrections.

MCP tool: `schedule_payroll_reopen` · Permission: `pay.payroll.approve` · Plan feature: `payroll`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `month` | body | string | yes | `YYYY-MM` |

Example response `data`:

```json
{
    "month": "2026-10",
    "status": "draft"
}
```

### POST /schedule/payroll/paid — Mark the payroll as paid

MCP tool: `schedule_payroll_paid` · Permission: `pay.payroll.approve` · Plan feature: `payroll`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `month` | body | string | yes | `YYYY-MM` |
| `paid` | body | boolean | no | `false` — undo, default `true` |

Example response `data`:

```json
{
    "month": "2026-10",
    "paid": true
}
```

### POST /schedule/payroll/adjustments — Add a payroll adjustment

Bonus, compensation, penalty, deduction or advance for an employee in an open month.

MCP tool: `schedule_payroll_adjustments_create` · Permission: `pay.payroll.approve` · Plan feature: `payroll`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `user_id` | body | integer | yes |  |
| `month` | body | string | yes | `YYYY-MM` |
| `kind` | body | string (bonus, compensation, penalty, deduction, advance) | yes |  |
| `amount` | body | number | yes |  |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "id": 81,
    "user_id": 311,
    "month": "2026-11",
    "kind": "advance",
    "amount": 50000,
    "comment": "Mid-month advance"
}
```

### DELETE /schedule/payroll/adjustments/{id} — Delete a payroll adjustment

MCP tool: `schedule_payroll_adjustments_delete` · Permission: `pay.payroll.approve` · Plan feature: `payroll`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Adjustment id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /schedule/kpi — Get the KPI scorecard

KPI metrics and each employee's values, achievement and total score for a month. The KPI bonus base is included only with `pay.rates.view`.

MCP tool: `schedule_kpi` · Permission: `schedule.kpi` · Plan feature: `payroll`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `month` | query | string | no | Month `YYYY-MM`, default — current |
| `location_ids` | query | array<integer> | no |  |

Example response `data`:

```json
{
    "month": "2026-11",
    "closed": false,
    "threshold": 70,
    "cap": 120,
    "metrics": [
        {
            "id": 4,
            "name": "Punctuality",
            "description": null,
            "unit": "%",
            "source": "punctuality",
            "direction": "up",
            "target": 95,
            "weight": 30,
            "position_ids": [],
            "active": true
        }
    ],
    "people": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "position": "Barista",
            "kpi_bonus": 20000,
            "kpi_bonus_type": "fixed",
            "score": 96.5,
            "metrics": [
                {
                    "metric_id": 4,
                    "value": 98,
                    "auto": 98,
                    "manual": false,
                    "comment": null,
                    "achievement": 103.2
                }
            ]
        }
    ]
}
```

### POST /schedule/kpi/values — Set a KPI value

Sets (or overrides an automatic) value of a metric for an employee and month. `value: null` clears the manual value.

MCP tool: `schedule_kpi_values_set` · Permission: `schedule.kpi` · Plan feature: `payroll`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `metric_id` | body | integer | yes |  |
| `user_id` | body | integer | yes |  |
| `month` | body | string | yes | `YYYY-MM` |
| `value` | body | number | no |  |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "saved": true
}
```

### GET /schedule/requests — List absence and swap requests

Requests of employees of locations with `schedule.requests`: `pending` (default) or `history` (decided in the last 90 days).

MCP tool: `schedule_requests_list` · Permission: `schedule.requests`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `status` | query | string (pending, history) | no |  |

Example response `data`:

```json
{
    "absences": [
        {
            "id": 912,
            "uuid": "8c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "pending",
            "paid": true,
            "comment": "Family trip",
            "file": null,
            "decided_by": null,
            "decided_at": null,
            "decision_note": null,
            "created_at": "2026-11-03T10:00:00+05:00"
        }
    ],
    "swaps": [
        {
            "id": 340,
            "uuid": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a",
            "type": "give",
            "status": "pending_peer",
            "note": "Doctor appointment",
            "from_user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "to_user": null,
            "shift": {
                "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
                "date": "2026-11-04",
                "start": "09:00",
                "end": "18:00",
                "location": {
                    "id": 4609,
                    "name": "Abay Ave"
                }
            },
            "to_shift": null,
            "decision_note": null,
            "created_at": "2026-11-03T10:00:00+05:00"
        }
    ]
}
```

### GET /schedule/my/requests — List my requests

Absences of the acting user (last 60 days and future) and swap requests — own and incoming (`can_answer`).

MCP tool: `schedule_my_requests`

Example response `data`:

```json
{
    "absences": [
        {
            "id": 912,
            "uuid": "8c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "pending",
            "paid": true,
            "comment": "Family trip",
            "file": null,
            "decided_by": null,
            "decided_at": null,
            "decision_note": null,
            "created_at": "2026-11-03T10:00:00+05:00"
        }
    ],
    "swaps": [
        {
            "id": 340,
            "uuid": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a",
            "type": "give",
            "status": "pending_peer",
            "note": "Doctor appointment",
            "from_user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "to_user": null,
            "shift": {
                "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
                "date": "2026-11-04",
                "start": "09:00",
                "end": "18:00",
                "location": {
                    "id": 4609,
                    "name": "Abay Ave"
                }
            },
            "to_shift": null,
            "decision_note": null,
            "created_at": "2026-11-03T10:00:00+05:00",
            "mine": true,
            "can_answer": false
        }
    ]
}
```

### POST /schedule/absences — Create an absence

Without `user_id` (or with the acting user) — a request of the acting user, goes to the manager. For another employee — recorded by the manager and approved right away (`schedule.requests` at the employee's location; `release_shifts` turns their shifts into open ones).

MCP tool: `schedule_absences_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `user_id` | body | integer | no | Employee, default — the acting user |
| `type` | body | string (vacation, sick, day_off, unpaid, business_trip, study, maternity, truancy, other) | yes |  |
| `date_from` | body | string | yes |  |
| `date_to` | body | string | no | Default — same day |
| `comment` | body | string | no |  |
| `paid` | body | boolean | no | Manager only: override whether the absence is paid |
| `file` | body | string | no | Document path from `schedule.media.upload` (`kind=absence`) |
| `release_shifts` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 912,
    "uuid": "8c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "type": "vacation",
    "date_from": "2026-12-01",
    "date_to": "2026-12-14",
    "days": 14,
    "status": "pending",
    "paid": true,
    "comment": "Family trip",
    "file": null,
    "decided_by": null,
    "decided_at": null,
    "decision_note": null,
    "created_at": "2026-11-03T10:00:00+05:00"
}
```

### POST /schedule/absences/{uuid}/cancel — Cancel an absence

The employee cancels their pending request; a manager with `schedule.requests` can cancel any absence of their employees.

MCP tool: `schedule_absences_cancel`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Absence UUID |

Example response `data`:

```json
{
    "id": 912,
    "uuid": "8c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "type": "vacation",
    "date_from": "2026-12-01",
    "date_to": "2026-12-14",
    "days": 14,
    "status": "pending",
    "paid": true,
    "comment": "Family trip",
    "file": null,
    "decided_by": null,
    "decided_at": null,
    "decision_note": null,
    "created_at": "2026-11-03T10:00:00+05:00"
}
```

### POST /schedule/absences/{uuid}/decide — Approve or reject an absence request

Manager decision; the employee is notified. `release_shifts` makes their shifts in these days open.

MCP tool: `schedule_absences_decide` · Permission: `schedule.requests`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Absence UUID |
| `approve` | body | boolean | yes |  |
| `note` | body | string | no |  |
| `release_shifts` | body | boolean | no |  |
| `paid` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 912,
    "uuid": "8c9d0e1f-2a3b-4c4d-9e5f-6a7b8c9d0e1f",
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "type": "vacation",
    "date_from": "2026-12-01",
    "date_to": "2026-12-14",
    "days": 14,
    "status": "pending",
    "paid": true,
    "comment": "Family trip",
    "file": null,
    "decided_by": null,
    "decided_at": null,
    "decision_note": null,
    "created_at": "2026-11-03T10:00:00+05:00"
}
```

### POST /schedule/swaps — Request a shift swap

`give` — give away own shift (to `to_user_id` or anyone eligible); `swap` — exchange with a colleague's shift `to_shift_uuid`. The colleague answers, then the manager decides.

MCP tool: `schedule_swaps_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `type` | body | string (give, swap) | yes |  |
| `shift_uuid` | body | string | yes | Own published shift |
| `to_shift_uuid` | body | string | no | Colleague's shift (for `swap`) |
| `to_user_id` | body | integer | no | Colleague (for `give`) |
| `note` | body | string | no |  |

Example response `data`:

```json
{
    "id": 340,
    "uuid": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a",
    "type": "give",
    "status": "pending_peer",
    "note": "Doctor appointment",
    "from_user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "to_user": null,
    "shift": {
        "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "date": "2026-11-04",
        "start": "09:00",
        "end": "18:00",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        }
    },
    "to_shift": null,
    "decision_note": null,
    "created_at": "2026-11-03T10:00:00+05:00"
}
```

### POST /schedule/swaps/{uuid}/answer — Answer a swap request

The colleague accepts or declines a swap / give-away addressed to them.

MCP tool: `schedule_swaps_answer`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Swap request UUID |
| `accept` | body | boolean | yes |  |

Example response `data`:

```json
{
    "id": 340,
    "uuid": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a",
    "type": "give",
    "status": "pending_peer",
    "note": "Doctor appointment",
    "from_user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "to_user": null,
    "shift": {
        "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "date": "2026-11-04",
        "start": "09:00",
        "end": "18:00",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        }
    },
    "to_shift": null,
    "decision_note": null,
    "created_at": "2026-11-03T10:00:00+05:00"
}
```

### POST /schedule/swaps/{uuid}/cancel — Cancel a swap request

MCP tool: `schedule_swaps_cancel`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Swap request UUID |

Example response `data`:

```json
{
    "id": 340,
    "uuid": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a",
    "type": "give",
    "status": "pending_peer",
    "note": "Doctor appointment",
    "from_user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "to_user": null,
    "shift": {
        "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "date": "2026-11-04",
        "start": "09:00",
        "end": "18:00",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        }
    },
    "to_shift": null,
    "decision_note": null,
    "created_at": "2026-11-03T10:00:00+05:00"
}
```

### POST /schedule/swaps/{uuid}/decide — Approve or reject a swap request

Manager decision (`schedule.requests` at the shift location). Approval reassigns the shifts and notifies both employees.

MCP tool: `schedule_swaps_decide` · Permission: `schedule.requests`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Swap request UUID |
| `approve` | body | boolean | yes |  |
| `note` | body | string | no |  |

Example response `data`:

```json
{
    "id": 340,
    "uuid": "9d0e1f2a-3b4c-4d5e-8f6a-7b8c9d0e1f2a",
    "type": "give",
    "status": "pending_peer",
    "note": "Doctor appointment",
    "from_user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "to_user": null,
    "shift": {
        "uuid": "2c3d4e5f-6a7b-4c8d-9e0f-1a2b3c4d5e6f",
        "date": "2026-11-04",
        "start": "09:00",
        "end": "18:00",
        "location": {
            "id": 4609,
            "name": "Abay Ave"
        }
    },
    "to_shift": null,
    "decision_note": null,
    "created_at": "2026-11-03T10:00:00+05:00"
}
```

### GET /schedule/availability — Get availability

Weekly / dated availability preferences of the acting user, or of an employee the token manages (`schedule.edit`).

MCP tool: `schedule_availability_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `user_id` | query | integer | no | Employee, default — the acting user |

Example response `data`:

```json
[
    {
        "id": 15,
        "weekday": 6,
        "date": null,
        "start_time": null,
        "end_time": null,
        "kind": "unavailable",
        "note": "University"
    }
]
```

### PUT /schedule/availability — Replace availability

Replaces all availability items of the acting user (or a managed employee). Each item: `weekday` (1–7) or `date`, optional `start_time`/`end_time` `HH:MM`, `kind` `unavailable` | `preferred`, `note`.

MCP tool: `schedule_availability_set`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `user_id` | body | integer | no | Employee, default — the acting user |
| `items` | body | array<object> | yes |  |

Example response `data`:

```json
[
    {
        "id": 15,
        "weekday": 6,
        "date": null,
        "start_time": null,
        "end_time": null,
        "kind": "unavailable",
        "note": "University"
    }
]
```

## HR

Employee HR records: the HR card, personal data, HR documents, hiring with onboarding checklists, transfers, dismissal and absences (vacations, sick leave). Permissions apply to the employee's locations; personal data, documents and pay rates are returned only with their own permissions and every view is written to the access log.

### GET /hr/employees/{uuid} — Retrieve an employee HR card

Work data, HR history, vacation balance, absences, documents and onboarding / offboarding checklists of an employee. Sections the token has no permission for are empty or `null` on the employee's locations: HR data — `hr.card`, personal data — `hr.personal`, documents — `hr.documents`, pay — `pay.rates.view`, contacts — `users.contacts`. Each view of sensitive data is written to the access log.

MCP tool: `hr_employees_get` · Permission: `users.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |

Example response `data`:

```json
{
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim",
        "lastname": "Sadykova",
        "email": "aigerim@example.com",
        "phone": "+7 701 111 22 33",
        "is_admin": false,
        "deleted": false,
        "deleted_at": null,
        "web_access": false,
        "position": {
            "id": 7,
            "name": "Barista"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home"
            }
        ],
        "home_location_id": 12,
        "work_anywhere": false,
        "teams": [
            {
                "id": 3,
                "name": "Bar",
                "is_head": false
            }
        ],
        "created_at": "2026-03-01T09:00:00+05:00",
        "last_seen_at": "2026-11-03T08:12:00+05:00",
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    },
    "status": {
        "status": "probation",
        "until": "2026-12-01",
        "absence_type": null,
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "onboarding": {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "done": 3,
            "total": 9,
            "overdue": 1
        }
    },
    "profile": {
        "middle_name": "Nurlanovna",
        "gender": "female",
        "birth_date": "1998-04-12",
        "birth_place": null,
        "citizenship": null,
        "marital_status": null,
        "children": null,
        "work_phone": null,
        "personal_phone": "+7 701 000 00 00",
        "personal_email": null,
        "address_actual": null,
        "address_registered": null,
        "tax_id": null,
        "insurance_no": null,
        "id_doc_type": null,
        "id_doc_number": null,
        "id_doc_issuer": null,
        "id_doc_issued_at": null,
        "id_doc_expires_at": null,
        "emergency_name": null,
        "emergency_relation": null,
        "emergency_phone": null,
        "bank_name": null,
        "bank_account": null,
        "education": null,
        "languages": null,
        "clothing_size": null,
        "shoe_size": null,
        "note": null,
        "custom": {}
    },
    "work": {
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "employment": "full",
        "probation_until": "2026-12-01",
        "contract_type": "permanent",
        "contract_number": "TD-2026/114",
        "contract_date": "2026-09-01",
        "contract_until": null,
        "vacation_days": null,
        "vacation_adjust": 0,
        "manager": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "team_managers": [],
        "reports": []
    },
    "pay": {
        "pay_type": "hourly",
        "rate": 1500,
        "kpi_bonus": null,
        "kpi_bonus_type": "fixed",
        "currency": "KZT",
        "history": [
            {
                "from": "2026-09-01",
                "pay_type": "hourly",
                "rate": 1500
            }
        ]
    },
    "vacation": {
        "per_year": 24,
        "accrued": 4.1,
        "adjust": 0,
        "used": 0,
        "planned": 0,
        "pending": 0,
        "balance": 4.1,
        "available": 4.1
    },
    "absences": [
        {
            "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "approved",
            "paid": true,
            "comment": null,
            "decision_note": null,
            "decided_by": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "decided_at": "2026-11-03T10:00:00+05:00",
            "has_file": false,
            "created_at": "2026-11-03T09:55:00+05:00"
        }
    ],
    "events": [
        {
            "id": 501,
            "type": "hire",
            "date": "2026-09-01",
            "data": {
                "position": "Barista"
            },
            "comment": null,
            "created_by": "Daniyar Omarov",
            "created_at": "2026-08-28T10:00:00+05:00"
        }
    ],
    "documents": [
        {
            "id": 88,
            "type": "contract",
            "title": "Employment contract",
            "number": "TD-2026/114",
            "issued_at": "2026-03-01",
            "expires_at": null,
            "note": null,
            "source": "hr",
            "file": {
                "name": "contract.pdf",
                "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
            }
        }
    ],
    "checklists": [
        {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "title": "Onboarding: Aigerim Sadykova",
            "start_date": "2026-11-04",
            "status": "active",
            "employee": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "sections": [
                {
                    "key": "s1",
                    "title": "Before the first day"
                }
            ],
            "progress": {
                "done": 3,
                "total": 9,
                "overdue": 1
            },
            "items": [
                {
                    "id": 7001,
                    "section_key": "s1",
                    "title": "Prepare a uniform",
                    "description": null,
                    "assignee": {
                        "id": 120,
                        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                        "name": "Daniyar Omarov"
                    },
                    "due_date": "2026-11-03",
                    "overdue": true,
                    "status": "open",
                    "requires": "none",
                    "required": true,
                    "comment": null,
                    "has_file": false,
                    "done_by": null,
                    "done_at": null,
                    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
                }
            ],
            "created_at": "2026-10-30T12:00:00+05:00",
            "completed_at": null
        }
    ],
    "can": {
        "contacts": true,
        "edit": true,
        "card": true,
        "personal": true,
        "documents": true,
        "hire": true,
        "absences": true,
        "pay_view": true,
        "pay_edit": false
    }
}
```

### PATCH /hr/employees/{uuid}/profile — Update personal data

Partial update of the employee profile. Name and other fields require `users.edit`, phone — `users.contacts`, personal data (documents, addresses, tax id, bank, emergency contact, family, custom fields) — `hr.personal`, all on the employee's locations.

MCP tool: `hr_employees_profile_update` · Permission: `users.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `name` | body | string | no | First name (`users.edit`) |
| `lastname` | body | string | no | Last name (`users.edit`) |
| `phone` | body | string | no | Phone (`users.contacts`) |
| `middle_name` | body | string | no |  |
| `work_phone` | body | string | no |  |
| `education` | body | string | no |  |
| `languages` | body | string | no |  |
| `clothing_size` | body | string | no |  |
| `shoe_size` | body | string | no |  |
| `note` | body | string | no |  |
| `gender` | body | string (male, female) | no | Personal data (`hr.personal`) |
| `birth_date` | body | string | no | Personal data (`hr.personal`) |
| `birth_place` | body | string | no |  |
| `citizenship` | body | string | no |  |
| `marital_status` | body | string (single, married, divorced, widowed) | no |  |
| `children` | body | integer | no |  |
| `personal_phone` | body | string | no |  |
| `personal_email` | body | string | no |  |
| `address_actual` | body | string | no |  |
| `address_registered` | body | string | no |  |
| `tax_id` | body | string | no |  |
| `insurance_no` | body | string | no |  |
| `id_doc_type` | body | string (id_card, passport, foreign, residence) | no |  |
| `id_doc_number` | body | string | no |  |
| `id_doc_issuer` | body | string | no |  |
| `id_doc_issued_at` | body | string | no |  |
| `id_doc_expires_at` | body | string | no |  |
| `emergency_name` | body | string | no |  |
| `emergency_relation` | body | string | no |  |
| `emergency_phone` | body | string | no |  |
| `bank_name` | body | string | no |  |
| `bank_account` | body | string | no |  |
| `custom` | body | object | no | Company custom fields: {key: value} (see `hr.settings.get`) |

Example response `data`:

```json
{
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim",
        "lastname": "Sadykova",
        "email": "aigerim@example.com",
        "phone": "+7 701 111 22 33",
        "is_admin": false,
        "deleted": false,
        "deleted_at": null,
        "web_access": false,
        "position": {
            "id": 7,
            "name": "Barista"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home"
            }
        ],
        "home_location_id": 12,
        "work_anywhere": false,
        "teams": [
            {
                "id": 3,
                "name": "Bar",
                "is_head": false
            }
        ],
        "created_at": "2026-03-01T09:00:00+05:00",
        "last_seen_at": "2026-11-03T08:12:00+05:00",
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    },
    "status": {
        "status": "probation",
        "until": "2026-12-01",
        "absence_type": null,
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "onboarding": {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "done": 3,
            "total": 9,
            "overdue": 1
        }
    },
    "profile": {
        "middle_name": "Nurlanovna",
        "gender": "female",
        "birth_date": "1998-04-12",
        "birth_place": null,
        "citizenship": null,
        "marital_status": null,
        "children": null,
        "work_phone": null,
        "personal_phone": "+7 701 000 00 00",
        "personal_email": null,
        "address_actual": null,
        "address_registered": null,
        "tax_id": null,
        "insurance_no": null,
        "id_doc_type": null,
        "id_doc_number": null,
        "id_doc_issuer": null,
        "id_doc_issued_at": null,
        "id_doc_expires_at": null,
        "emergency_name": null,
        "emergency_relation": null,
        "emergency_phone": null,
        "bank_name": null,
        "bank_account": null,
        "education": null,
        "languages": null,
        "clothing_size": null,
        "shoe_size": null,
        "note": null,
        "custom": {}
    },
    "work": {
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "employment": "full",
        "probation_until": "2026-12-01",
        "contract_type": "permanent",
        "contract_number": "TD-2026/114",
        "contract_date": "2026-09-01",
        "contract_until": null,
        "vacation_days": null,
        "vacation_adjust": 0,
        "manager": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "team_managers": [],
        "reports": []
    },
    "pay": {
        "pay_type": "hourly",
        "rate": 1500,
        "kpi_bonus": null,
        "kpi_bonus_type": "fixed",
        "currency": "KZT",
        "history": [
            {
                "from": "2026-09-01",
                "pay_type": "hourly",
                "rate": 1500
            }
        ]
    },
    "vacation": {
        "per_year": 24,
        "accrued": 4.1,
        "adjust": 0,
        "used": 0,
        "planned": 0,
        "pending": 0,
        "balance": 4.1,
        "available": 4.1
    },
    "absences": [
        {
            "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "approved",
            "paid": true,
            "comment": null,
            "decision_note": null,
            "decided_by": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "decided_at": "2026-11-03T10:00:00+05:00",
            "has_file": false,
            "created_at": "2026-11-03T09:55:00+05:00"
        }
    ],
    "events": [
        {
            "id": 501,
            "type": "hire",
            "date": "2026-09-01",
            "data": {
                "position": "Barista"
            },
            "comment": null,
            "created_by": "Daniyar Omarov",
            "created_at": "2026-08-28T10:00:00+05:00"
        }
    ],
    "documents": [
        {
            "id": 88,
            "type": "contract",
            "title": "Employment contract",
            "number": "TD-2026/114",
            "issued_at": "2026-03-01",
            "expires_at": null,
            "note": null,
            "source": "hr",
            "file": {
                "name": "contract.pdf",
                "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
            }
        }
    ],
    "checklists": [
        {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "title": "Onboarding: Aigerim Sadykova",
            "start_date": "2026-11-04",
            "status": "active",
            "employee": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "sections": [
                {
                    "key": "s1",
                    "title": "Before the first day"
                }
            ],
            "progress": {
                "done": 3,
                "total": 9,
                "overdue": 1
            },
            "items": [
                {
                    "id": 7001,
                    "section_key": "s1",
                    "title": "Prepare a uniform",
                    "description": null,
                    "assignee": {
                        "id": 120,
                        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                        "name": "Daniyar Omarov"
                    },
                    "due_date": "2026-11-03",
                    "overdue": true,
                    "status": "open",
                    "requires": "none",
                    "required": true,
                    "comment": null,
                    "has_file": false,
                    "done_by": null,
                    "done_at": null,
                    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
                }
            ],
            "created_at": "2026-10-30T12:00:00+05:00",
            "completed_at": null
        }
    ],
    "can": {
        "contacts": true,
        "edit": true,
        "card": true,
        "personal": true,
        "documents": true,
        "hire": true,
        "absences": true,
        "pay_view": true,
        "pay_edit": false
    }
}
```

### PATCH /hr/employees/{uuid}/work — Update work data

Hire date, personnel number, employment type, probation, manager, contract, vacation entitlement (`hr.hire`) and pay (`pay.rates.edit`; a new rate with `rate_from` goes to the rate history used by payroll).

MCP tool: `hr_employees_work_update` · Permission: `hr.hire` or `pay.rates.edit`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `hired_at` | body | string | no |  |
| `personnel_no` | body | string | no |  |
| `employment` | body | string (full, part, contract, intern) | no |  |
| `probation_until` | body | string | no |  |
| `manager_id` | body | integer | no | Line manager (user id) |
| `contract_type` | body | string (permanent, fixed, civil, self) | no |  |
| `contract_number` | body | string | no |  |
| `contract_date` | body | string | no |  |
| `contract_until` | body | string | no |  |
| `vacation_days` | body | integer | no | Personal annual vacation days (null — company default) |
| `vacation_adjust` | body | number | no | Manual vacation balance correction, days |
| `pay_type` | body | string (hourly, salary, shift) | no |  |
| `rate` | body | number | no |  |
| `rate_from` | body | string | no | Rate effective from |
| `kpi_bonus` | body | number | no |  |
| `kpi_bonus_type` | body | string (fixed, percent) | no |  |

Example response `data`:

```json
{
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim",
        "lastname": "Sadykova",
        "email": "aigerim@example.com",
        "phone": "+7 701 111 22 33",
        "is_admin": false,
        "deleted": false,
        "deleted_at": null,
        "web_access": false,
        "position": {
            "id": 7,
            "name": "Barista"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home"
            }
        ],
        "home_location_id": 12,
        "work_anywhere": false,
        "teams": [
            {
                "id": 3,
                "name": "Bar",
                "is_head": false
            }
        ],
        "created_at": "2026-03-01T09:00:00+05:00",
        "last_seen_at": "2026-11-03T08:12:00+05:00",
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    },
    "status": {
        "status": "probation",
        "until": "2026-12-01",
        "absence_type": null,
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "onboarding": {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "done": 3,
            "total": 9,
            "overdue": 1
        }
    },
    "profile": {
        "middle_name": "Nurlanovna",
        "gender": "female",
        "birth_date": "1998-04-12",
        "birth_place": null,
        "citizenship": null,
        "marital_status": null,
        "children": null,
        "work_phone": null,
        "personal_phone": "+7 701 000 00 00",
        "personal_email": null,
        "address_actual": null,
        "address_registered": null,
        "tax_id": null,
        "insurance_no": null,
        "id_doc_type": null,
        "id_doc_number": null,
        "id_doc_issuer": null,
        "id_doc_issued_at": null,
        "id_doc_expires_at": null,
        "emergency_name": null,
        "emergency_relation": null,
        "emergency_phone": null,
        "bank_name": null,
        "bank_account": null,
        "education": null,
        "languages": null,
        "clothing_size": null,
        "shoe_size": null,
        "note": null,
        "custom": {}
    },
    "work": {
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "employment": "full",
        "probation_until": "2026-12-01",
        "contract_type": "permanent",
        "contract_number": "TD-2026/114",
        "contract_date": "2026-09-01",
        "contract_until": null,
        "vacation_days": null,
        "vacation_adjust": 0,
        "manager": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "team_managers": [],
        "reports": []
    },
    "pay": {
        "pay_type": "hourly",
        "rate": 1500,
        "kpi_bonus": null,
        "kpi_bonus_type": "fixed",
        "currency": "KZT",
        "history": [
            {
                "from": "2026-09-01",
                "pay_type": "hourly",
                "rate": 1500
            }
        ]
    },
    "vacation": {
        "per_year": 24,
        "accrued": 4.1,
        "adjust": 0,
        "used": 0,
        "planned": 0,
        "pending": 0,
        "balance": 4.1,
        "available": 4.1
    },
    "absences": [
        {
            "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "approved",
            "paid": true,
            "comment": null,
            "decision_note": null,
            "decided_by": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "decided_at": "2026-11-03T10:00:00+05:00",
            "has_file": false,
            "created_at": "2026-11-03T09:55:00+05:00"
        }
    ],
    "events": [
        {
            "id": 501,
            "type": "hire",
            "date": "2026-09-01",
            "data": {
                "position": "Barista"
            },
            "comment": null,
            "created_by": "Daniyar Omarov",
            "created_at": "2026-08-28T10:00:00+05:00"
        }
    ],
    "documents": [
        {
            "id": 88,
            "type": "contract",
            "title": "Employment contract",
            "number": "TD-2026/114",
            "issued_at": "2026-03-01",
            "expires_at": null,
            "note": null,
            "source": "hr",
            "file": {
                "name": "contract.pdf",
                "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
            }
        }
    ],
    "checklists": [
        {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "title": "Onboarding: Aigerim Sadykova",
            "start_date": "2026-11-04",
            "status": "active",
            "employee": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "sections": [
                {
                    "key": "s1",
                    "title": "Before the first day"
                }
            ],
            "progress": {
                "done": 3,
                "total": 9,
                "overdue": 1
            },
            "items": [
                {
                    "id": 7001,
                    "section_key": "s1",
                    "title": "Prepare a uniform",
                    "description": null,
                    "assignee": {
                        "id": 120,
                        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                        "name": "Daniyar Omarov"
                    },
                    "due_date": "2026-11-03",
                    "overdue": true,
                    "status": "open",
                    "requires": "none",
                    "required": true,
                    "comment": null,
                    "has_file": false,
                    "done_by": null,
                    "done_at": null,
                    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
                }
            ],
            "created_at": "2026-10-30T12:00:00+05:00",
            "completed_at": null
        }
    ],
    "can": {
        "contacts": true,
        "edit": true,
        "card": true,
        "personal": true,
        "documents": true,
        "hire": true,
        "absences": true,
        "pay_view": true,
        "pay_edit": false
    }
}
```

### POST /hr/hire — Hire an employee

Creates the employee account (or hires an existing user by `uuid`), sets position, locations, manager and work data, and starts an onboarding checklist (`template_id`; default — the template for the position, 0 — none). Requires `hr.hire` and, for a new account, `users.create`; with a location-scoped `hr.hire` only your locations can be chosen.

MCP tool: `hr_hire` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | body | string | no | Existing user of the company to hire (instead of creating a new account) |
| `name` | body | string | no | First name (new account) |
| `lastname` | body | string | no | Last name (new account) |
| `email` | body | string | no | Login email (new account) |
| `phone` | body | string | no |  |
| `middle_name` | body | string | no |  |
| `gender` | body | string (male, female) | no |  |
| `birth_date` | body | string | no |  |
| `personal_phone` | body | string | no |  |
| `position_id` | body | integer | no |  |
| `home_location_id` | body | integer | no | Main location |
| `location_ids` | body | array<integer> | no | Additional locations |
| `work_anywhere` | body | boolean | no | Works at all locations (requires `hr.hire` for all locations) |
| `team_id` | body | integer | no |  |
| `manager_id` | body | integer | no |  |
| `hired_at` | body | string | yes | First working day |
| `probation_until` | body | string | no |  |
| `employment` | body | string (full, part, contract, intern) | no |  |
| `personnel_no` | body | string | no |  |
| `contract_type` | body | string (permanent, fixed, civil, self) | no |  |
| `contract_number` | body | string | no |  |
| `contract_until` | body | string | no |  |
| `web_access` | body | boolean | no | Access to the web cabinet (requires `users.access`) |
| `role_ids` | body | array<integer> | no | Access roles for the new account (only roles you may assign) |
| `invite` | body | string (now, start, none) | no | Send the invitation now, on the first working day or never (default `now`) |
| `template_id` | body | integer | no | Onboarding checklist template; 0 — no checklist |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim",
        "lastname": "Sadykova",
        "email": "aigerim@example.com",
        "phone": "+7 701 111 22 33",
        "is_admin": false,
        "deleted": false,
        "deleted_at": null,
        "web_access": false,
        "position": {
            "id": 7,
            "name": "Barista"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home"
            }
        ],
        "home_location_id": 12,
        "work_anywhere": false,
        "teams": [
            {
                "id": 3,
                "name": "Bar",
                "is_head": false
            }
        ],
        "created_at": "2026-03-01T09:00:00+05:00",
        "last_seen_at": "2026-11-03T08:12:00+05:00",
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    },
    "status": {
        "status": "probation",
        "until": "2026-12-01",
        "absence_type": null,
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "onboarding": {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "done": 3,
            "total": 9,
            "overdue": 1
        }
    },
    "profile": {
        "middle_name": "Nurlanovna",
        "gender": "female",
        "birth_date": "1998-04-12",
        "birth_place": null,
        "citizenship": null,
        "marital_status": null,
        "children": null,
        "work_phone": null,
        "personal_phone": "+7 701 000 00 00",
        "personal_email": null,
        "address_actual": null,
        "address_registered": null,
        "tax_id": null,
        "insurance_no": null,
        "id_doc_type": null,
        "id_doc_number": null,
        "id_doc_issuer": null,
        "id_doc_issued_at": null,
        "id_doc_expires_at": null,
        "emergency_name": null,
        "emergency_relation": null,
        "emergency_phone": null,
        "bank_name": null,
        "bank_account": null,
        "education": null,
        "languages": null,
        "clothing_size": null,
        "shoe_size": null,
        "note": null,
        "custom": {}
    },
    "work": {
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "employment": "full",
        "probation_until": "2026-12-01",
        "contract_type": "permanent",
        "contract_number": "TD-2026/114",
        "contract_date": "2026-09-01",
        "contract_until": null,
        "vacation_days": null,
        "vacation_adjust": 0,
        "manager": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "team_managers": [],
        "reports": []
    },
    "pay": {
        "pay_type": "hourly",
        "rate": 1500,
        "kpi_bonus": null,
        "kpi_bonus_type": "fixed",
        "currency": "KZT",
        "history": [
            {
                "from": "2026-09-01",
                "pay_type": "hourly",
                "rate": 1500
            }
        ]
    },
    "vacation": {
        "per_year": 24,
        "accrued": 4.1,
        "adjust": 0,
        "used": 0,
        "planned": 0,
        "pending": 0,
        "balance": 4.1,
        "available": 4.1
    },
    "absences": [
        {
            "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "approved",
            "paid": true,
            "comment": null,
            "decision_note": null,
            "decided_by": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "decided_at": "2026-11-03T10:00:00+05:00",
            "has_file": false,
            "created_at": "2026-11-03T09:55:00+05:00"
        }
    ],
    "events": [
        {
            "id": 501,
            "type": "hire",
            "date": "2026-09-01",
            "data": {
                "position": "Barista"
            },
            "comment": null,
            "created_by": "Daniyar Omarov",
            "created_at": "2026-08-28T10:00:00+05:00"
        }
    ],
    "documents": [
        {
            "id": 88,
            "type": "contract",
            "title": "Employment contract",
            "number": "TD-2026/114",
            "issued_at": "2026-03-01",
            "expires_at": null,
            "note": null,
            "source": "hr",
            "file": {
                "name": "contract.pdf",
                "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
            }
        }
    ],
    "checklists": [
        {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "title": "Onboarding: Aigerim Sadykova",
            "start_date": "2026-11-04",
            "status": "active",
            "employee": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "sections": [
                {
                    "key": "s1",
                    "title": "Before the first day"
                }
            ],
            "progress": {
                "done": 3,
                "total": 9,
                "overdue": 1
            },
            "items": [
                {
                    "id": 7001,
                    "section_key": "s1",
                    "title": "Prepare a uniform",
                    "description": null,
                    "assignee": {
                        "id": 120,
                        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                        "name": "Daniyar Omarov"
                    },
                    "due_date": "2026-11-03",
                    "overdue": true,
                    "status": "open",
                    "requires": "none",
                    "required": true,
                    "comment": null,
                    "has_file": false,
                    "done_by": null,
                    "done_at": null,
                    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
                }
            ],
            "created_at": "2026-10-30T12:00:00+05:00",
            "completed_at": null
        }
    ],
    "can": {
        "contacts": true,
        "edit": true,
        "card": true,
        "personal": true,
        "documents": true,
        "hire": true,
        "absences": true,
        "pay_view": true,
        "pay_edit": false
    }
}
```

### POST /hr/employees/{uuid}/transfer — Transfer an employee

Changes position, locations or manager; the change is recorded in the HR history with the before/after values.

MCP tool: `hr_employees_transfer` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `date` | body | string | yes | Effective date |
| `position_id` | body | integer | no |  |
| `home_location_id` | body | integer | no |  |
| `location_ids` | body | array<integer> | no |  |
| `work_anywhere` | body | boolean | no |  |
| `manager_id` | body | integer | no |  |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim",
        "lastname": "Sadykova",
        "email": "aigerim@example.com",
        "phone": "+7 701 111 22 33",
        "is_admin": false,
        "deleted": false,
        "deleted_at": null,
        "web_access": false,
        "position": {
            "id": 7,
            "name": "Barista"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home"
            }
        ],
        "home_location_id": 12,
        "work_anywhere": false,
        "teams": [
            {
                "id": 3,
                "name": "Bar",
                "is_head": false
            }
        ],
        "created_at": "2026-03-01T09:00:00+05:00",
        "last_seen_at": "2026-11-03T08:12:00+05:00",
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    },
    "status": {
        "status": "probation",
        "until": "2026-12-01",
        "absence_type": null,
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "onboarding": {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "done": 3,
            "total": 9,
            "overdue": 1
        }
    },
    "profile": {
        "middle_name": "Nurlanovna",
        "gender": "female",
        "birth_date": "1998-04-12",
        "birth_place": null,
        "citizenship": null,
        "marital_status": null,
        "children": null,
        "work_phone": null,
        "personal_phone": "+7 701 000 00 00",
        "personal_email": null,
        "address_actual": null,
        "address_registered": null,
        "tax_id": null,
        "insurance_no": null,
        "id_doc_type": null,
        "id_doc_number": null,
        "id_doc_issuer": null,
        "id_doc_issued_at": null,
        "id_doc_expires_at": null,
        "emergency_name": null,
        "emergency_relation": null,
        "emergency_phone": null,
        "bank_name": null,
        "bank_account": null,
        "education": null,
        "languages": null,
        "clothing_size": null,
        "shoe_size": null,
        "note": null,
        "custom": {}
    },
    "work": {
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "employment": "full",
        "probation_until": "2026-12-01",
        "contract_type": "permanent",
        "contract_number": "TD-2026/114",
        "contract_date": "2026-09-01",
        "contract_until": null,
        "vacation_days": null,
        "vacation_adjust": 0,
        "manager": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "team_managers": [],
        "reports": []
    },
    "pay": {
        "pay_type": "hourly",
        "rate": 1500,
        "kpi_bonus": null,
        "kpi_bonus_type": "fixed",
        "currency": "KZT",
        "history": [
            {
                "from": "2026-09-01",
                "pay_type": "hourly",
                "rate": 1500
            }
        ]
    },
    "vacation": {
        "per_year": 24,
        "accrued": 4.1,
        "adjust": 0,
        "used": 0,
        "planned": 0,
        "pending": 0,
        "balance": 4.1,
        "available": 4.1
    },
    "absences": [
        {
            "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "approved",
            "paid": true,
            "comment": null,
            "decision_note": null,
            "decided_by": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "decided_at": "2026-11-03T10:00:00+05:00",
            "has_file": false,
            "created_at": "2026-11-03T09:55:00+05:00"
        }
    ],
    "events": [
        {
            "id": 501,
            "type": "hire",
            "date": "2026-09-01",
            "data": {
                "position": "Barista"
            },
            "comment": null,
            "created_by": "Daniyar Omarov",
            "created_at": "2026-08-28T10:00:00+05:00"
        }
    ],
    "documents": [
        {
            "id": 88,
            "type": "contract",
            "title": "Employment contract",
            "number": "TD-2026/114",
            "issued_at": "2026-03-01",
            "expires_at": null,
            "note": null,
            "source": "hr",
            "file": {
                "name": "contract.pdf",
                "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
            }
        }
    ],
    "checklists": [
        {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "title": "Onboarding: Aigerim Sadykova",
            "start_date": "2026-11-04",
            "status": "active",
            "employee": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "sections": [
                {
                    "key": "s1",
                    "title": "Before the first day"
                }
            ],
            "progress": {
                "done": 3,
                "total": 9,
                "overdue": 1
            },
            "items": [
                {
                    "id": 7001,
                    "section_key": "s1",
                    "title": "Prepare a uniform",
                    "description": null,
                    "assignee": {
                        "id": 120,
                        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                        "name": "Daniyar Omarov"
                    },
                    "due_date": "2026-11-03",
                    "overdue": true,
                    "status": "open",
                    "requires": "none",
                    "required": true,
                    "comment": null,
                    "has_file": false,
                    "done_by": null,
                    "done_at": null,
                    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
                }
            ],
            "created_at": "2026-10-30T12:00:00+05:00",
            "completed_at": null
        }
    ],
    "can": {
        "contacts": true,
        "edit": true,
        "card": true,
        "personal": true,
        "documents": true,
        "hire": true,
        "absences": true,
        "pay_view": true,
        "pay_edit": false
    }
}
```

### POST /hr/employees/{uuid}/dismiss — Dismiss an employee

Registers the dismissal with the last working day and reason, cancels future absences and the active onboarding, starts the offboarding checklist. The account is closed the day after the last day (or now with `block_now`).

MCP tool: `hr_employees_dismiss` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `last_day` | body | string | yes |  |
| `reason` | body | string (own, agreement, contract_end, employer, probation, discipline, relocation, other) | yes |  |
| `comment` | body | string | no |  |
| `rehire` | body | boolean | no | Can be hired again, default true |
| `template_id` | body | integer | no | Offboarding template; 0 — no checklist |
| `block_now` | body | boolean | no | Close the account immediately |

Example response `data`:

```json
{
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim",
        "lastname": "Sadykova",
        "email": "aigerim@example.com",
        "phone": "+7 701 111 22 33",
        "is_admin": false,
        "deleted": false,
        "deleted_at": null,
        "web_access": false,
        "position": {
            "id": 7,
            "name": "Barista"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home"
            }
        ],
        "home_location_id": 12,
        "work_anywhere": false,
        "teams": [
            {
                "id": 3,
                "name": "Bar",
                "is_head": false
            }
        ],
        "created_at": "2026-03-01T09:00:00+05:00",
        "last_seen_at": "2026-11-03T08:12:00+05:00",
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    },
    "status": {
        "status": "probation",
        "until": "2026-12-01",
        "absence_type": null,
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "onboarding": {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "done": 3,
            "total": 9,
            "overdue": 1
        }
    },
    "profile": {
        "middle_name": "Nurlanovna",
        "gender": "female",
        "birth_date": "1998-04-12",
        "birth_place": null,
        "citizenship": null,
        "marital_status": null,
        "children": null,
        "work_phone": null,
        "personal_phone": "+7 701 000 00 00",
        "personal_email": null,
        "address_actual": null,
        "address_registered": null,
        "tax_id": null,
        "insurance_no": null,
        "id_doc_type": null,
        "id_doc_number": null,
        "id_doc_issuer": null,
        "id_doc_issued_at": null,
        "id_doc_expires_at": null,
        "emergency_name": null,
        "emergency_relation": null,
        "emergency_phone": null,
        "bank_name": null,
        "bank_account": null,
        "education": null,
        "languages": null,
        "clothing_size": null,
        "shoe_size": null,
        "note": null,
        "custom": {}
    },
    "work": {
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "employment": "full",
        "probation_until": "2026-12-01",
        "contract_type": "permanent",
        "contract_number": "TD-2026/114",
        "contract_date": "2026-09-01",
        "contract_until": null,
        "vacation_days": null,
        "vacation_adjust": 0,
        "manager": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "team_managers": [],
        "reports": []
    },
    "pay": {
        "pay_type": "hourly",
        "rate": 1500,
        "kpi_bonus": null,
        "kpi_bonus_type": "fixed",
        "currency": "KZT",
        "history": [
            {
                "from": "2026-09-01",
                "pay_type": "hourly",
                "rate": 1500
            }
        ]
    },
    "vacation": {
        "per_year": 24,
        "accrued": 4.1,
        "adjust": 0,
        "used": 0,
        "planned": 0,
        "pending": 0,
        "balance": 4.1,
        "available": 4.1
    },
    "absences": [
        {
            "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "approved",
            "paid": true,
            "comment": null,
            "decision_note": null,
            "decided_by": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "decided_at": "2026-11-03T10:00:00+05:00",
            "has_file": false,
            "created_at": "2026-11-03T09:55:00+05:00"
        }
    ],
    "events": [
        {
            "id": 501,
            "type": "hire",
            "date": "2026-09-01",
            "data": {
                "position": "Barista"
            },
            "comment": null,
            "created_by": "Daniyar Omarov",
            "created_at": "2026-08-28T10:00:00+05:00"
        }
    ],
    "documents": [
        {
            "id": 88,
            "type": "contract",
            "title": "Employment contract",
            "number": "TD-2026/114",
            "issued_at": "2026-03-01",
            "expires_at": null,
            "note": null,
            "source": "hr",
            "file": {
                "name": "contract.pdf",
                "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
            }
        }
    ],
    "checklists": [
        {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "title": "Onboarding: Aigerim Sadykova",
            "start_date": "2026-11-04",
            "status": "active",
            "employee": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "sections": [
                {
                    "key": "s1",
                    "title": "Before the first day"
                }
            ],
            "progress": {
                "done": 3,
                "total": 9,
                "overdue": 1
            },
            "items": [
                {
                    "id": 7001,
                    "section_key": "s1",
                    "title": "Prepare a uniform",
                    "description": null,
                    "assignee": {
                        "id": 120,
                        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                        "name": "Daniyar Omarov"
                    },
                    "due_date": "2026-11-03",
                    "overdue": true,
                    "status": "open",
                    "requires": "none",
                    "required": true,
                    "comment": null,
                    "has_file": false,
                    "done_by": null,
                    "done_at": null,
                    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
                }
            ],
            "created_at": "2026-10-30T12:00:00+05:00",
            "completed_at": null
        }
    ],
    "can": {
        "contacts": true,
        "edit": true,
        "card": true,
        "personal": true,
        "documents": true,
        "hire": true,
        "absences": true,
        "pay_view": true,
        "pay_edit": false
    }
}
```

### POST /hr/employees/{uuid}/cancel-dismissal — Cancel a dismissal

The employee stays: dismissal dates and the offboarding checklist are removed, access is restored.

MCP tool: `hr_employees_cancel_dismissal` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |

Example response `data`:

```json
{
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim",
        "lastname": "Sadykova",
        "email": "aigerim@example.com",
        "phone": "+7 701 111 22 33",
        "is_admin": false,
        "deleted": false,
        "deleted_at": null,
        "web_access": false,
        "position": {
            "id": 7,
            "name": "Barista"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home"
            }
        ],
        "home_location_id": 12,
        "work_anywhere": false,
        "teams": [
            {
                "id": 3,
                "name": "Bar",
                "is_head": false
            }
        ],
        "created_at": "2026-03-01T09:00:00+05:00",
        "last_seen_at": "2026-11-03T08:12:00+05:00",
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    },
    "status": {
        "status": "probation",
        "until": "2026-12-01",
        "absence_type": null,
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "onboarding": {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "done": 3,
            "total": 9,
            "overdue": 1
        }
    },
    "profile": {
        "middle_name": "Nurlanovna",
        "gender": "female",
        "birth_date": "1998-04-12",
        "birth_place": null,
        "citizenship": null,
        "marital_status": null,
        "children": null,
        "work_phone": null,
        "personal_phone": "+7 701 000 00 00",
        "personal_email": null,
        "address_actual": null,
        "address_registered": null,
        "tax_id": null,
        "insurance_no": null,
        "id_doc_type": null,
        "id_doc_number": null,
        "id_doc_issuer": null,
        "id_doc_issued_at": null,
        "id_doc_expires_at": null,
        "emergency_name": null,
        "emergency_relation": null,
        "emergency_phone": null,
        "bank_name": null,
        "bank_account": null,
        "education": null,
        "languages": null,
        "clothing_size": null,
        "shoe_size": null,
        "note": null,
        "custom": {}
    },
    "work": {
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "employment": "full",
        "probation_until": "2026-12-01",
        "contract_type": "permanent",
        "contract_number": "TD-2026/114",
        "contract_date": "2026-09-01",
        "contract_until": null,
        "vacation_days": null,
        "vacation_adjust": 0,
        "manager": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "team_managers": [],
        "reports": []
    },
    "pay": {
        "pay_type": "hourly",
        "rate": 1500,
        "kpi_bonus": null,
        "kpi_bonus_type": "fixed",
        "currency": "KZT",
        "history": [
            {
                "from": "2026-09-01",
                "pay_type": "hourly",
                "rate": 1500
            }
        ]
    },
    "vacation": {
        "per_year": 24,
        "accrued": 4.1,
        "adjust": 0,
        "used": 0,
        "planned": 0,
        "pending": 0,
        "balance": 4.1,
        "available": 4.1
    },
    "absences": [
        {
            "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "approved",
            "paid": true,
            "comment": null,
            "decision_note": null,
            "decided_by": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "decided_at": "2026-11-03T10:00:00+05:00",
            "has_file": false,
            "created_at": "2026-11-03T09:55:00+05:00"
        }
    ],
    "events": [
        {
            "id": 501,
            "type": "hire",
            "date": "2026-09-01",
            "data": {
                "position": "Barista"
            },
            "comment": null,
            "created_by": "Daniyar Omarov",
            "created_at": "2026-08-28T10:00:00+05:00"
        }
    ],
    "documents": [
        {
            "id": 88,
            "type": "contract",
            "title": "Employment contract",
            "number": "TD-2026/114",
            "issued_at": "2026-03-01",
            "expires_at": null,
            "note": null,
            "source": "hr",
            "file": {
                "name": "contract.pdf",
                "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
            }
        }
    ],
    "checklists": [
        {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "title": "Onboarding: Aigerim Sadykova",
            "start_date": "2026-11-04",
            "status": "active",
            "employee": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "sections": [
                {
                    "key": "s1",
                    "title": "Before the first day"
                }
            ],
            "progress": {
                "done": 3,
                "total": 9,
                "overdue": 1
            },
            "items": [
                {
                    "id": 7001,
                    "section_key": "s1",
                    "title": "Prepare a uniform",
                    "description": null,
                    "assignee": {
                        "id": 120,
                        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                        "name": "Daniyar Omarov"
                    },
                    "due_date": "2026-11-03",
                    "overdue": true,
                    "status": "open",
                    "requires": "none",
                    "required": true,
                    "comment": null,
                    "has_file": false,
                    "done_by": null,
                    "done_at": null,
                    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
                }
            ],
            "created_at": "2026-10-30T12:00:00+05:00",
            "completed_at": null
        }
    ],
    "can": {
        "contacts": true,
        "edit": true,
        "card": true,
        "personal": true,
        "documents": true,
        "hire": true,
        "absences": true,
        "pay_view": true,
        "pay_edit": false
    }
}
```

### POST /hr/employees/{uuid}/rehire — Rehire a former employee

MCP tool: `hr_employees_rehire` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `hired_at` | body | string | yes |  |
| `position_id` | body | integer | no |  |
| `home_location_id` | body | integer | no |  |
| `location_ids` | body | array<integer> | no |  |
| `work_anywhere` | body | boolean | no |  |
| `manager_id` | body | integer | no |  |
| `template_id` | body | integer | no |  |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim",
        "lastname": "Sadykova",
        "email": "aigerim@example.com",
        "phone": "+7 701 111 22 33",
        "is_admin": false,
        "deleted": false,
        "deleted_at": null,
        "web_access": false,
        "position": {
            "id": 7,
            "name": "Barista"
        },
        "locations": [
            {
                "id": 12,
                "name": "Abay Ave",
                "kind": "home"
            }
        ],
        "home_location_id": 12,
        "work_anywhere": false,
        "teams": [
            {
                "id": 3,
                "name": "Bar",
                "is_head": false
            }
        ],
        "created_at": "2026-03-01T09:00:00+05:00",
        "last_seen_at": "2026-11-03T08:12:00+05:00",
        "url": "https://app.jecheck.io/ui/users/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b"
    },
    "status": {
        "status": "probation",
        "until": "2026-12-01",
        "absence_type": null,
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "onboarding": {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "done": 3,
            "total": 9,
            "overdue": 1
        }
    },
    "profile": {
        "middle_name": "Nurlanovna",
        "gender": "female",
        "birth_date": "1998-04-12",
        "birth_place": null,
        "citizenship": null,
        "marital_status": null,
        "children": null,
        "work_phone": null,
        "personal_phone": "+7 701 000 00 00",
        "personal_email": null,
        "address_actual": null,
        "address_registered": null,
        "tax_id": null,
        "insurance_no": null,
        "id_doc_type": null,
        "id_doc_number": null,
        "id_doc_issuer": null,
        "id_doc_issued_at": null,
        "id_doc_expires_at": null,
        "emergency_name": null,
        "emergency_relation": null,
        "emergency_phone": null,
        "bank_name": null,
        "bank_account": null,
        "education": null,
        "languages": null,
        "clothing_size": null,
        "shoe_size": null,
        "note": null,
        "custom": {}
    },
    "work": {
        "hired_at": "2026-09-01",
        "fired_at": null,
        "personnel_no": "0114",
        "employment": "full",
        "probation_until": "2026-12-01",
        "contract_type": "permanent",
        "contract_number": "TD-2026/114",
        "contract_date": "2026-09-01",
        "contract_until": null,
        "vacation_days": null,
        "vacation_adjust": 0,
        "manager": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "team_managers": [],
        "reports": []
    },
    "pay": {
        "pay_type": "hourly",
        "rate": 1500,
        "kpi_bonus": null,
        "kpi_bonus_type": "fixed",
        "currency": "KZT",
        "history": [
            {
                "from": "2026-09-01",
                "pay_type": "hourly",
                "rate": 1500
            }
        ]
    },
    "vacation": {
        "per_year": 24,
        "accrued": 4.1,
        "adjust": 0,
        "used": 0,
        "planned": 0,
        "pending": 0,
        "balance": 4.1,
        "available": 4.1
    },
    "absences": [
        {
            "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "type": "vacation",
            "date_from": "2026-12-01",
            "date_to": "2026-12-14",
            "days": 14,
            "status": "approved",
            "paid": true,
            "comment": null,
            "decision_note": null,
            "decided_by": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "decided_at": "2026-11-03T10:00:00+05:00",
            "has_file": false,
            "created_at": "2026-11-03T09:55:00+05:00"
        }
    ],
    "events": [
        {
            "id": 501,
            "type": "hire",
            "date": "2026-09-01",
            "data": {
                "position": "Barista"
            },
            "comment": null,
            "created_by": "Daniyar Omarov",
            "created_at": "2026-08-28T10:00:00+05:00"
        }
    ],
    "documents": [
        {
            "id": 88,
            "type": "contract",
            "title": "Employment contract",
            "number": "TD-2026/114",
            "issued_at": "2026-03-01",
            "expires_at": null,
            "note": null,
            "source": "hr",
            "file": {
                "name": "contract.pdf",
                "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
            }
        }
    ],
    "checklists": [
        {
            "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
            "kind": "onboarding",
            "title": "Onboarding: Aigerim Sadykova",
            "start_date": "2026-11-04",
            "status": "active",
            "employee": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "sections": [
                {
                    "key": "s1",
                    "title": "Before the first day"
                }
            ],
            "progress": {
                "done": 3,
                "total": 9,
                "overdue": 1
            },
            "items": [
                {
                    "id": 7001,
                    "section_key": "s1",
                    "title": "Prepare a uniform",
                    "description": null,
                    "assignee": {
                        "id": 120,
                        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                        "name": "Daniyar Omarov"
                    },
                    "due_date": "2026-11-03",
                    "overdue": true,
                    "status": "open",
                    "requires": "none",
                    "required": true,
                    "comment": null,
                    "has_file": false,
                    "done_by": null,
                    "done_at": null,
                    "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
                }
            ],
            "created_at": "2026-10-30T12:00:00+05:00",
            "completed_at": null
        }
    ],
    "can": {
        "contacts": true,
        "edit": true,
        "card": true,
        "personal": true,
        "documents": true,
        "hire": true,
        "absences": true,
        "pay_view": true,
        "pay_edit": false
    }
}
```

### POST /hr/employees/{uuid}/checklists — Start a checklist for an employee

Starts an onboarding or offboarding checklist from a template manually (tasks are created for the responsible people).

MCP tool: `hr_employees_checklists_start` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `template_id` | body | integer | yes |  |
| `date` | body | string | yes | First working day / last day |

Example response `data`:

```json
{
    "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
    "kind": "onboarding",
    "title": "Onboarding: Aigerim Sadykova",
    "start_date": "2026-11-04",
    "status": "active",
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "progress": {
        "done": 3,
        "total": 9,
        "overdue": 1
    },
    "items": [
        {
            "id": 7001,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "due_date": "2026-11-03",
            "overdue": true,
            "status": "open",
            "requires": "none",
            "required": true,
            "comment": null,
            "has_file": false,
            "done_by": null,
            "done_at": null,
            "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    ],
    "created_at": "2026-10-30T12:00:00+05:00",
    "completed_at": null
}
```

### POST /hr/employees/{uuid}/notes — Add a note to the HR history

MCP tool: `hr_employees_notes_create` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `date` | body | string | yes |  |
| `comment` | body | string | yes |  |
| `title` | body | string | no |  |

Example response `data`:

```json
{
    "id": 502,
    "type": "note",
    "date": "2026-11-03",
    "data": {
        "title": "Warning"
    },
    "comment": "Late three times this month",
    "created_by": "Daniyar Omarov",
    "created_at": "2026-11-03T10:00:00+05:00"
}
```

### DELETE /hr/employees/{uuid}/notes/{id} — Delete a note from the HR history

Only notes can be deleted; hiring and dismissal are undone by their own operations.

MCP tool: `hr_employees_notes_delete` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `id` | path | integer | yes | Event id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /hr/employees/{uuid}/documents — List HR documents of an employee

Contracts, orders, IDs, certificates (plus medical books from HACCP, read-only). Each view is written to the access log.

MCP tool: `hr_documents_list` · Permission: `hr.documents`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |

Example response `data`:

```json
[
    {
        "id": 88,
        "type": "contract",
        "title": "Employment contract",
        "number": "TD-2026/114",
        "issued_at": "2026-03-01",
        "expires_at": null,
        "note": null,
        "source": "hr",
        "file": {
            "name": "contract.pdf",
            "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
        }
    }
]
```

### POST /hr/employees/{uuid}/documents — Add an HR document

MCP tool: `hr_documents_create` · Permission: `hr.documents`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `type` | body | string (contract, addendum, order, id, medical, certificate, diploma, nda, consent, other) | yes |  |
| `title` | body | string | no |  |
| `number` | body | string | no |  |
| `issued_at` | body | string | no |  |
| `expires_at` | body | string | no |  |
| `note` | body | string | no |  |
| `file` | body | file | no | Scan: PDF, image, Office or TXT, up to 20 MB |

Example response `data`:

```json
{
    "id": 88,
    "type": "contract",
    "title": "Employment contract",
    "number": "TD-2026/114",
    "issued_at": "2026-03-01",
    "expires_at": null,
    "note": null,
    "source": "hr",
    "file": {
        "name": "contract.pdf",
        "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
    }
}
```

### PATCH /hr/employees/{uuid}/documents/{id} — Update an HR document

MCP tool: `hr_documents_update` · Permission: `hr.documents`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `id` | path | integer | yes | Document id |
| `type` | body | string (contract, addendum, order, id, medical, certificate, diploma, nda, consent, other) | no |  |
| `title` | body | string | no |  |
| `number` | body | string | no |  |
| `issued_at` | body | string | no |  |
| `expires_at` | body | string | no |  |
| `note` | body | string | no |  |
| `file` | body | file | no | Replace the scan |

Example response `data`:

```json
{
    "id": 88,
    "type": "contract",
    "title": "Employment contract",
    "number": "TD-2026/114",
    "issued_at": "2026-03-01",
    "expires_at": null,
    "note": null,
    "source": "hr",
    "file": {
        "name": "contract.pdf",
        "download_url": "https://app.jecheck.io/api/ext/v1/hr/employees/1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b/documents/88/file"
    }
}
```

### DELETE /hr/employees/{uuid}/documents/{id} — Delete an HR document

MCP tool: `hr_documents_delete` · Permission: `hr.documents`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `id` | path | integer | yes | Document id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /hr/employees/{uuid}/documents/{id}/file — Download an HR document file

Returns the file itself (REST only, not available as an MCP tool). The download is written to the access log.

MCP tool: `hr_documents_download` · Permission: `hr.documents`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Employee UUID |
| `id` | path | integer | yes | Document id |

Example response `data`:

```json
{
    "file": "binary"
}
```

### GET /hr/absences — List absences

Vacations, sick leave and other absences of employees of the locations of `hr.absences` (the same records as in the shift schedule). Overlapping the period when `date_from`/`date_to` are given.

MCP tool: `hr_absences_list` · Permission: `hr.absences`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `date_from` | query | string | no | Period start |
| `date_to` | query | string | no | Period end |
| `status` | query | array<string> | no | pending, approved, rejected, cancelled (default — pending and approved) |
| `type` | query | array<string> | no | Absence types: vacation, sick, day_off, unpaid, business_trip, study, maternity, truancy, other |
| `user_id` | query | integer | no | Only this employee |
| `location_ids` | query | array<integer> | no | Only employees of these locations |
| `updated_since` | query | string | no | Only records changed after this moment |

Example response `data`:

```json
[
    {
        "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "type": "vacation",
        "date_from": "2026-12-01",
        "date_to": "2026-12-14",
        "days": 14,
        "status": "approved",
        "paid": true,
        "comment": null,
        "decision_note": null,
        "decided_by": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "decided_at": "2026-11-03T10:00:00+05:00",
        "has_file": false,
        "created_at": "2026-11-03T09:55:00+05:00"
    }
]
```

### GET /hr/absences/balances — Get vacation balances

Annual vacation balance of each active employee of the locations of `hr.absences`: accrued since the hire date, used, planned, pending and available days.

MCP tool: `hr_absences_balances` · Permission: `hr.absences`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `location_ids` | query | array<integer> | no | Only employees of these locations |

Example response `data`:

```json
[
    {
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "hired_at": "2026-03-01",
        "per_year": 24,
        "accrued": 16.4,
        "adjust": 0,
        "used": 7,
        "planned": 14,
        "pending": 0,
        "balance": 9.4,
        "available": -4.6
    }
]
```

### POST /hr/absences — Add an absence

Records a vacation, sick leave etc. for an employee — approved immediately (requires `hr.absences` and `schedule.requests` on the employee's locations). With `release_shifts` the employee's shifts on those days become open shifts.

MCP tool: `hr_absences_create` · Permission: `hr.absences`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `user_id` | body | integer | yes | Employee |
| `type` | body | string (vacation, sick, day_off, unpaid, business_trip, study, maternity, truancy, other) | yes |  |
| `date_from` | body | string | yes |  |
| `date_to` | body | string | yes |  |
| `paid` | body | boolean | no | Default — depends on the type |
| `comment` | body | string | no |  |
| `release_shifts` | body | boolean | no |  |
| `file` | body | file | no | Sick note or order: PDF or image up to 10 MB |

Example response `data`:

```json
{
    "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "type": "vacation",
    "date_from": "2026-12-01",
    "date_to": "2026-12-14",
    "days": 14,
    "status": "approved",
    "paid": true,
    "comment": null,
    "decision_note": null,
    "decided_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "decided_at": "2026-11-03T10:00:00+05:00",
    "has_file": false,
    "created_at": "2026-11-03T09:55:00+05:00"
}
```

### PATCH /hr/absences/{uuid} — Update an absence

MCP tool: `hr_absences_update` · Permission: `hr.absences`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Absence UUID |
| `type` | body | string (vacation, sick, day_off, unpaid, business_trip, study, maternity, truancy, other) | no |  |
| `date_from` | body | string | no |  |
| `date_to` | body | string | no |  |
| `paid` | body | boolean | no |  |
| `comment` | body | string | no |  |
| `release_shifts` | body | boolean | no |  |
| `file` | body | file | no |  |

Example response `data`:

```json
{
    "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "type": "vacation",
    "date_from": "2026-12-01",
    "date_to": "2026-12-14",
    "days": 14,
    "status": "approved",
    "paid": true,
    "comment": null,
    "decision_note": null,
    "decided_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "decided_at": "2026-11-03T10:00:00+05:00",
    "has_file": false,
    "created_at": "2026-11-03T09:55:00+05:00"
}
```

### POST /hr/absences/{uuid}/decide — Approve or reject an absence request

Decision on a pending request submitted by an employee; the employee is notified.

MCP tool: `hr_absences_decide` · Permission: `hr.absences`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Absence UUID |
| `approve` | body | boolean | yes |  |
| `note` | body | string | no |  |
| `release_shifts` | body | boolean | no | Open the employee's shifts on those days |

Example response `data`:

```json
{
    "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "type": "vacation",
    "date_from": "2026-12-01",
    "date_to": "2026-12-14",
    "days": 14,
    "status": "approved",
    "paid": true,
    "comment": null,
    "decision_note": null,
    "decided_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "decided_at": "2026-11-03T10:00:00+05:00",
    "has_file": false,
    "created_at": "2026-11-03T09:55:00+05:00"
}
```

### POST /hr/absences/{uuid}/cancel — Cancel an absence

MCP tool: `hr_absences_cancel` · Permission: `hr.absences`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Absence UUID |

Example response `data`:

```json
{
    "uuid": "2b3c4d5e-6f70-4a81-92b3-c4d5e6f7a8b9",
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "type": "vacation",
    "date_from": "2026-12-01",
    "date_to": "2026-12-14",
    "days": 14,
    "status": "approved",
    "paid": true,
    "comment": null,
    "decision_note": null,
    "decided_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "decided_at": "2026-11-03T10:00:00+05:00",
    "has_file": false,
    "created_at": "2026-11-03T09:55:00+05:00"
}
```

### GET /hr/checklists — List onboarding and offboarding checklists

Active checklists and those finished in the last 90 days, for employees of the locations of `hr.hire`.

MCP tool: `hr_checklists_list` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `kind` | query | string (onboarding, offboarding) | no |  |
| `status` | query | string (active, completed, cancelled) | no |  |

Example response `data`:

```json
[
    {
        "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
        "kind": "onboarding",
        "title": "Onboarding: Aigerim Sadykova",
        "start_date": "2026-11-04",
        "status": "active",
        "employee": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "sections": [
            {
                "key": "s1",
                "title": "Before the first day"
            }
        ],
        "progress": {
            "done": 3,
            "total": 9,
            "overdue": 1
        },
        "items": [
            {
                "id": 7001,
                "section_key": "s1",
                "title": "Prepare a uniform",
                "description": null,
                "assignee": {
                    "id": 120,
                    "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                    "name": "Daniyar Omarov"
                },
                "due_date": "2026-11-03",
                "overdue": true,
                "status": "open",
                "requires": "none",
                "required": true,
                "comment": null,
                "has_file": false,
                "done_by": null,
                "done_at": null,
                "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
            }
        ],
        "created_at": "2026-10-30T12:00:00+05:00",
        "completed_at": null
    }
]
```

### GET /hr/checklists/{uuid} — Retrieve a checklist

MCP tool: `hr_checklists_get` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Checklist UUID |

Example response `data`:

```json
{
    "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
    "kind": "onboarding",
    "title": "Onboarding: Aigerim Sadykova",
    "start_date": "2026-11-04",
    "status": "active",
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "progress": {
        "done": 3,
        "total": 9,
        "overdue": 1
    },
    "items": [
        {
            "id": 7001,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "due_date": "2026-11-03",
            "overdue": true,
            "status": "open",
            "requires": "none",
            "required": true,
            "comment": null,
            "has_file": false,
            "done_by": null,
            "done_at": null,
            "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    ],
    "created_at": "2026-10-30T12:00:00+05:00",
    "completed_at": null
}
```

### POST /hr/checklists/{uuid}/items — Add a checklist item

MCP tool: `hr_checklists_items_create` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Checklist UUID |
| `title` | body | string | yes |  |
| `description` | body | string | no |  |
| `assignee_id` | body | integer | no |  |
| `due_date` | body | string | no |  |
| `section_key` | body | string | no |  |
| `requires` | body | string (none, comment, file) | no | What is required to complete the item |
| `create_task` | body | boolean | no | Create a task for the assignee |

Example response `data`:

```json
{
    "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
    "kind": "onboarding",
    "title": "Onboarding: Aigerim Sadykova",
    "start_date": "2026-11-04",
    "status": "active",
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "progress": {
        "done": 3,
        "total": 9,
        "overdue": 1
    },
    "items": [
        {
            "id": 7001,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "due_date": "2026-11-03",
            "overdue": true,
            "status": "open",
            "requires": "none",
            "required": true,
            "comment": null,
            "has_file": false,
            "done_by": null,
            "done_at": null,
            "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    ],
    "created_at": "2026-10-30T12:00:00+05:00",
    "completed_at": null
}
```

### PATCH /hr/checklist-items/{id} — Update a checklist item

MCP tool: `hr_checklists_items_update` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Item id |
| `title` | body | string | no |  |
| `description` | body | string | no |  |
| `assignee_id` | body | integer | no |  |
| `due_date` | body | string | no |  |
| `section_key` | body | string | no |  |
| `requires` | body | string (none, comment, file) | no |  |

Example response `data`:

```json
{
    "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
    "kind": "onboarding",
    "title": "Onboarding: Aigerim Sadykova",
    "start_date": "2026-11-04",
    "status": "active",
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "progress": {
        "done": 3,
        "total": 9,
        "overdue": 1
    },
    "items": [
        {
            "id": 7001,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "due_date": "2026-11-03",
            "overdue": true,
            "status": "open",
            "requires": "none",
            "required": true,
            "comment": null,
            "has_file": false,
            "done_by": null,
            "done_at": null,
            "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    ],
    "created_at": "2026-10-30T12:00:00+05:00",
    "completed_at": null
}
```

### POST /hr/checklist-items/{id}/status — Complete, skip or reopen a checklist item

MCP tool: `hr_checklists_items_status` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Item id |
| `action` | body | string (complete, skip, reopen) | yes |  |
| `comment` | body | string | no |  |
| `file` | body | file | no | Attachment when completing (if the item requires a file) |

Example response `data`:

```json
{
    "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
    "kind": "onboarding",
    "title": "Onboarding: Aigerim Sadykova",
    "start_date": "2026-11-04",
    "status": "active",
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "progress": {
        "done": 3,
        "total": 9,
        "overdue": 1
    },
    "items": [
        {
            "id": 7001,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "due_date": "2026-11-03",
            "overdue": true,
            "status": "open",
            "requires": "none",
            "required": true,
            "comment": null,
            "has_file": false,
            "done_by": null,
            "done_at": null,
            "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    ],
    "created_at": "2026-10-30T12:00:00+05:00",
    "completed_at": null
}
```

### DELETE /hr/checklist-items/{id} — Delete a checklist item

MCP tool: `hr_checklists_items_delete` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Item id |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /hr/checklists/{uuid}/cancel — Cancel a checklist

MCP tool: `hr_checklists_cancel` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Checklist UUID |

Example response `data`:

```json
{
    "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
    "kind": "onboarding",
    "title": "Onboarding: Aigerim Sadykova",
    "start_date": "2026-11-04",
    "status": "active",
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "progress": {
        "done": 3,
        "total": 9,
        "overdue": 1
    },
    "items": [
        {
            "id": 7001,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "due_date": "2026-11-03",
            "overdue": true,
            "status": "open",
            "requires": "none",
            "required": true,
            "comment": null,
            "has_file": false,
            "done_by": null,
            "done_at": null,
            "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    ],
    "created_at": "2026-10-30T12:00:00+05:00",
    "completed_at": null
}
```

### POST /hr/checklists/{uuid}/reschedule — Move the start date of a checklist

Moves the first working day / last day; due dates of open items shift by the same number of days.

MCP tool: `hr_checklists_reschedule` · Permission: `hr.hire`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Checklist UUID |
| `date` | body | string | yes |  |

Example response `data`:

```json
{
    "uuid": "4d5e6f70-8192-4a3b-8c4d-5e6f708192a3",
    "kind": "onboarding",
    "title": "Onboarding: Aigerim Sadykova",
    "start_date": "2026-11-04",
    "status": "active",
    "employee": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "progress": {
        "done": 3,
        "total": 9,
        "overdue": 1
    },
    "items": [
        {
            "id": 7001,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "due_date": "2026-11-03",
            "overdue": true,
            "status": "open",
            "requires": "none",
            "required": true,
            "comment": null,
            "has_file": false,
            "done_by": null,
            "done_at": null,
            "task_uuid": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        }
    ],
    "created_at": "2026-10-30T12:00:00+05:00",
    "completed_at": null
}
```

### GET /hr/templates — List checklist templates

MCP tool: `hr_templates_list` · Permission: `hr.hire` or `hr.onboarding`

Example response `data`:

```json
[
    {
        "id": 4,
        "kind": "onboarding",
        "name": "Barista onboarding",
        "description": null,
        "color": "#1f9d63",
        "position_ids": [
            7
        ],
        "is_default": true,
        "sections": [
            {
                "key": "s1",
                "title": "Before the first day"
            }
        ],
        "items": [],
        "items_count": 9,
        "used": 14
    }
]
```

### GET /hr/templates/{id} — Retrieve a checklist template

MCP tool: `hr_templates_get` · Permission: `hr.onboarding`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Template id |

Example response `data`:

```json
{
    "id": 4,
    "kind": "onboarding",
    "name": "Barista onboarding",
    "description": null,
    "color": "#1f9d63",
    "position_ids": [
        7
    ],
    "is_default": true,
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "items": [
        {
            "id": 31,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee_type": "location_manager",
            "assignee_id": null,
            "due_offset": -1,
            "requires": "none",
            "required": true,
            "create_task": true,
            "position_ids": []
        }
    ],
    "items_count": 1,
    "used": 14
}
```

### POST /hr/templates — Create a checklist template

Sections are stages ("Before the first day", "First week"…); items reference a section by `section_key`. Assignee types: `user` (assignee_id — user id), `position` (assignee_id — position id), `manager` (line manager), `location_manager`, `employee` (the employee themselves), `initiator` (who starts the checklist); `due_offset` — days from the start date (negative — before).

MCP tool: `hr_templates_create` · Permission: `hr.onboarding`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes |  |
| `kind` | body | string (onboarding, offboarding) | yes |  |
| `description` | body | string | no |  |
| `color` | body | string | no |  |
| `position_ids` | body | array<integer> | no | Used by default for these positions |
| `is_default` | body | boolean | no | Default template of its kind |
| `sections` | body | array<object> | yes |  |
| `items` | body | array<object> | no |  |

Example response `data`:

```json
{
    "id": 4,
    "kind": "onboarding",
    "name": "Barista onboarding",
    "description": null,
    "color": "#1f9d63",
    "position_ids": [
        7
    ],
    "is_default": true,
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "items": [
        {
            "id": 31,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee_type": "location_manager",
            "assignee_id": null,
            "due_offset": -1,
            "requires": "none",
            "required": true,
            "create_task": true,
            "position_ids": []
        }
    ],
    "items_count": 1,
    "used": 14
}
```

### PUT /hr/templates/{id} — Replace a checklist template

Saves the whole template (sections and all items). Running checklists keep their own copy of items.

MCP tool: `hr_templates_update` · Permission: `hr.onboarding`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Template id |
| `name` | body | string | yes |  |
| `kind` | body | string (onboarding, offboarding) | yes |  |
| `description` | body | string | no |  |
| `color` | body | string | no |  |
| `position_ids` | body | array<integer> | no | Used by default for these positions |
| `is_default` | body | boolean | no | Default template of its kind |
| `sections` | body | array<object> | yes |  |
| `items` | body | array<object> | no |  |

Example response `data`:

```json
{
    "id": 4,
    "kind": "onboarding",
    "name": "Barista onboarding",
    "description": null,
    "color": "#1f9d63",
    "position_ids": [
        7
    ],
    "is_default": true,
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "items": [
        {
            "id": 31,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee_type": "location_manager",
            "assignee_id": null,
            "due_offset": -1,
            "requires": "none",
            "required": true,
            "create_task": true,
            "position_ids": []
        }
    ],
    "items_count": 1,
    "used": 14
}
```

### POST /hr/templates/{id}/duplicate — Duplicate a checklist template

MCP tool: `hr_templates_duplicate` · Permission: `hr.onboarding`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Template id |

Example response `data`:

```json
{
    "id": 4,
    "kind": "onboarding",
    "name": "Barista onboarding",
    "description": null,
    "color": "#1f9d63",
    "position_ids": [
        7
    ],
    "is_default": true,
    "sections": [
        {
            "key": "s1",
            "title": "Before the first day"
        }
    ],
    "items": [
        {
            "id": 31,
            "section_key": "s1",
            "title": "Prepare a uniform",
            "description": null,
            "assignee_type": "location_manager",
            "assignee_id": null,
            "due_offset": -1,
            "requires": "none",
            "required": true,
            "create_task": true,
            "position_ids": []
        }
    ],
    "items_count": 1,
    "used": 14
}
```

### DELETE /hr/templates/{id} — Delete a checklist template

MCP tool: `hr_templates_delete` · Permission: `hr.onboarding`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Template id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /hr/settings — Get HR settings

Company defaults: annual vacation days, probation months, closing the account on dismissal, custom profile fields.

MCP tool: `hr_settings_get` · Permission: `hr.card` or `hr.onboarding`

Example response `data`:

```json
{
    "vacation_days": 24,
    "probation_months": 3,
    "block_on_dismissal": true,
    "custom_fields": [
        {
            "key": "locker",
            "label": "Locker no.",
            "type": "text",
            "options": []
        }
    ]
}
```

### PATCH /hr/settings — Update HR settings

MCP tool: `hr_settings_update` · Permission: `hr.onboarding`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `vacation_days` | body | integer | no |  |
| `probation_months` | body | integer | no |  |
| `block_on_dismissal` | body | boolean | no |  |
| `custom_fields` | body | array<object> | no | Replaces the custom field list |

Example response `data`:

```json
{
    "vacation_days": 24,
    "probation_months": 3,
    "block_on_dismissal": true,
    "custom_fields": []
}
```

## Instructions

Company knowledge base: folders, documents with published versions, read acknowledgements and editing. Visibility follows the document and folder permissions of the acting user ("who can view / who can edit"); content is returned as Markdown, HTML or plain text and accepted as Markdown or HTML.

### GET /instructions — List instructions

Instructions visible to the acting user, in library order. Readers get published documents; editors also get drafts and archived documents they can edit (filter with `status`). Read status (`viewed`, `acknowledged`, `needs_ack`) is for the acting user.

MCP tool: `instructions_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `folder_id` | query | integer | no | Only documents directly in this folder |
| `root` | query | boolean | no | Only documents in the library root (no folder) |
| `search` | query | string | no | Search in title, text, summary and tags |
| `status` | query | array<string> | no | Filter by status: draft, published, archived |
| `filter` | query | string (needs_ack, unread) | no | `needs_ack` — waiting for the acting user's acknowledgement; `unread` — never opened by the acting user |
| `tags` | query | array<string> | no | Only documents with any of these tags |
| `updated_since` | query | string | no | Only documents published (or, for editors, edited) after this moment — use for incremental sync |

Example response `data`:

```json
[
    {
        "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
        "title": "Opening the coffee shop",
        "icon": "coffee",
        "summary": "Morning checklist for baristas",
        "folder_id": 14,
        "status": "published",
        "version": 3,
        "published_at": "2026-10-28T09:12:00+05:00",
        "updated_at": "2026-10-28T09:12:00+05:00",
        "require_ack": true,
        "tags": [
            "Barista",
            "Opening"
        ],
        "words": 412,
        "viewed": true,
        "acknowledged": false,
        "needs_ack": true,
        "has_unpublished_changes": null,
        "can_edit": false,
        "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
    }
]
```

### GET /instructions/badge — Get my reading counters

Counters for the acting user over the published instructions they can read: total, never opened, and waiting for acknowledgement.

MCP tool: `instructions_badge`

Example response `data`:

```json
{
    "total": 42,
    "unread": 5,
    "needs_ack": 2
}
```

### GET /instructions/search — Search instructions

Full-text search over the instructions visible to the acting user (readers — published text; editors — also working titles). Returns up to `limit` matches with a text snippet. Legacy AI-bot keys keep the previous behaviour of this endpoint (see the instructions external API guide).

MCP tool: `instructions_search`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `q` | query | string | yes | Search query |
| `folder_id` | query | integer | no | Only documents in this folder and its subfolders |
| `limit` | query | integer | no | Maximum number of results |

Example response `data`:

```json
[
    {
        "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
        "title": "Opening the coffee shop",
        "icon": "coffee",
        "status": "published",
        "folder_id": 14,
        "snippet": "…calibrate the grinder before the first espresso…",
        "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
    }
]
```

### GET /instructions/tree — Get the library tree

The whole library in one call: the folder tree (with document counts and edit rights) and the list of documents visible to the acting user (see `instructions.list` for the item shape).

MCP tool: `instructions_tree`

Example response `data`:

```json
{
    "folders": [
        {
            "id": 14,
            "parent_id": 3,
            "name": "Barista",
            "icon": "coffee",
            "color": null,
            "count": 6,
            "total": 6,
            "can_edit": false,
            "restricted": false,
            "children": []
        }
    ],
    "documents": [
        {
            "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
            "title": "Opening the coffee shop",
            "icon": "coffee",
            "summary": "Morning checklist for baristas",
            "folder_id": 14,
            "status": "published",
            "version": 3,
            "published_at": "2026-10-28T09:12:00+05:00",
            "updated_at": "2026-10-28T09:12:00+05:00",
            "require_ack": true,
            "tags": [
                "Barista",
                "Opening"
            ],
            "words": 412,
            "viewed": true,
            "acknowledged": false,
            "needs_ack": true,
            "has_unpublished_changes": null,
            "can_edit": false,
            "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
        }
    ],
    "can_create_in_root": false
}
```

### GET /instructions/changes — List instruction changes

Change feed for mirrors and caches, oldest first: every document of the company changed after `updated_since`, with `state` = `active` (published and readable by the acting user) or `removed` (deleted, archived, unpublished or no longer visible). Store the last `updated_at` and pass it as `updated_since` next time.

MCP tool: `instructions_changes`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `updated_since` | query | string | no | Only changes after this moment; omit for a full snapshot |

Example response `data`:

```json
[
    {
        "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
        "state": "active",
        "version": 3,
        "updated_at": "2026-10-28T09:12:00+05:00"
    }
]
```

### GET /instructions/by-ref/{ref} — Find an instruction by external reference

Resolves an external reference (for example `confluence:12345` for pages imported from Confluence) to the instruction UUID — use it to redirect old links.

MCP tool: `instructions_by_ref`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `ref` | path | string | yes | External reference, e.g. `confluence:12345` |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### GET /instructions/{uuid} — Retrieve an instruction

The current published version of a document (or an older one with `version`) with its content in Markdown (default), HTML or plain text, the table of contents, attachments with download links and the acting user's acknowledgement status. Editors can request the unpublished draft with `draft=true`; a never-published document returns its draft. Pass `track=true` to count the call as a view by the acting user (as when the employee opens it in the app).

MCP tool: `instructions_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `format` | query | string (markdown, html, text) | no | Format of `content` |
| `version` | query | integer | no | Version number (default — current published version) |
| `draft` | query | boolean | no | Return the working draft (editors only) |
| `track` | query | boolean | no | Record a view of the current version by the acting user |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### GET /instructions/{uuid}/versions — List versions of an instruction

Published versions, newest first. Read an older version with `GET /instructions/{uuid}?version=N`.

MCP tool: `instructions_versions`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
[
    {
        "number": 3,
        "title": "Opening the coffee shop",
        "change_note": "New grinder settings",
        "is_major": true,
        "words": 412,
        "published_by": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "published_at": "2026-10-28T09:12:00+05:00",
        "is_latest": true
    }
]
```

### GET /instructions/{uuid}/diff — Compare versions

Block-level comparison of two versions: `from` (0 — empty document) and `to` (default — current version; editors may pass `draft`). Each block is HTML with `op` = `added`, `removed` or `same`.

MCP tool: `instructions_diff` · Plan feature: `instructions_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `from` | query | integer | yes | Version number to compare from (0 — empty) |
| `to` | query | string | no | Version number to compare to, or `draft` (editors only). Default — current version |

Example response `data`:

```json
{
    "from": 2,
    "to": "3",
    "blocks": [
        {
            "op": "removed",
            "html": "<p>Grind size 12</p>"
        },
        {
            "op": "added",
            "html": "<p>Grind size 10</p>"
        }
    ],
    "added": 1,
    "removed": 1
}
```

### GET /instructions/{uuid}/files — List instruction files

Images and attachments used in the current published version (editors: in the draft with `draft=true`) with download links.

MCP tool: `instructions_files_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `draft` | query | boolean | no | Files of the working draft (editors only) |

Example response `data`:

```json
[
    {
        "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "kind": "file",
        "name": "Cleaning schedule.pdf",
        "mime": "application/pdf",
        "size": 182044,
        "width": null,
        "height": null,
        "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
    }
]
```

### POST /instructions/{uuid}/acknowledge — Acknowledge an instruction

Records that the acting user has read the current version ("I have read it"). Required again after each major version when the document has `require_ack`.

MCP tool: `instructions_acknowledge`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
{
    "acknowledged": true,
    "version": 3
}
```

### GET /instructions/{uuid}/acknowledgements — Get the acknowledgement report

Who in the audience of the document has opened and acknowledged the current major version. Editors of the document only.

MCP tool: `instructions_acknowledgements` · Plan feature: `instructions_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
{
    "total": 18,
    "viewed": 15,
    "acknowledged": 12,
    "ack_rate": 67,
    "ack_since_version": 3,
    "users": [
        {
            "user": {
                "id": 120,
                "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
                "name": "Daniyar Omarov"
            },
            "position": "Barista",
            "last_viewed_at": "2026-10-29T08:01:00+05:00",
            "viewed_version": 3,
            "acknowledged_at": "2026-10-29T08:03:00+05:00",
            "source": "app"
        }
    ]
}
```

### POST /instructions/{uuid}/remind — Remind employees to read an instruction

Sends a push / in-app notification to everyone in the audience who has not acknowledged the current version yet.

MCP tool: `instructions_remind` · Plan feature: `instructions_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
{
    "reminded": 6
}
```

### POST /instructions — Create an instruction

Creates a draft in a folder where the acting user can edit (the library root — knowledge-base managers only). Content is converted to the editor format; pass `publish=true` to publish the first version right away (employees are notified).

MCP tool: `instructions_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `title` | body | string | yes | Title |
| `content` | body | string | no | Body text |
| `content_format` | body | string (markdown, html, text) | no | Format of `content`, default `markdown`. HTML is sanitised to text formatting, headings, lists, tables, quotes, code and links; add images and files with the upload operation |
| `template` | body | string (sop, opening, sanitation, equipment) | no | Start from a built-in template (ignored when `content` is passed) |
| `folder_id` | body | integer | no | Folder id (null — library root, managers only). Moving requires edit rights on the target folder |
| `icon` | body | string | no | Material Symbols icon name |
| `summary` | body | string | no | Short description shown in the list |
| `require_ack` | body | boolean | no | Employees must confirm they have read each major version |
| `review_every_days` | body | integer | no | Review period in days (null — no review reminders) |
| `owner_id` | body | integer | no | Responsible user id |
| `tags` | body | array<string> | no | Tags (replace the current ones) |
| `ai_visibility` | body | string (inherit, none, staff, client) | no | Visibility for the external AI knowledge base (only when the AI knowledge base is enabled for the company; managers only) |
| `ai_note` | body | string | no | Hint for the external AI (same conditions as `ai_visibility`) |
| `publish` | body | boolean | no | Publish the first version immediately |
| `change_note` | body | string | no | Version note when publishing |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### PATCH /instructions/{uuid} — Update an instruction

Edits the working draft (`title`, `content`) and document settings. Changes to title or content are saved to the draft and become visible to readers only after `publish`; settings (folder, tags, review period, acknowledgement) apply immediately. Moving to another folder requires edit rights on it.

MCP tool: `instructions_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `title` | body | string | no | Title (draft) |
| `content` | body | string | no | New body text, replaces the draft content |
| `content_format` | body | string (markdown, html, text) | no | Format of `content`, default `markdown`. HTML is sanitised to text formatting, headings, lists, tables, quotes, code and links; add images and files with the upload operation |
| `folder_id` | body | integer | no | Folder id (null — library root, managers only). Moving requires edit rights on the target folder |
| `icon` | body | string | no | Material Symbols icon name |
| `summary` | body | string | no | Short description shown in the list |
| `require_ack` | body | boolean | no | Employees must confirm they have read each major version |
| `review_every_days` | body | integer | no | Review period in days (null — no review reminders) |
| `owner_id` | body | integer | no | Responsible user id |
| `tags` | body | array<string> | no | Tags (replace the current ones) |
| `ai_visibility` | body | string (inherit, none, staff, client) | no | Visibility for the external AI knowledge base (only when the AI knowledge base is enabled for the company; managers only) |
| `ai_note` | body | string | no | Hint for the external AI (same conditions as `ai_visibility`) |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### POST /instructions/{uuid}/publish — Publish an instruction

Publishes the draft as a new version. A major version (default) requires a new acknowledgement and notifies employees; a minor one notifies only when `notify=true`.

MCP tool: `instructions_publish`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `change_note` | body | string | no | What changed |
| `is_major` | body | boolean | no | Major version, default true |
| `notify` | body | boolean | no | Notify employees (default — only for a major version) |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### POST /instructions/{uuid}/discard — Discard draft changes

Resets the draft to the current published version.

MCP tool: `instructions_discard`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### POST /instructions/{uuid}/restore — Restore an old version to the draft

Copies an older version into the draft; publish it to make it current again.

MCP tool: `instructions_restore` · Plan feature: `instructions_pro`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `version` | body | integer | yes | Version number to restore |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### POST /instructions/{uuid}/archive — Archive or unarchive an instruction

Archived documents are hidden from readers. Pass `archived=false` to return a document from the archive.

MCP tool: `instructions_archive`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `archived` | body | boolean | no | Default true |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### POST /instructions/{uuid}/duplicate — Duplicate an instruction

Creates a draft copy in the same folder with the same settings and permissions.

MCP tool: `instructions_duplicate`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### POST /instructions/{uuid}/reviewed — Mark an instruction as reviewed

Confirms the document is still up to date without changes: the next review date moves one period ahead. Only for published documents with a review period.

MCP tool: `instructions_reviewed`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
{
    "uuid": "5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a",
    "title": "Opening the coffee shop",
    "icon": "coffee",
    "summary": "Morning checklist for baristas",
    "status": "published",
    "folder": {
        "id": 14,
        "name": "Barista"
    },
    "folder_path": [
        {
            "id": 3,
            "name": "Operations"
        },
        {
            "id": 14,
            "name": "Barista"
        }
    ],
    "tags": [
        "Barista",
        "Opening"
    ],
    "version": 3,
    "is_latest": true,
    "is_draft": false,
    "published_at": "2026-10-28T09:12:00+05:00",
    "published_by": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "change_note": "New grinder settings",
    "is_major": true,
    "versions_count": 3,
    "format": "markdown",
    "content": "## Before opening\n\n1. Turn on the espresso machine\n2. Calibrate the grinder\n\n> **Important:** check fridge temperatures.",
    "toc": [
        {
            "id": "before-opening",
            "text": "Before opening",
            "level": 2
        }
    ],
    "words": 412,
    "read_minutes": 3,
    "require_ack": true,
    "acknowledged": false,
    "needs_ack": true,
    "attachments": [
        {
            "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
            "kind": "file",
            "name": "Cleaning schedule.pdf",
            "mime": "application/pdf",
            "size": 182044,
            "width": null,
            "height": null,
            "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
        }
    ],
    "can_edit": true,
    "has_unpublished_changes": false,
    "review_every_days": 180,
    "review_due_at": "2027-04-26",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "updated_at": "2026-10-28T09:12:00+05:00",
    "url": "https://app.jecheck.io/ui/instructions/5d3c2b1a-9f8e-4d7c-8b6a-5f4e3d2c1b0a"
}
```

### DELETE /instructions/{uuid} — Delete an instruction

MCP tool: `instructions_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /instructions/{uuid}/files — Upload a file to an instruction

Uploads an image (JPEG, PNG, WebP, GIF up to 15 MB) or a file (PDF, Office, TXT, CSV, ZIP, MP4, MOV up to 50 MB) and, by default, appends it to the end of the draft as an image or attachment block. Publish to show it to readers.

MCP tool: `instructions_files_upload`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `file` | body | file | yes |  |
| `append` | body | boolean | no | Append to the end of the draft, default true |

Example response `data`:

```json
{
    "uuid": "0c1d2e3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
    "kind": "file",
    "name": "Cleaning schedule.pdf",
    "mime": "application/pdf",
    "size": 182044,
    "width": null,
    "height": null,
    "url": "https://files.jecheck.io/instructions/761/2026/11/0c1d2e3f.pdf"
}
```

### GET /instructions/{uuid}/access — Get instruction permissions

Who can view and who can edit the document, plus folders above that restrict viewing and the inherited editors. Editors of the document only.

MCP tool: `instructions_access_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |

Example response `data`:

```json
{
    "view": [
        {
            "type": "position",
            "id": 7,
            "name": "Barista"
        },
        {
            "type": "department",
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "edit": [
        {
            "type": "user",
            "id": 120,
            "name": "Daniyar Omarov"
        }
    ],
    "restricted_by": [],
    "inherited_edit": {
        "folder": null,
        "subjects": []
    }
}
```

### PUT /instructions/{uuid}/access — Set instruction permissions

Replaces the view and/or edit lists of the document. Subjects: `user`, `position`, `department` (location), `team`. If the change would remove the acting user's own edit rights, they are kept as an editor.

MCP tool: `instructions_access_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Instruction UUID |
| `view` | body | array<object> | no | Who can view. Empty array — everyone who sees the parent folder; omit to keep unchanged |
| `edit` | body | array<object> | no | Who can edit. Empty array — inherit editors from the folder; omit to keep unchanged |

Example response `data`:

```json
{
    "view": [
        {
            "type": "position",
            "id": 7,
            "name": "Barista"
        },
        {
            "type": "department",
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "edit": [
        {
            "type": "user",
            "id": 120,
            "name": "Daniyar Omarov"
        }
    ],
    "restricted_by": [],
    "inherited_edit": {
        "folder": null,
        "subjects": []
    }
}
```

### GET /instructions/folders — List folders

Folder tree visible to the acting user with document counts (`count` — directly in the folder, `total` — with subfolders). Readers see only branches that contain readable documents.

MCP tool: `instructions_folders_list`

Example response `data`:

```json
[
    {
        "id": 14,
        "parent_id": 3,
        "name": "Barista",
        "icon": "coffee",
        "color": null,
        "count": 6,
        "total": 6,
        "can_edit": false,
        "restricted": false,
        "children": []
    }
]
```

### POST /instructions/folders — Create a folder

Creates a folder inside `parent_id` (requires edit rights on it) or in the root (knowledge-base managers only).

MCP tool: `instructions_folders_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes |  |
| `parent_id` | body | integer | no | Parent folder id |
| `icon` | body | string | no | Material Symbols icon name |

Example response `data`:

```json
{
    "id": 14,
    "parent_id": 3,
    "name": "Barista",
    "icon": "coffee",
    "color": null,
    "count": 6,
    "total": 6,
    "can_edit": false,
    "restricted": false,
    "children": []
}
```

### PATCH /instructions/folders/{id} — Update a folder

Rename, change the icon/colour or move the folder (`parent_id`; moving requires edit rights on both the old and the new parent).

MCP tool: `instructions_folders_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Folder id |
| `name` | body | string | no |  |
| `parent_id` | body | integer | no | New parent folder id (null — root) |
| `icon` | body | string | no |  |
| `color` | body | string | no |  |
| `ai_visibility` | body | string (inherit, none, staff, client) | no | Visibility for the external AI knowledge base (managers of companies with the AI knowledge base only) |

Example response `data`:

```json
{
    "id": 14,
    "parent_id": 3,
    "name": "Barista",
    "icon": "coffee",
    "color": null,
    "count": 6,
    "total": 6,
    "can_edit": false,
    "restricted": false,
    "children": []
}
```

### DELETE /instructions/folders/{id} — Delete a folder

Deletes the folder; its subfolders and documents move one level up and keep the folder's permissions.

MCP tool: `instructions_folders_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Folder id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /instructions/folders/{id}/access — Get folder permissions

Who can view and edit the folder (inherited by its documents and subfolders). Requires edit rights on the folder.

MCP tool: `instructions_folders_access_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Folder id |

Example response `data`:

```json
{
    "view": [
        {
            "type": "position",
            "id": 7,
            "name": "Barista"
        },
        {
            "type": "department",
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "edit": [
        {
            "type": "user",
            "id": 120,
            "name": "Daniyar Omarov"
        }
    ],
    "restricted_by": [],
    "inherited_edit": {
        "folder": null,
        "subjects": []
    }
}
```

### PUT /instructions/folders/{id}/access — Set folder permissions

Replaces the view and/or edit lists of the folder. Same subjects and self-protection rules as for documents.

MCP tool: `instructions_folders_access_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Folder id |
| `view` | body | array<object> | no | Who can view. Empty array — everyone who sees the parent folder; omit to keep unchanged |
| `edit` | body | array<object> | no | Who can edit. Empty array — inherit editors from the folder; omit to keep unchanged |

Example response `data`:

```json
{
    "view": [
        {
            "type": "position",
            "id": 7,
            "name": "Barista"
        },
        {
            "type": "department",
            "id": 12,
            "name": "Abay Ave"
        }
    ],
    "edit": [
        {
            "type": "user",
            "id": 120,
            "name": "Daniyar Omarov"
        }
    ],
    "restricted_by": [],
    "inherited_edit": {
        "folder": null,
        "subjects": []
    }
}
```

## Employee testing

Knowledge tests for employees: build tests and questions, assign them to people, positions or locations, read results, grade open answers — and take assigned tests as the acting user. Results are limited to employees of the locations of the `testing.results` / `testing.review` permissions.

### GET /testing/tests — List tests

All tests of the company with statistics limited to the locations of the `testing.results` permission.

MCP tool: `testing_tests_list` · Permission: `testing.manage` or `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `status` | query | array<string> | no | Filter by status: draft, published, archived |
| `search` | query | string | no | Search in title and category |
| `updated_since` | query | string | no | Only tests changed after this moment |

Example response `data`:

```json
[
    {
        "id": 58,
        "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
        "title": "Food safety basics",
        "description": "For new kitchen staff",
        "category": "Kitchen",
        "status": "published",
        "strict": false,
        "public_enabled": false,
        "time_limit_minutes": 20,
        "pass_score": 80,
        "questions_count": 15,
        "attempts": 42,
        "avg_percent": 84.5,
        "pass_rate": 76,
        "assigned": 30,
        "assigned_passed": 22,
        "overdue": 3,
        "pending_review": 1,
        "last_attempt_at": "2026-11-02T16:20:00+05:00",
        "updated_at": "2026-10-20T10:00:00+05:00",
        "url": "https://app.jecheck.io/ui/testing/58/results"
    }
]
```

### GET /testing/tests/{id} — Retrieve a test

Test settings. Sections with questions and correct answers (`sections`, `problems`) are returned only with `testing.manage`; otherwise they are `null`.

MCP tool: `testing_tests_get` · Permission: `testing.manage` or `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
        "enabled": false,
        "camera": true,
        "identity_photo": true,
        "snapshot_interval": 60,
        "fullscreen": true,
        "block_copy": true,
        "face_detection": true,
        "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
        {
            "id": 77,
            "title": "Storage",
            "pick_count": null,
            "questions": [
                {
                    "id": 901,
                    "section_id": 77,
                    "type": "single",
                    "body": "At what temperature must chilled food be stored?",
                    "media_url": null,
                    "options": {
                        "choices": [
                            {
                                "id": "a1b2c3d4",
                                "text": "0…+4 °C",
                                "correct": true
                            },
                            {
                                "id": "e5f6g7h8",
                                "text": "+8…+12 °C",
                                "correct": false
                            }
                        ]
                    },
                    "points": 1,
                    "partial_credit": false,
                    "is_critical": true,
                    "explanation": "SanPiN 2.3.6",
                    "problems": []
                }
            ]
        }
    ]
}
```

### POST /testing/tests — Create a test

Creates a draft test with one section. Optionally pass `questions` to fill it in one call, then `publish` it. 

MCP tool: `testing_tests_create` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `title` | body | string | yes | Test title |
| `description` | body | string | no |  |
| `category` | body | string | no |  |
| `cover_color` | body | string | no | Card colour, e.g. `#2563eb` |
| `time_limit_minutes` | body | integer | no | Time limit (null — unlimited) |
| `pass_score` | body | integer | no | Pass threshold, percent |
| `attempts_limit` | body | integer | no | Attempts per assignment cycle (null — unlimited) |
| `retry_cooldown_hours` | body | integer | no | Pause between attempts, hours |
| `shuffle_questions` | body | boolean | no |  |
| `shuffle_options` | body | boolean | no |  |
| `navigation` | body | string (free, linear) | no | `linear` — no going back to previous questions |
| `show_result` | body | string (none, score, answers, full) | no | What the employee sees after finishing |
| `allow_app` | body | boolean | no | Can be taken in the mobile app |
| `allow_web` | body | boolean | no | Can be taken on the website |
| `certificate_enabled` | body | boolean | no | Issue a certificate on passing |
| `certificate_valid_days` | body | integer | no | Certificate validity; employees are re-assigned when it expires |
| `public_enabled` | body | boolean | no | Open link for people without an account |
| `proctoring` | body | object | no | Strict mode settings (partial update) |
| `questions` | body | array<object> | no | Questions to add to the first section |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
        "enabled": false,
        "camera": true,
        "identity_photo": true,
        "snapshot_interval": 60,
        "fullscreen": true,
        "block_copy": true,
        "face_detection": true,
        "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
        {
            "id": 77,
            "title": "Storage",
            "pick_count": null,
            "questions": [
                {
                    "id": 901,
                    "section_id": 77,
                    "type": "single",
                    "body": "At what temperature must chilled food be stored?",
                    "media_url": null,
                    "options": {
                        "choices": [
                            {
                                "id": "a1b2c3d4",
                                "text": "0…+4 °C",
                                "correct": true
                            },
                            {
                                "id": "e5f6g7h8",
                                "text": "+8…+12 °C",
                                "correct": false
                            }
                        ]
                    },
                    "points": 1,
                    "partial_credit": false,
                    "is_critical": true,
                    "explanation": "SanPiN 2.3.6",
                    "problems": []
                }
            ]
        }
    ]
}
```

### PATCH /testing/tests/{id} — Update test settings

Partial update of the test settings. Enabling strict mode (`proctoring.enabled`) also enables taking the test on the website.

MCP tool: `testing_tests_update` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `title` | body | string | no | Test title |
| `description` | body | string | no |  |
| `category` | body | string | no |  |
| `cover_color` | body | string | no | Card colour, e.g. `#2563eb` |
| `time_limit_minutes` | body | integer | no | Time limit (null — unlimited) |
| `pass_score` | body | integer | no | Pass threshold, percent |
| `attempts_limit` | body | integer | no | Attempts per assignment cycle (null — unlimited) |
| `retry_cooldown_hours` | body | integer | no | Pause between attempts, hours |
| `shuffle_questions` | body | boolean | no |  |
| `shuffle_options` | body | boolean | no |  |
| `navigation` | body | string (free, linear) | no | `linear` — no going back to previous questions |
| `show_result` | body | string (none, score, answers, full) | no | What the employee sees after finishing |
| `allow_app` | body | boolean | no | Can be taken in the mobile app |
| `allow_web` | body | boolean | no | Can be taken on the website |
| `certificate_enabled` | body | boolean | no | Issue a certificate on passing |
| `certificate_valid_days` | body | integer | no | Certificate validity; employees are re-assigned when it expires |
| `public_enabled` | body | boolean | no | Open link for people without an account |
| `proctoring` | body | object | no | Strict mode settings (partial update) |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
        "enabled": false,
        "camera": true,
        "identity_photo": true,
        "snapshot_interval": 60,
        "fullscreen": true,
        "block_copy": true,
        "face_detection": true,
        "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
        {
            "id": 77,
            "title": "Storage",
            "pick_count": null,
            "questions": [
                {
                    "id": 901,
                    "section_id": 77,
                    "type": "single",
                    "body": "At what temperature must chilled food be stored?",
                    "media_url": null,
                    "options": {
                        "choices": [
                            {
                                "id": "a1b2c3d4",
                                "text": "0…+4 °C",
                                "correct": true
                            },
                            {
                                "id": "e5f6g7h8",
                                "text": "+8…+12 °C",
                                "correct": false
                            }
                        ]
                    },
                    "points": 1,
                    "partial_credit": false,
                    "is_critical": true,
                    "explanation": "SanPiN 2.3.6",
                    "problems": []
                }
            ]
        }
    ]
}
```

### POST /testing/tests/{id}/publish — Publish a test

Publishes the test so it can be assigned. Fails with `validation_failed` listing the problems (no questions, missing correct answers…).

MCP tool: `testing_tests_publish` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
        "enabled": false,
        "camera": true,
        "identity_photo": true,
        "snapshot_interval": 60,
        "fullscreen": true,
        "block_copy": true,
        "face_detection": true,
        "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
        {
            "id": 77,
            "title": "Storage",
            "pick_count": null,
            "questions": [
                {
                    "id": 901,
                    "section_id": 77,
                    "type": "single",
                    "body": "At what temperature must chilled food be stored?",
                    "media_url": null,
                    "options": {
                        "choices": [
                            {
                                "id": "a1b2c3d4",
                                "text": "0…+4 °C",
                                "correct": true
                            },
                            {
                                "id": "e5f6g7h8",
                                "text": "+8…+12 °C",
                                "correct": false
                            }
                        ]
                    },
                    "points": 1,
                    "partial_credit": false,
                    "is_critical": true,
                    "explanation": "SanPiN 2.3.6",
                    "problems": []
                }
            ]
        }
    ]
}
```

### POST /testing/tests/{id}/status — Change test status

Move a test to `draft`, `archived` or `published` (same checks as publish).

MCP tool: `testing_tests_status` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `status` | body | string (draft, published, archived) | yes |  |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
        "enabled": false,
        "camera": true,
        "identity_photo": true,
        "snapshot_interval": 60,
        "fullscreen": true,
        "block_copy": true,
        "face_detection": true,
        "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
        {
            "id": 77,
            "title": "Storage",
            "pick_count": null,
            "questions": [
                {
                    "id": 901,
                    "section_id": 77,
                    "type": "single",
                    "body": "At what temperature must chilled food be stored?",
                    "media_url": null,
                    "options": {
                        "choices": [
                            {
                                "id": "a1b2c3d4",
                                "text": "0…+4 °C",
                                "correct": true
                            },
                            {
                                "id": "e5f6g7h8",
                                "text": "+8…+12 °C",
                                "correct": false
                            }
                        ]
                    },
                    "points": 1,
                    "partial_credit": false,
                    "is_critical": true,
                    "explanation": "SanPiN 2.3.6",
                    "problems": []
                }
            ]
        }
    ]
}
```

### POST /testing/tests/{id}/public-link — Regenerate the public link

Issues a new open link for the test; the old one stops working.

MCP tool: `testing_tests_public_link` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
        "enabled": false,
        "camera": true,
        "identity_photo": true,
        "snapshot_interval": 60,
        "fullscreen": true,
        "block_copy": true,
        "face_detection": true,
        "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
        {
            "id": 77,
            "title": "Storage",
            "pick_count": null,
            "questions": [
                {
                    "id": 901,
                    "section_id": 77,
                    "type": "single",
                    "body": "At what temperature must chilled food be stored?",
                    "media_url": null,
                    "options": {
                        "choices": [
                            {
                                "id": "a1b2c3d4",
                                "text": "0…+4 °C",
                                "correct": true
                            },
                            {
                                "id": "e5f6g7h8",
                                "text": "+8…+12 °C",
                                "correct": false
                            }
                        ]
                    },
                    "points": 1,
                    "partial_credit": false,
                    "is_critical": true,
                    "explanation": "SanPiN 2.3.6",
                    "problems": []
                }
            ]
        }
    ]
}
```

### POST /testing/tests/{id}/duplicate — Duplicate a test

Creates a draft copy with all sections and questions.

MCP tool: `testing_tests_duplicate` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |

Example response `data`:

```json
{
    "id": 58,
    "uuid": "6a5b4c3d-2e1f-4a0b-9c8d-7e6f5a4b3c2d",
    "title": "Food safety basics",
    "description": "For new kitchen staff",
    "category": "Kitchen",
    "cover_color": "#2563eb",
    "status": "published",
    "time_limit_minutes": 20,
    "pass_score": 80,
    "attempts_limit": 3,
    "retry_cooldown_hours": 24,
    "shuffle_questions": true,
    "shuffle_options": true,
    "navigation": "free",
    "show_result": "answers",
    "allow_app": true,
    "allow_web": true,
    "certificate_enabled": true,
    "certificate_valid_days": 365,
    "public_enabled": false,
    "public_url": null,
    "strict": false,
    "proctoring": {
        "enabled": false,
        "camera": true,
        "identity_photo": true,
        "snapshot_interval": 60,
        "fullscreen": true,
        "block_copy": true,
        "face_detection": true,
        "max_violations": null
    },
    "questions_count": 15,
    "updated_at": "2026-10-20T10:00:00+05:00",
    "url": "https://app.jecheck.io/ui/testing/58/edit",
    "problems": [],
    "sections": [
        {
            "id": 77,
            "title": "Storage",
            "pick_count": null,
            "questions": [
                {
                    "id": 901,
                    "section_id": 77,
                    "type": "single",
                    "body": "At what temperature must chilled food be stored?",
                    "media_url": null,
                    "options": {
                        "choices": [
                            {
                                "id": "a1b2c3d4",
                                "text": "0…+4 °C",
                                "correct": true
                            },
                            {
                                "id": "e5f6g7h8",
                                "text": "+8…+12 °C",
                                "correct": false
                            }
                        ]
                    },
                    "points": 1,
                    "partial_credit": false,
                    "is_critical": true,
                    "explanation": "SanPiN 2.3.6",
                    "problems": []
                }
            ]
        }
    ]
}
```

### DELETE /testing/tests/{id} — Delete a test

Deletes the test and closes its assignments. Results of past attempts are kept.

MCP tool: `testing_tests_delete` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /testing/tests/{id}/analytics — Get test analytics

Summary of finished attempts (pass rate, average, score distribution), per-question difficulty, results by section and by location — for employees of the locations of `testing.results`.

MCP tool: `testing_tests_analytics` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `date_from` | query | string | no | Attempts submitted from |
| `date_to` | query | string | no | Attempts submitted to |

Example response `data`:

```json
{
    "summary": {
        "attempts": 42,
        "participants": 30,
        "pass_rate": 76,
        "avg_percent": 84.5,
        "median_duration": 960,
        "pending_review": 1,
        "low_trust": 0,
        "distribution": [
            {
                "from": 90,
                "to": 100,
                "count": 12
            }
        ],
        "pass_score": 80
    },
    "questions": [
        {
            "id": 901,
            "exists": true,
            "position": 1,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "section": "Storage",
            "answers": 42,
            "correct_rate": 64,
            "avg_time": 38,
            "discrimination": 0.31,
            "flag": null,
            "choices": [
                {
                    "text": "0…+4 °C",
                    "correct": true,
                    "share": 64
                }
            ],
            "top_wrong": null
        }
    ],
    "sections": [
        {
            "name": "Storage",
            "percent": 71
        }
    ],
    "locations": [
        {
            "id": 12,
            "name": "Abay Ave",
            "total": 10,
            "passed": 8,
            "completion": 80,
            "avg_percent": 85.2
        }
    ]
}
```

### POST /testing/tests/{id}/sections — Add a section

MCP tool: `testing_sections_create` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `title` | body | string | no |  |

Example response `data`:

```json
{
    "id": 78,
    "title": "Hygiene",
    "pick_count": null
}
```

### PATCH /testing/tests/{id}/sections/{section_id} — Update a section

Rename a section or set `pick_count` — how many random questions of the section each attempt gets (null — all).

MCP tool: `testing_sections_update` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `section_id` | path | integer | yes | Section id |
| `title` | body | string | no |  |
| `pick_count` | body | integer | no |  |

Example response `data`:

```json
{
    "id": 78,
    "title": "Hygiene",
    "pick_count": 5
}
```

### DELETE /testing/tests/{id}/sections/{section_id} — Delete a section

Questions of the deleted section move to another section.

MCP tool: `testing_sections_delete` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `section_id` | path | integer | yes | Section id |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /testing/tests/{id}/questions — Add a question

Adds a question to a section. Answer options by type: `single`/`multiple` — {"choices": [{"text": "…", "correct": true}]}; `true_false` — {"correct": true}; `text` — {"answers": ["…"], "case_sensitive": false}; `number` — {"value": 4, "tolerance": 0}; `ordering` — {"items": [{"text": "…"}]} in the correct order; `matching` — {"pairs": [{"left": "…", "right": "…"}]}; `essay` — {"min_words": 50, "rubric": "…"} (graded manually).

MCP tool: `testing_questions_create` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `type` | body | string (single, multiple, true_false, text, number, ordering, matching, essay) | yes | Question type |
| `section_id` | body | integer | no | Section (block) id; default — the first section |
| `body` | body | string | yes | Question text |
| `options` | body | object | no | Answer options by type: `single`/`multiple` — {"choices": [{"text": "…", "correct": true}]}; `true_false` — {"correct": true}; `text` — {"answers": ["…"], "case_sensitive": false}; `number` — {"value": 4, "tolerance": 0}; `ordering` — {"items": [{"text": "…"}]} in the correct order; `matching` — {"pairs": [{"left": "…", "right": "…"}]}; `essay` — {"min_words": 50, "rubric": "…"} (graded manually). |
| `points` | body | number | no | Points, default 1 |
| `partial_credit` | body | boolean | no | Partial points for multiple / ordering / matching |
| `is_critical` | body | boolean | no | A wrong answer fails the whole test |
| `explanation` | body | string | no | Shown to the employee after the test when `show_result` = `full` |
| `media_url` | body | string | no | Image URL (upload with `testing.tests.images.upload`) |

Example response `data`:

```json
{
    "id": 901,
    "section_id": 77,
    "type": "single",
    "body": "At what temperature must chilled food be stored?",
    "media_url": null,
    "options": {
        "choices": [
            {
                "id": "a1b2c3d4",
                "text": "0…+4 °C",
                "correct": true
            },
            {
                "id": "e5f6g7h8",
                "text": "+8…+12 °C",
                "correct": false
            }
        ]
    },
    "points": 1,
    "partial_credit": false,
    "is_critical": true,
    "explanation": "SanPiN 2.3.6",
    "problems": []
}
```

### POST /testing/tests/{id}/questions/batch — Add questions in bulk

Adds up to 100 questions to a section in one call; items without a known type or text are skipped.

MCP tool: `testing_questions_batch` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `section_id` | body | integer | no | Target section, default — the first one |
| `questions` | body | array<object> | yes |  |

Example response `data`:

```json
{
    "added": 10,
    "sections": [
        {
            "id": 77,
            "title": "Storage",
            "pick_count": null,
            "questions": [
                {
                    "id": 901,
                    "section_id": 77,
                    "type": "single",
                    "body": "At what temperature must chilled food be stored?",
                    "media_url": null,
                    "options": {
                        "choices": [
                            {
                                "id": "a1b2c3d4",
                                "text": "0…+4 °C",
                                "correct": true
                            },
                            {
                                "id": "e5f6g7h8",
                                "text": "+8…+12 °C",
                                "correct": false
                            }
                        ]
                    },
                    "points": 1,
                    "partial_credit": false,
                    "is_critical": true,
                    "explanation": "SanPiN 2.3.6",
                    "problems": []
                }
            ]
        }
    ]
}
```

### PATCH /testing/tests/{id}/questions/{question_id} — Update a question

Partial update. Changing `type` converts the options where possible.

MCP tool: `testing_questions_update` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `question_id` | path | integer | yes | Question id |
| `type` | body | string (single, multiple, true_false, text, number, ordering, matching, essay) | no | Question type |
| `section_id` | body | integer | no | Section (block) id; default — the first section |
| `body` | body | string | no | Question text |
| `options` | body | object | no | Answer options by type: `single`/`multiple` — {"choices": [{"text": "…", "correct": true}]}; `true_false` — {"correct": true}; `text` — {"answers": ["…"], "case_sensitive": false}; `number` — {"value": 4, "tolerance": 0}; `ordering` — {"items": [{"text": "…"}]} in the correct order; `matching` — {"pairs": [{"left": "…", "right": "…"}]}; `essay` — {"min_words": 50, "rubric": "…"} (graded manually). |
| `points` | body | number | no | Points, default 1 |
| `partial_credit` | body | boolean | no | Partial points for multiple / ordering / matching |
| `is_critical` | body | boolean | no | A wrong answer fails the whole test |
| `explanation` | body | string | no | Shown to the employee after the test when `show_result` = `full` |
| `media_url` | body | string | no | Image URL (upload with `testing.tests.images.upload`) |

Example response `data`:

```json
{
    "id": 901,
    "section_id": 77,
    "type": "single",
    "body": "At what temperature must chilled food be stored?",
    "media_url": null,
    "options": {
        "choices": [
            {
                "id": "a1b2c3d4",
                "text": "0…+4 °C",
                "correct": true
            },
            {
                "id": "e5f6g7h8",
                "text": "+8…+12 °C",
                "correct": false
            }
        ]
    },
    "points": 1,
    "partial_credit": false,
    "is_critical": true,
    "explanation": "SanPiN 2.3.6",
    "problems": []
}
```

### POST /testing/tests/{id}/questions/{question_id}/duplicate — Duplicate a question

MCP tool: `testing_questions_duplicate` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `question_id` | path | integer | yes | Question id |

Example response `data`:

```json
{
    "id": 901,
    "section_id": 77,
    "type": "single",
    "body": "At what temperature must chilled food be stored?",
    "media_url": null,
    "options": {
        "choices": [
            {
                "id": "a1b2c3d4",
                "text": "0…+4 °C",
                "correct": true
            },
            {
                "id": "e5f6g7h8",
                "text": "+8…+12 °C",
                "correct": false
            }
        ]
    },
    "points": 1,
    "partial_credit": false,
    "is_critical": true,
    "explanation": "SanPiN 2.3.6",
    "problems": []
}
```

### DELETE /testing/tests/{id}/questions/{question_id} — Delete a question

MCP tool: `testing_questions_delete` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `question_id` | path | integer | yes | Question id |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /testing/tests/{id}/images — Upload a question image

Uploads an image (up to 10 MB, converted to JPEG) and returns its URL for `media_url` of a question.

MCP tool: `testing_tests_images_upload` · Permission: `testing.manage` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `image` | body | file | yes |  |

Example response `data`:

```json
{
    "url": "https://files.jecheck.io/exams/761/questions/a8Kd02.jpg"
}
```

### GET /testing/tests/{id}/assignments — List assignments of a test

Assignments with progress and per-employee status. Only employees of the locations of `testing.results` are included.

MCP tool: `testing_assignments_list` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |

Example response `data`:

```json
[
    {
        "id": 140,
        "title": "Onboarding — October",
        "message": "Please pass by Friday",
        "test": {
            "id": 58,
            "title": "Food safety basics"
        },
        "status": "active",
        "starts_at": null,
        "due_at": "2026-11-07T18:00:00+05:00",
        "recur_days": 365,
        "recur_due_days": 14,
        "close_after_due": false,
        "auto_enroll": true,
        "created_by": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "created_at": "2026-10-31T09:00:00+05:00",
        "targets": [
            {
                "kind": "position",
                "id": 7
            }
        ],
        "progress": {
            "total": 12,
            "passed": 8,
            "failed": 1,
            "in_progress": 1,
            "review": 0,
            "not_started": 2,
            "overdue": 0,
            "completion": 67
        },
        "assignees": [
            {
                "id": 5012,
                "user": {
                    "id": 311,
                    "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                    "name": "Aigerim Sadykova"
                },
                "position": "Cook",
                "status": "passed",
                "overdue": false,
                "attempts": 1,
                "best_percent": 86.7,
                "best_attempt_uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
                "due_at": "2026-11-07T18:00:00+05:00",
                "completed_at": "2026-11-02T16:20:00+05:00",
                "valid_until": "2027-11-02T16:20:00+05:00",
                "cycle": 1,
                "link": "https://app.jecheck.io/exam/Xy7…"
            }
        ]
    }
]
```

### POST /testing/tests/{id}/assignments — Assign a test

Assigns a published test to employees, positions, locations or the whole company. Employees get a notification with a personal link; with `auto_enroll` new employees matching the targets are added automatically, with `recur_days` the test is re-assigned periodically. With a location-scoped permission only `user` and `department` targets within those locations are allowed.

MCP tool: `testing_assignments_create` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Test id |
| `targets` | body | array<object> | yes |  |
| `title` | body | string | no |  |
| `message` | body | string | no | Message to employees |
| `starts_at` | body | string | no | Open from (notifications are sent then) |
| `due_at` | body | string | no | Deadline (future) |
| `recur_days` | body | integer | no | Repeat every N days |
| `recur_due_days` | body | integer | no | Days to pass each repetition, default 14 |
| `close_after_due` | body | boolean | no | Close the test for an employee after the deadline |
| `auto_enroll` | body | boolean | no | Add new matching employees automatically, default true |
| `remind` | body | boolean | no | Send deadline reminders, default true |

Example response `data`:

```json
{
    "id": 140,
    "title": "Onboarding — October",
    "message": "Please pass by Friday",
    "test": {
        "id": 58,
        "title": "Food safety basics"
    },
    "status": "active",
    "starts_at": null,
    "due_at": "2026-11-07T18:00:00+05:00",
    "recur_days": 365,
    "recur_due_days": 14,
    "close_after_due": false,
    "auto_enroll": true,
    "created_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "created_at": "2026-10-31T09:00:00+05:00",
    "targets": [
        {
            "kind": "position",
            "id": 7
        }
    ],
    "progress": {
        "total": 12,
        "passed": 8,
        "failed": 1,
        "in_progress": 1,
        "review": 0,
        "not_started": 2,
        "overdue": 0,
        "completion": 67
    },
    "assignees": [
        {
            "id": 5012,
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "position": "Cook",
            "status": "passed",
            "overdue": false,
            "attempts": 1,
            "best_percent": 86.7,
            "best_attempt_uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
            "due_at": "2026-11-07T18:00:00+05:00",
            "completed_at": "2026-11-02T16:20:00+05:00",
            "valid_until": "2027-11-02T16:20:00+05:00",
            "cycle": 1,
            "link": "https://app.jecheck.io/exam/Xy7…"
        }
    ]
}
```

### GET /testing/assignments/{id} — Retrieve an assignment

MCP tool: `testing_assignments_get` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignment id |

Example response `data`:

```json
{
    "id": 140,
    "title": "Onboarding — October",
    "message": "Please pass by Friday",
    "test": {
        "id": 58,
        "title": "Food safety basics"
    },
    "status": "active",
    "starts_at": null,
    "due_at": "2026-11-07T18:00:00+05:00",
    "recur_days": 365,
    "recur_due_days": 14,
    "close_after_due": false,
    "auto_enroll": true,
    "created_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "created_at": "2026-10-31T09:00:00+05:00",
    "targets": [
        {
            "kind": "position",
            "id": 7
        }
    ],
    "progress": {
        "total": 12,
        "passed": 8,
        "failed": 1,
        "in_progress": 1,
        "review": 0,
        "not_started": 2,
        "overdue": 0,
        "completion": 67
    },
    "assignees": [
        {
            "id": 5012,
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "position": "Cook",
            "status": "passed",
            "overdue": false,
            "attempts": 1,
            "best_percent": 86.7,
            "best_attempt_uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
            "due_at": "2026-11-07T18:00:00+05:00",
            "completed_at": "2026-11-02T16:20:00+05:00",
            "valid_until": "2027-11-02T16:20:00+05:00",
            "cycle": 1,
            "link": "https://app.jecheck.io/exam/Xy7…"
        }
    ]
}
```

### PATCH /testing/assignments/{id} — Update an assignment

Close / reopen the assignment (`status`) or move the deadline for everyone who has not passed yet (`due_at`). Allowed to the author or with `testing.results` for all locations.

MCP tool: `testing_assignments_update` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignment id |
| `status` | body | string (active, closed) | no |  |
| `due_at` | body | string | no | New deadline |

Example response `data`:

```json
{
    "id": 140,
    "title": "Onboarding — October",
    "message": "Please pass by Friday",
    "test": {
        "id": 58,
        "title": "Food safety basics"
    },
    "status": "active",
    "starts_at": null,
    "due_at": "2026-11-07T18:00:00+05:00",
    "recur_days": 365,
    "recur_due_days": 14,
    "close_after_due": false,
    "auto_enroll": true,
    "created_by": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "created_at": "2026-10-31T09:00:00+05:00",
    "targets": [
        {
            "kind": "position",
            "id": 7
        }
    ],
    "progress": {
        "total": 12,
        "passed": 8,
        "failed": 1,
        "in_progress": 1,
        "review": 0,
        "not_started": 2,
        "overdue": 0,
        "completion": 67
    },
    "assignees": [
        {
            "id": 5012,
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "position": "Cook",
            "status": "passed",
            "overdue": false,
            "attempts": 1,
            "best_percent": 86.7,
            "best_attempt_uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
            "due_at": "2026-11-07T18:00:00+05:00",
            "completed_at": "2026-11-02T16:20:00+05:00",
            "valid_until": "2027-11-02T16:20:00+05:00",
            "cycle": 1,
            "link": "https://app.jecheck.io/exam/Xy7…"
        }
    ]
}
```

### POST /testing/assignments/{id}/sync — Add new matching employees

Adds employees who now match the targets (new hires in the position or location) and notifies them.

MCP tool: `testing_assignments_sync` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignment id |

Example response `data`:

```json
{
    "added": 2
}
```

### POST /testing/assignments/{id}/remind — Remind assignees

Sends a reminder to everyone who has not passed yet.

MCP tool: `testing_assignments_remind` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignment id |

Example response `data`:

```json
{
    "reminded": 4
}
```

### DELETE /testing/assignments/{id} — Delete an assignment

MCP tool: `testing_assignments_delete` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignment id |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /testing/assignees/{id}/restart — Reset attempts of an employee

Starts a new cycle for one employee of an assignment (previous attempts stay in history) and notifies them.

MCP tool: `testing_assignees_restart` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignee id (one employee in an assignment) |
| `due_at` | body | string | no | New personal deadline |

Example response `data`:

```json
{
    "ok": true
}
```

### POST /testing/assignees/{id}/remind — Remind an employee

MCP tool: `testing_assignees_remind` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignee id (one employee in an assignment) |

Example response `data`:

```json
{
    "ok": true
}
```

### PATCH /testing/assignees/{id} — Change the deadline of an employee

MCP tool: `testing_assignees_update` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignee id (one employee in an assignment) |
| `due_at` | body | string | yes | Personal deadline (null — none) |

Example response `data`:

```json
{
    "ok": true
}
```

### DELETE /testing/assignees/{id} — Remove an employee from an assignment

MCP tool: `testing_assignees_delete` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Assignee id (one employee in an assignment) |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /testing/attempts — List attempts

Attempts of employees of the locations of `testing.results` (guests of open links — only with the permission for all locations), newest first.

MCP tool: `testing_attempts_list` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `test_id` | query | integer | no | Only this test |
| `user_id` | query | integer | no | Only this employee |
| `location_ids` | query | array<integer> | no | Only employees of these locations |
| `status` | query | string (passed, failed, review, annulled, in_progress, flagged) | no | `flagged` — strict-mode attempts with a low trust score |
| `search` | query | string | no | Search by employee or guest name / email |
| `date_from` | query | string | no | Started from |
| `date_to` | query | string | no | Started to |
| `updated_since` | query | string | no | Only attempts changed after this moment — use for incremental sync |

Example response `data`:

```json
[
    {
        "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
        "test": {
            "id": 58,
            "title": "Food safety basics"
        },
        "user": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "guest": null,
        "channel": "app",
        "strict": false,
        "status": "completed",
        "percent": 86.7,
        "score": 13,
        "max_score": 15,
        "passed": true,
        "critical_failed": false,
        "finish_reason": "manual",
        "started_at": "2026-11-02T16:02:00+05:00",
        "submitted_at": "2026-11-02T16:20:00+05:00",
        "duration_sec": 1080,
        "violations": 0,
        "trust_score": null,
        "trust_level": null,
        "reviewed": false,
        "certificate_number": "JC-2026-000412",
        "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a"
    }
]
```

### GET /testing/attempts/{uuid} — Retrieve an attempt

Attempt with every question: the employee's answer, the correct answer, points and manual-review state. `can_review` tells whether the token may grade it.

MCP tool: `testing_attempts_get` · Permission: `testing.results` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Attempt UUID |

Example response `data`:

```json
{
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "test": {
        "id": 58,
        "title": "Food safety basics"
    },
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "guest": null,
    "channel": "app",
    "strict": false,
    "status": "completed",
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "finish_reason": "manual",
    "started_at": "2026-11-02T16:02:00+05:00",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "violations": 0,
    "trust_score": null,
    "trust_level": null,
    "reviewed": false,
    "certificate_number": "JC-2026-000412",
    "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "review_note": null,
    "can_review": true,
    "answers": [
        {
            "question_id": 901,
            "n": 1,
            "section": "Storage",
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "your_answer": "0…+4 °C",
            "correct_answer": "0…+4 °C",
            "is_correct": true,
            "needs_review": false,
            "points": 1,
            "max_points": 1,
            "is_critical": true,
            "feedback": null,
            "rubric": null,
            "time_spent_sec": 41,
            "graded_manually": false
        }
    ]
}
```

### POST /testing/attempts/{uuid}/grade — Grade an open answer

Sets points (and optional feedback) for a question, usually an essay waiting for manual review. When all answers are graded the attempt gets its final result and the employee is notified.

MCP tool: `testing_attempts_grade` · Permission: `testing.review` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Attempt UUID |
| `question_id` | body | integer | yes |  |
| `points` | body | number | yes |  |
| `feedback` | body | string | no |  |

Example response `data`:

```json
{
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "test": {
        "id": 58,
        "title": "Food safety basics"
    },
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "guest": null,
    "channel": "app",
    "strict": false,
    "status": "completed",
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "finish_reason": "manual",
    "started_at": "2026-11-02T16:02:00+05:00",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "violations": 0,
    "trust_score": null,
    "trust_level": null,
    "reviewed": false,
    "certificate_number": "JC-2026-000412",
    "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "review_note": null,
    "can_review": true,
    "answers": [
        {
            "question_id": 901,
            "n": 1,
            "section": "Storage",
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "your_answer": "0…+4 °C",
            "correct_answer": "0…+4 °C",
            "is_correct": true,
            "needs_review": false,
            "points": 1,
            "max_points": 1,
            "is_critical": true,
            "feedback": null,
            "rubric": null,
            "time_spent_sec": 41,
            "graded_manually": false
        }
    ]
}
```

### POST /testing/attempts/{uuid}/annul — Annul an attempt

Cancels the result (e.g. cheating in strict mode); the employee can take the test again.

MCP tool: `testing_attempts_annul` · Permission: `testing.review` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Attempt UUID |
| `note` | body | string | yes | Reason |

Example response `data`:

```json
{
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "test": {
        "id": 58,
        "title": "Food safety basics"
    },
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "guest": null,
    "channel": "app",
    "strict": false,
    "status": "completed",
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "finish_reason": "manual",
    "started_at": "2026-11-02T16:02:00+05:00",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "violations": 0,
    "trust_score": null,
    "trust_level": null,
    "reviewed": false,
    "certificate_number": "JC-2026-000412",
    "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "review_note": null,
    "can_review": true,
    "answers": [
        {
            "question_id": 901,
            "n": 1,
            "section": "Storage",
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "your_answer": "0…+4 °C",
            "correct_answer": "0…+4 °C",
            "is_correct": true,
            "needs_review": false,
            "points": 1,
            "max_points": 1,
            "is_critical": true,
            "feedback": null,
            "rubric": null,
            "time_spent_sec": 41,
            "graded_manually": false
        }
    ]
}
```

### POST /testing/attempts/{uuid}/confirm — Confirm an attempt result

Marks the attempt as reviewed (e.g. after checking a strict-mode recording).

MCP tool: `testing_attempts_confirm` · Permission: `testing.review` · Plan feature: `testing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Attempt UUID |
| `note` | body | string | no |  |

Example response `data`:

```json
{
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "test": {
        "id": 58,
        "title": "Food safety basics"
    },
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "guest": null,
    "channel": "app",
    "strict": false,
    "status": "completed",
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "finish_reason": "manual",
    "started_at": "2026-11-02T16:02:00+05:00",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "violations": 0,
    "trust_score": null,
    "trust_level": null,
    "reviewed": false,
    "certificate_number": "JC-2026-000412",
    "url": "https://app.jecheck.io/ui/testing/attempts/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "review_note": null,
    "can_review": true,
    "answers": [
        {
            "question_id": 901,
            "n": 1,
            "section": "Storage",
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "your_answer": "0…+4 °C",
            "correct_answer": "0…+4 °C",
            "is_correct": true,
            "needs_review": false,
            "points": 1,
            "max_points": 1,
            "is_critical": true,
            "feedback": null,
            "rubric": null,
            "time_spent_sec": 41,
            "graded_manually": false
        }
    ]
}
```

### GET /testing/my — List my assigned tests

Tests assigned to the acting user with availability: `can_start`, `reason`, `open_attempt`, `web_only` (strict-mode tests are taken on the website by `url`).

MCP tool: `testing_my_list`

Example response `data`:

```json
[
    {
        "assignee_id": 5012,
        "title": "Food safety basics",
        "description": "For new kitchen staff",
        "category": "Kitchen",
        "message": "Please pass by Friday",
        "status": "assigned",
        "overdue": false,
        "due_at": "2026-11-07T18:00:00+05:00",
        "starts_at": null,
        "valid_until": null,
        "best_percent": null,
        "pass_score": 80,
        "questions_count": 15,
        "time_limit_minutes": 20,
        "strict": false,
        "web_only": false,
        "can_start": true,
        "reason": null,
        "open_attempt": null,
        "attempts_left": 3,
        "last_attempt": null,
        "url": "https://app.jecheck.io/exam/Xy7…"
    }
]
```

### GET /testing/my/history — List my finished attempts

MCP tool: `testing_my_history`

Example response `data`:

```json
[
    {
        "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
        "title": "Food safety basics",
        "status": "completed",
        "submitted_at": "2026-11-02T16:20:00+05:00",
        "percent": 86.7,
        "passed": true,
        "certificate_number": "JC-2026-000412"
    }
]
```

### POST /testing/my/start — Start or continue my test

Starts an attempt of an assigned test as the acting user (or returns the open one) and returns questions without correct answers. Strict-mode and website-only tests return `409 web_only` with `web_url` to open in a browser.

MCP tool: `testing_my_start`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `assignee_id` | body | integer | yes | `assignee_id` from `testing.my.list` |

Example response `data`:

```json
{
    "attempt": {
        "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
        "status": "in_progress",
        "channel": "app",
        "strict": false,
        "started_at": "2026-11-02T16:02:00+05:00",
        "deadline_at": "2026-11-02T16:22:00+05:00",
        "remaining_sec": 1150,
        "current_index": 0,
        "violations": 0,
        "identity_photo": false
    },
    "test": {
        "title": "Food safety basics",
        "description": "For new kitchen staff",
        "navigation": "free",
        "pass_score": 80,
        "time_limit_minutes": 20,
        "questions_count": 15,
        "max_score": 15,
        "proctoring": null
    },
    "questions": [
        {
            "id": 901,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "points": 1,
            "section": "Storage",
            "multiple_hint": null,
            "choices": [
                {
                    "id": "a1b2c3d4",
                    "text": "0…+4 °C"
                },
                {
                    "id": "e5f6g7h8",
                    "text": "+8…+12 °C"
                }
            ],
            "response": null,
            "flagged": false
        }
    ]
}
```

### GET /testing/my/attempts/{uuid} — Get my attempt state

Questions, saved answers and the remaining time of an attempt of the acting user.

MCP tool: `testing_my_attempts_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Attempt UUID |

Example response `data`:

```json
{
    "attempt": {
        "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
        "status": "in_progress",
        "channel": "app",
        "strict": false,
        "started_at": "2026-11-02T16:02:00+05:00",
        "deadline_at": "2026-11-02T16:22:00+05:00",
        "remaining_sec": 1150,
        "current_index": 0,
        "violations": 0,
        "identity_photo": false
    },
    "test": {
        "title": "Food safety basics",
        "description": "For new kitchen staff",
        "navigation": "free",
        "pass_score": 80,
        "time_limit_minutes": 20,
        "questions_count": 15,
        "max_score": 15,
        "proctoring": null
    },
    "questions": [
        {
            "id": 901,
            "type": "single",
            "body": "At what temperature must chilled food be stored?",
            "media_url": null,
            "points": 1,
            "section": "Storage",
            "multiple_hint": null,
            "choices": [
                {
                    "id": "a1b2c3d4",
                    "text": "0…+4 °C"
                },
                {
                    "id": "e5f6g7h8",
                    "text": "+8…+12 °C"
                }
            ],
            "response": null,
            "flagged": false
        }
    ]
}
```

### POST /testing/my/attempts/{uuid}/answer — Save an answer

Saves the answer to one question. `response` by type: single {"choice": "<id>"}, multiple {"choices": ["<id>"]}, true_false {"value": true}, text/essay {"text": "…"}, number {"value": 4}, ordering {"order": ["<id>", …]}, matching {"pairs": {"<left id>": "<right id>"}}. Pass `flagged` alone to mark the question for later.

MCP tool: `testing_my_attempts_answer`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Attempt UUID |
| `question_id` | body | integer | yes |  |
| `response` | body | object | no | Answer, see the description |
| `time_spent_sec` | body | integer | no |  |
| `flagged` | body | boolean | no |  |

Example response `data`:

```json
{
    "saved": true,
    "answered": true,
    "remaining_sec": 1120
}
```

### POST /testing/my/attempts/{uuid}/finish — Finish my attempt

Submits the attempt and returns the result in the scope allowed by the test (`show`).

MCP tool: `testing_my_attempts_finish`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Attempt UUID |

Example response `data`:

```json
{
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "title": "Food safety basics",
    "status": "completed",
    "show": "score",
    "finish_reason": "manual",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "pass_score": 80,
    "questions_count": 15,
    "certificate_number": "JC-2026-000412",
    "pdf_url": "https://app.jecheck.io/exam/a/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a/pdf",
    "retry": null,
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "correct_count": 13
}
```

### GET /testing/my/attempts/{uuid}/result — Get my attempt result

MCP tool: `testing_my_attempts_result`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Attempt UUID |

Example response `data`:

```json
{
    "uuid": "8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a",
    "title": "Food safety basics",
    "status": "completed",
    "show": "score",
    "finish_reason": "manual",
    "submitted_at": "2026-11-02T16:20:00+05:00",
    "duration_sec": 1080,
    "pass_score": 80,
    "questions_count": 15,
    "certificate_number": "JC-2026-000412",
    "pdf_url": "https://app.jecheck.io/exam/a/8c7b6a59-4e3d-4c2b-9a19-0f8e7d6c5b4a/pdf",
    "retry": null,
    "percent": 86.7,
    "score": 13,
    "max_score": 15,
    "passed": true,
    "critical_failed": false,
    "correct_count": 13
}
```

## Mystery shopper

Mystery shopper visits without an app: create visits with a personal link for a guest, review submitted questionnaires with automatic fraud flags, accept a visit into a regular audit, manage the guest pool and payouts, and order turnkey checks performed by jeCheck guests.

### GET /mystery/visits — List mystery shopper visits

Visits of the company, newest first. The guest link (`guest_url`) is returned with `mystery.manage`; payouts — with `mystery.payouts` (logged). Turnkey visits (`managed`) hide the guest.

MCP tool: `mystery_visits_list` · Permission: `mystery.view` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `status` | query | array<string> | no | scheduled, opened, in_progress, submitted, returned, approved, rejected, cancelled, expired |
| `location_ids` | query | array<integer> | no | Only these locations |
| `checklist_id` | query | integer | no | Only this questionnaire |
| `shopper_id` | query | integer | no | Only this guest |
| `wave` | query | string | no | Only this wave |
| `managed` | query | boolean | no | `true` — only turnkey visits, `false` — only own visits |
| `flagged` | query | boolean | no | Only submitted visits with serious fraud flags |
| `date_from` | query | string | no | Submitted from |
| `date_to` | query | string | no | Submitted to |
| `updated_since` | query | string | no | Only visits changed after this moment — use for incremental sync |

Example response `data`:

```json
[
    {
        "id": 2041,
        "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
        "status": "submitted",
        "managed": false,
        "order_uuid": null,
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "checklist": {
            "id": 310,
            "name": "Guest experience"
        },
        "shopper": {
            "id": 55,
            "name": "Madina K."
        },
        "wave": "November",
        "window_from": "2026-11-01T09:00:00+05:00",
        "window_to": "2026-11-07T21:00:00+05:00",
        "score": 87.5,
        "level": "warning",
        "submitted_at": "2026-11-03T13:40:00+05:00",
        "created_at": "2026-10-30T10:00:00+05:00",
        "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
        "audit_uuid": null,
        "payout_status": "pending",
        "payout_amount": 4500,
        "updated_at": "2026-11-03T13:40:00+05:00",
        "url": "https://app.jecheck.io/ui/mystery/visits/2041"
    }
]
```

### GET /mystery/visits/{id} — Retrieve a visit

Full questionnaire: answers with per-question results, timing, guest report, geolocation vs the location, fraud flags, AI check, files (receipt, photos, audio — private media via `mystery.media.get`), history. Turnkey visits are available after jeCheck approves them (`409 managed_visit` before).

MCP tool: `mystery_visits_get` · Permission: `mystery.view` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits — Create visits

Creates one visit per location for a questionnaire (checklist). Each visit gets a personal guest link (`guest_url`) that works without the app; with `shopper_id` and `send_email` the guest receives it by email.

MCP tool: `mystery_visits_create` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `checklist_id` | body | integer | yes | Questionnaire (checklist) |
| `location_ids` | body | array<integer> | yes |  |
| `shopper_id` | body | integer | no | Guest from the pool |
| `wave` | body | string | no |  |
| `brief` | body | string | no | Task for the guest |
| `legend` | body | string | no | Cover story |
| `window_from` | body | string | no |  |
| `window_to` | body | string | no | Deadline (after `window_from`) |
| `send_email` | body | boolean | no | Email the link to the guest |
| `settings` | body | object | no | Questionnaire requirements and anti-fraud checks |

Example response `data`:

```json
[
    {
        "id": 2041,
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
        "invited": true
    }
]
```

### PATCH /mystery/visits/{id} — Update a visit

Change the brief, cover story, time window or guest of an open visit. The guest cannot be changed after the visit has started.

MCP tool: `mystery_visits_update` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |
| `brief` | body | string | no |  |
| `legend` | body | string | no |  |
| `window_from` | body | string | no |  |
| `window_to` | body | string | no |  |
| `shopper_id` | body | integer | no |  |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/approve — Accept a visit

Accepts a submitted questionnaire: a regular audit is created from it (reports, mailings, corrective tasks follow as for any audit). Returns the visit with `audit_uuid`.

MCP tool: `mystery_visits_approve` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |
| `note` | body | string | no | Comment |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/return — Return a visit to the guest

Sends a submitted questionnaire back for corrections with a comment; the guest gets the link again.

MCP tool: `mystery_visits_return` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |
| `note` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/reject — Reject a visit

Rejects a submitted questionnaire (e.g. fraud); no payout.

MCP tool: `mystery_visits_reject` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |
| `note` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/cancel — Cancel a visit

Cancels a visit that has not been submitted; the guest link stops working.

MCP tool: `mystery_visits_cancel` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/reopen — Reopen a visit

Reopens a cancelled or expired visit with a new deadline.

MCP tool: `mystery_visits_reopen` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |
| `window_to` | body | string | no | New deadline (future) |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/invite — Send the link to the guest

Emails the questionnaire link to the guest of the visit (requires a guest with an email).

MCP tool: `mystery_visits_invite` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/recheck — Re-run fraud checks

Recalculates automatic flags: geolocation, time on site, receipt, photo metadata, duplicates.

MCP tool: `mystery_visits_recheck` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/ai-review — Run the AI review of a visit

Transcribes audio and asks the AI to check the questionnaire for consistency (requires the AI feature for the company). The result is in `ai_check` of the visit.

MCP tool: `mystery_visits_ai_review` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### POST /mystery/visits/{id}/payout — Set the payout of a visit

Payout status and amount for the guest (receipt reimbursement and fee). Requires `mystery.payouts`.

MCP tool: `mystery_visits_payout` · Permission: `mystery.payouts` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |
| `status` | body | string (none, pending, paid) | yes |  |
| `amount` | body | number | no |  |

Example response `data`:

```json
{
    "id": 2041,
    "uuid": "9e8d7c6b-5a49-4382-9716-05f4e3d2c1b0",
    "status": "submitted",
    "managed": false,
    "order_uuid": null,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "checklist": {
        "id": 310,
        "name": "Guest experience"
    },
    "shopper": {
        "id": 55,
        "name": "Madina K."
    },
    "wave": "November",
    "window_from": "2026-11-01T09:00:00+05:00",
    "window_to": "2026-11-07T21:00:00+05:00",
    "score": 87.5,
    "level": "warning",
    "submitted_at": "2026-11-03T13:40:00+05:00",
    "created_at": "2026-10-30T10:00:00+05:00",
    "guest_url": "https://app.jecheck.io/mg/Ab12Cd34Ef56Gh78Ij90Kl12",
    "audit_uuid": null,
    "payout_status": "pending",
    "payout_amount": 4500,
    "updated_at": "2026-11-03T13:40:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/visits/2041",
    "address": "12 Abay Ave",
    "brief": "Order a cappuccino and a dessert",
    "legend": "Ask about vegan options",
    "settings": {
        "require_receipt": true,
        "require_geo": true,
        "require_timing": true,
        "require_staff": true,
        "require_audio": false,
        "require_place_photos": false,
        "review_required": true,
        "min_minutes": 10,
        "geo_radius": 300,
        "receipt_limit": 5000,
        "currency": "KZT"
    },
    "shopper_contacts": {
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00"
    },
    "answers": {
        "8812": {
            "v": 1,
            "c": "Greeted within 10 seconds"
        }
    },
    "results": {
        "8812": {
            "r": 1,
            "t": "Yes"
        }
    },
    "timing": {
        "arrived": "2026-11-03T13:05:00+05:00",
        "left": "2026-11-03T13:31:00+05:00"
    },
    "report": {
        "staff_name": "Aliya",
        "receipt_amount": 4500,
        "summary": "Friendly service, long wait for dessert"
    },
    "geo_start": {
        "lat": 43.2389,
        "lng": 76.9455,
        "acc": 12
    },
    "geo_end": null,
    "geo_ref": {
        "lat": 43.2391,
        "lng": 76.9452
    },
    "distance_m": 35,
    "duration_sec": 1560,
    "flags": [
        {
            "code": "short_visit",
            "level": "warning",
            "text": "The visit took less than the minimum time"
        }
    ],
    "sections_score": [
        {
            "id": 101,
            "name": "Service",
            "score": 90
        }
    ],
    "ai_check": null,
    "review_note": null,
    "reviewer": null,
    "reviewed_at": null,
    "returned_count": 0,
    "audit": null,
    "invited_at": "2026-10-30T10:01:00+05:00",
    "opened_at": "2026-11-03T12:58:00+05:00",
    "started_at": "2026-11-03T13:02:00+05:00",
    "form": [
        {
            "id": 101,
            "name": "Service",
            "hint": null,
            "questions": [
                {
                    "id": 8812,
                    "section_id": 101,
                    "name": "Were you greeted?",
                    "hint": null,
                    "type": "yes_no",
                    "settings": null,
                    "weight": 1,
                    "can_skip": false,
                    "photo": false
                }
            ]
        }
    ],
    "files": [
        {
            "id": 9001,
            "question_id": null,
            "kind": "receipt",
            "url": "https://files.jecheck.io/mystery/761/receipt.jpg",
            "taken_at": "2026-11-03T13:30:00+05:00",
            "mime": "image/jpeg",
            "name": null,
            "duration": null,
            "size": 182044,
            "transcript": null
        }
    ],
    "events": [
        {
            "type": "submitted",
            "at": "2026-11-03T13:40:00+05:00",
            "user": null,
            "note": null
        }
    ]
}
```

### DELETE /mystery/visits/{id} — Delete a visit

An accepted visit has become an audit — delete the audit first.

MCP tool: `mystery_visits_delete` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Visit id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /mystery/media/{id} — Download a private visit file

Audio / video recorded by the guest (REST only, not available as an MCP tool).

MCP tool: `mystery_media_get` · Permission: `mystery.view` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | File id from `files` of the visit |

Example response `data`:

```json
{
    "file": "binary"
}
```

### GET /mystery/locations — Get mystery shopper scores by location

Accepted visits per location: count, average and last scores, trend of the last 8 visits.

MCP tool: `mystery_locations` · Permission: `mystery.view` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `date_from` | query | string | no | Submitted from |
| `date_to` | query | string | no | Submitted to |

Example response `data`:

```json
[
    {
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "visits": 6,
        "avg": 84.2,
        "last": 87.5,
        "prev": 80,
        "last_at": "2026-11-03T13:40:00+05:00",
        "trend": [
            78,
            80,
            87.5
        ]
    }
]
```

### GET /mystery/shoppers — List guests

The company's pool of mystery guests with visit statistics. Email and phone — only with `mystery.payouts` (logged).

MCP tool: `mystery_shoppers_list` · Permission: `mystery.view` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `active` | query | boolean | no | Only active guests |

Example response `data`:

```json
[
    {
        "id": 55,
        "name": "Madina K.",
        "city": "Almaty",
        "email": "madina@example.com",
        "phone": "+7 701 000 00 00",
        "has_email": true,
        "note": "Prefers weekends",
        "is_active": true,
        "visits": {
            "total": 12,
            "approved": 10,
            "rejected": 1,
            "active": 1,
            "returned": 2
        },
        "last_visit_at": "2026-11-03T13:40:00+05:00"
    }
]
```

### POST /mystery/shoppers — Add a guest

MCP tool: `mystery_shoppers_create` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes |  |
| `email` | body | string | no |  |
| `phone` | body | string | no |  |
| `city` | body | string | no |  |
| `note` | body | string | no |  |
| `is_active` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 55,
    "name": "Madina K.",
    "city": "Almaty",
    "email": "madina@example.com",
    "phone": "+7 701 000 00 00",
    "has_email": true,
    "note": "Prefers weekends",
    "is_active": true,
    "visits": {
        "total": 12,
        "approved": 10,
        "rejected": 1,
        "active": 1,
        "returned": 2
    },
    "last_visit_at": "2026-11-03T13:40:00+05:00"
}
```

### PATCH /mystery/shoppers/{id} — Update a guest

Contacts (email, phone) can be changed only with `mystery.payouts`.

MCP tool: `mystery_shoppers_update` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Guest id |
| `name` | body | string | no |  |
| `email` | body | string | no |  |
| `phone` | body | string | no |  |
| `city` | body | string | no |  |
| `note` | body | string | no |  |
| `is_active` | body | boolean | no |  |

Example response `data`:

```json
{
    "id": 55,
    "name": "Madina K.",
    "city": "Almaty",
    "email": "madina@example.com",
    "phone": "+7 701 000 00 00",
    "has_email": true,
    "note": "Prefers weekends",
    "is_active": true,
    "visits": {
        "total": 12,
        "approved": 10,
        "rejected": 1,
        "active": 1,
        "returned": 2
    },
    "last_visit_at": "2026-11-03T13:40:00+05:00"
}
```

### DELETE /mystery/shoppers/{id} — Delete a guest

MCP tool: `mystery_shoppers_delete` · Permission: `mystery.manage` · Plan feature: `mystery`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Guest id |

Example response `data`:

```json
{
    "deleted": true
}
```

### GET /mystery/orders — List turnkey orders

Orders for mystery shopper checks performed by jeCheck guests, with progress of their visits. No plan feature is needed — the service is paid per order.

MCP tool: `mystery_orders_list` · Permission: `mystery.orders`

Example response `data`:

```json
[
    {
        "uuid": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
        "number": "MS-00042",
        "status": "quoted",
        "business_type": "cafe",
        "scenarios": [
            "service",
            "cleanliness"
        ],
        "goal": "Check the new menu launch",
        "checklist_id": 310,
        "needs_questionnaire": false,
        "questionnaire_brief": null,
        "visits_per_location": 2,
        "period_from": "2026-11-10",
        "period_to": "2026-11-30",
        "time_slots": [
            "weekday",
            "evening"
        ],
        "requirements": {
            "audio": false,
            "receipt": true,
            "place_photos": false,
            "staff": true,
            "express": false
        },
        "shopper_profile": {
            "gender": "any",
            "age_from": 20,
            "age_to": 35
        },
        "legend": null,
        "reimbursement": 5000,
        "currency": "KZT",
        "quote": {
            "currency": "KZT",
            "visits": 6,
            "lines": [
                {
                    "key": "visit",
                    "qty": 6,
                    "unit": 9000,
                    "amount": 54000
                }
            ],
            "total": 84000,
            "per_visit": 14000
        },
        "offer": null,
        "total": 84000,
        "contact_name": "Daniyar Omarov",
        "contact_phone": "+7 701 000 00 01",
        "comment": null,
        "payment_status": "none",
        "decline_reason": null,
        "locations": [
            {
                "id": 77,
                "department_id": 12,
                "name": "Abay Ave",
                "city": "Almaty",
                "address": "12 Abay Ave",
                "visits": 2,
                "note": null
            }
        ],
        "problems": {},
        "progress": {
            "done": 0,
            "avg": null,
            "planned": 6
        },
        "submitted_at": "2026-11-03T10:00:00+05:00",
        "confirmed_at": null,
        "launched_at": null,
        "completed_at": null,
        "created_at": "2026-11-02T18:00:00+05:00",
        "url": "https://app.jecheck.io/ui/mystery/orders/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
    }
]
```

### GET /mystery/orders/catalog — Get turnkey pricing catalog

Whether the service is available in the company's country, business types, cities and prices used by the quote.

MCP tool: `mystery_orders_catalog` · Permission: `mystery.orders`

Example response `data`:

```json
{
    "availability": {
        "available": true,
        "country": "KZ",
        "countries": [
            "KZ"
        ]
    },
    "catalog": {
        "currency": "KZT",
        "cities": [
            "Almaty"
        ],
        "major_cities": [
            "Almaty"
        ],
        "business_types": [
            {
                "key": "cafe",
                "reimbursement": 5000,
                "tier": "standard"
            }
        ]
    }
}
```

### POST /mystery/orders/quote — Calculate a turnkey quote

Price estimate for the given order parameters without saving anything.

MCP tool: `mystery_orders_quote` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `business_type` | body | string (restaurant, cafe, bar, coffee, fastfood, retail, beauty, clinic, hotel, other) | no | Business type (affects price and reimbursement) |
| `scenarios` | body | array<string> | no | What the guest checks: dine_in, takeaway, delivery, phone, consultation |
| `goal` | body | string | no |  |
| `checklist_id` | body | integer | no | Questionnaire (checklist) to use |
| `needs_questionnaire` | body | boolean | no | Ask jeCheck to prepare the questionnaire |
| `questionnaire_brief` | body | string | no |  |
| `visits_per_location` | body | integer | no |  |
| `period_from` | body | string | no |  |
| `period_to` | body | string | no |  |
| `time_slots` | body | array<string> | no | weekday, weekend, morning, lunch, evening |
| `requirements` | body | object | no |  |
| `shopper_profile` | body | object | no |  |
| `legend` | body | string | no |  |
| `reimbursement` | body | number | no | Receipt reimbursement per visit |
| `locations` | body | array<object> | no |  |
| `contact_name` | body | string | no |  |
| `contact_phone` | body | string | no |  |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "currency": "KZT",
    "visits": 6,
    "lines": [
        {
            "key": "visit",
            "qty": 6,
            "unit": 9000,
            "amount": 54000
        }
    ],
    "total": 84000,
    "per_visit": 14000
}
```

### GET /mystery/orders/{uuid} — Retrieve a turnkey order

Order terms, quote and offer, progress, visits (results after jeCheck approves them) and the message history.

MCP tool: `mystery_orders_get` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Order UUID |

Example response `data`:

```json
{
    "uuid": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "number": "MS-00042",
    "status": "quoted",
    "business_type": "cafe",
    "scenarios": [
        "service",
        "cleanliness"
    ],
    "goal": "Check the new menu launch",
    "checklist_id": 310,
    "needs_questionnaire": false,
    "questionnaire_brief": null,
    "visits_per_location": 2,
    "period_from": "2026-11-10",
    "period_to": "2026-11-30",
    "time_slots": [
        "weekday",
        "evening"
    ],
    "requirements": {
        "audio": false,
        "receipt": true,
        "place_photos": false,
        "staff": true,
        "express": false
    },
    "shopper_profile": {
        "gender": "any",
        "age_from": 20,
        "age_to": 35
    },
    "legend": null,
    "reimbursement": 5000,
    "currency": "KZT",
    "quote": {
        "currency": "KZT",
        "visits": 6,
        "lines": [
            {
                "key": "visit",
                "qty": 6,
                "unit": 9000,
                "amount": 54000
            }
        ],
        "total": 84000,
        "per_visit": 14000
    },
    "offer": null,
    "total": 84000,
    "contact_name": "Daniyar Omarov",
    "contact_phone": "+7 701 000 00 01",
    "comment": null,
    "payment_status": "none",
    "decline_reason": null,
    "locations": [
        {
            "id": 77,
            "department_id": 12,
            "name": "Abay Ave",
            "city": "Almaty",
            "address": "12 Abay Ave",
            "visits": 2,
            "note": null
        }
    ],
    "problems": {},
    "progress": {
        "done": 0,
        "avg": null,
        "planned": 6
    },
    "submitted_at": "2026-11-03T10:00:00+05:00",
    "confirmed_at": null,
    "launched_at": null,
    "completed_at": null,
    "created_at": "2026-11-02T18:00:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/orders/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "visits": [
        {
            "id": 2050,
            "location": {
                "id": 12,
                "name": "Abay Ave"
            },
            "stage": "done",
            "window_to": "2026-11-20T21:00:00+05:00",
            "visited_at": "2026-11-18T19:10:00+05:00",
            "score": 91,
            "audit_uuid": "5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e"
        }
    ],
    "events": [
        {
            "id": 1,
            "type": "message",
            "side": "service",
            "author": "Anna",
            "body": "We have prepared the terms",
            "payload": null,
            "at": "2026-11-03T11:00:00+05:00"
        }
    ]
}
```

### POST /mystery/orders — Create a turnkey order draft

Saves a draft with a quote; `problems` lists what is missing before `submit`. The service is currently available only in some countries (see the catalog).

MCP tool: `mystery_orders_create` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `business_type` | body | string (restaurant, cafe, bar, coffee, fastfood, retail, beauty, clinic, hotel, other) | no | Business type (affects price and reimbursement) |
| `scenarios` | body | array<string> | no | What the guest checks: dine_in, takeaway, delivery, phone, consultation |
| `goal` | body | string | no |  |
| `checklist_id` | body | integer | no | Questionnaire (checklist) to use |
| `needs_questionnaire` | body | boolean | no | Ask jeCheck to prepare the questionnaire |
| `questionnaire_brief` | body | string | no |  |
| `visits_per_location` | body | integer | no |  |
| `period_from` | body | string | no |  |
| `period_to` | body | string | no |  |
| `time_slots` | body | array<string> | no | weekday, weekend, morning, lunch, evening |
| `requirements` | body | object | no |  |
| `shopper_profile` | body | object | no |  |
| `legend` | body | string | no |  |
| `reimbursement` | body | number | no | Receipt reimbursement per visit |
| `locations` | body | array<object> | no |  |
| `contact_name` | body | string | no |  |
| `contact_phone` | body | string | no |  |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "uuid": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "number": "MS-00042",
    "status": "quoted",
    "business_type": "cafe",
    "scenarios": [
        "service",
        "cleanliness"
    ],
    "goal": "Check the new menu launch",
    "checklist_id": 310,
    "needs_questionnaire": false,
    "questionnaire_brief": null,
    "visits_per_location": 2,
    "period_from": "2026-11-10",
    "period_to": "2026-11-30",
    "time_slots": [
        "weekday",
        "evening"
    ],
    "requirements": {
        "audio": false,
        "receipt": true,
        "place_photos": false,
        "staff": true,
        "express": false
    },
    "shopper_profile": {
        "gender": "any",
        "age_from": 20,
        "age_to": 35
    },
    "legend": null,
    "reimbursement": 5000,
    "currency": "KZT",
    "quote": {
        "currency": "KZT",
        "visits": 6,
        "lines": [
            {
                "key": "visit",
                "qty": 6,
                "unit": 9000,
                "amount": 54000
            }
        ],
        "total": 84000,
        "per_visit": 14000
    },
    "offer": null,
    "total": 84000,
    "contact_name": "Daniyar Omarov",
    "contact_phone": "+7 701 000 00 01",
    "comment": null,
    "payment_status": "none",
    "decline_reason": null,
    "locations": [
        {
            "id": 77,
            "department_id": 12,
            "name": "Abay Ave",
            "city": "Almaty",
            "address": "12 Abay Ave",
            "visits": 2,
            "note": null
        }
    ],
    "problems": {},
    "progress": {
        "done": 0,
        "avg": null,
        "planned": 6
    },
    "submitted_at": "2026-11-03T10:00:00+05:00",
    "confirmed_at": null,
    "launched_at": null,
    "completed_at": null,
    "created_at": "2026-11-02T18:00:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/orders/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
}
```

### PATCH /mystery/orders/{uuid} — Update a turnkey order draft

Only drafts can be edited; after submitting, changes are agreed with the manager in messages.

MCP tool: `mystery_orders_update` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Order UUID |
| `business_type` | body | string (restaurant, cafe, bar, coffee, fastfood, retail, beauty, clinic, hotel, other) | no | Business type (affects price and reimbursement) |
| `scenarios` | body | array<string> | no | What the guest checks: dine_in, takeaway, delivery, phone, consultation |
| `goal` | body | string | no |  |
| `checklist_id` | body | integer | no | Questionnaire (checklist) to use |
| `needs_questionnaire` | body | boolean | no | Ask jeCheck to prepare the questionnaire |
| `questionnaire_brief` | body | string | no |  |
| `visits_per_location` | body | integer | no |  |
| `period_from` | body | string | no |  |
| `period_to` | body | string | no |  |
| `time_slots` | body | array<string> | no | weekday, weekend, morning, lunch, evening |
| `requirements` | body | object | no |  |
| `shopper_profile` | body | object | no |  |
| `legend` | body | string | no |  |
| `reimbursement` | body | number | no | Receipt reimbursement per visit |
| `locations` | body | array<object> | no |  |
| `contact_name` | body | string | no |  |
| `contact_phone` | body | string | no |  |
| `comment` | body | string | no |  |

Example response `data`:

```json
{
    "uuid": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "number": "MS-00042",
    "status": "quoted",
    "business_type": "cafe",
    "scenarios": [
        "service",
        "cleanliness"
    ],
    "goal": "Check the new menu launch",
    "checklist_id": 310,
    "needs_questionnaire": false,
    "questionnaire_brief": null,
    "visits_per_location": 2,
    "period_from": "2026-11-10",
    "period_to": "2026-11-30",
    "time_slots": [
        "weekday",
        "evening"
    ],
    "requirements": {
        "audio": false,
        "receipt": true,
        "place_photos": false,
        "staff": true,
        "express": false
    },
    "shopper_profile": {
        "gender": "any",
        "age_from": 20,
        "age_to": 35
    },
    "legend": null,
    "reimbursement": 5000,
    "currency": "KZT",
    "quote": {
        "currency": "KZT",
        "visits": 6,
        "lines": [
            {
                "key": "visit",
                "qty": 6,
                "unit": 9000,
                "amount": 54000
            }
        ],
        "total": 84000,
        "per_visit": 14000
    },
    "offer": null,
    "total": 84000,
    "contact_name": "Daniyar Omarov",
    "contact_phone": "+7 701 000 00 01",
    "comment": null,
    "payment_status": "none",
    "decline_reason": null,
    "locations": [
        {
            "id": 77,
            "department_id": 12,
            "name": "Abay Ave",
            "city": "Almaty",
            "address": "12 Abay Ave",
            "visits": 2,
            "note": null
        }
    ],
    "problems": {},
    "progress": {
        "done": 0,
        "avg": null,
        "planned": 6
    },
    "submitted_at": "2026-11-03T10:00:00+05:00",
    "confirmed_at": null,
    "launched_at": null,
    "completed_at": null,
    "created_at": "2026-11-02T18:00:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/orders/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
}
```

### POST /mystery/orders/{uuid}/submit — Submit a turnkey order

Sends the completed draft to jeCheck managers, who reply with an offer.

MCP tool: `mystery_orders_submit` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Order UUID |

Example response `data`:

```json
{
    "uuid": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "number": "MS-00042",
    "status": "quoted",
    "business_type": "cafe",
    "scenarios": [
        "service",
        "cleanliness"
    ],
    "goal": "Check the new menu launch",
    "checklist_id": 310,
    "needs_questionnaire": false,
    "questionnaire_brief": null,
    "visits_per_location": 2,
    "period_from": "2026-11-10",
    "period_to": "2026-11-30",
    "time_slots": [
        "weekday",
        "evening"
    ],
    "requirements": {
        "audio": false,
        "receipt": true,
        "place_photos": false,
        "staff": true,
        "express": false
    },
    "shopper_profile": {
        "gender": "any",
        "age_from": 20,
        "age_to": 35
    },
    "legend": null,
    "reimbursement": 5000,
    "currency": "KZT",
    "quote": {
        "currency": "KZT",
        "visits": 6,
        "lines": [
            {
                "key": "visit",
                "qty": 6,
                "unit": 9000,
                "amount": 54000
            }
        ],
        "total": 84000,
        "per_visit": 14000
    },
    "offer": null,
    "total": 84000,
    "contact_name": "Daniyar Omarov",
    "contact_phone": "+7 701 000 00 01",
    "comment": null,
    "payment_status": "none",
    "decline_reason": null,
    "locations": [
        {
            "id": 77,
            "department_id": 12,
            "name": "Abay Ave",
            "city": "Almaty",
            "address": "12 Abay Ave",
            "visits": 2,
            "note": null
        }
    ],
    "problems": {},
    "progress": {
        "done": 0,
        "avg": null,
        "planned": 6
    },
    "submitted_at": "2026-11-03T10:00:00+05:00",
    "confirmed_at": null,
    "launched_at": null,
    "completed_at": null,
    "created_at": "2026-11-02T18:00:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/orders/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
}
```

### POST /mystery/orders/{uuid}/accept — Accept the offer of a turnkey order

MCP tool: `mystery_orders_accept` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Order UUID |

Example response `data`:

```json
{
    "uuid": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "number": "MS-00042",
    "status": "quoted",
    "business_type": "cafe",
    "scenarios": [
        "service",
        "cleanliness"
    ],
    "goal": "Check the new menu launch",
    "checklist_id": 310,
    "needs_questionnaire": false,
    "questionnaire_brief": null,
    "visits_per_location": 2,
    "period_from": "2026-11-10",
    "period_to": "2026-11-30",
    "time_slots": [
        "weekday",
        "evening"
    ],
    "requirements": {
        "audio": false,
        "receipt": true,
        "place_photos": false,
        "staff": true,
        "express": false
    },
    "shopper_profile": {
        "gender": "any",
        "age_from": 20,
        "age_to": 35
    },
    "legend": null,
    "reimbursement": 5000,
    "currency": "KZT",
    "quote": {
        "currency": "KZT",
        "visits": 6,
        "lines": [
            {
                "key": "visit",
                "qty": 6,
                "unit": 9000,
                "amount": 54000
            }
        ],
        "total": 84000,
        "per_visit": 14000
    },
    "offer": null,
    "total": 84000,
    "contact_name": "Daniyar Omarov",
    "contact_phone": "+7 701 000 00 01",
    "comment": null,
    "payment_status": "none",
    "decline_reason": null,
    "locations": [
        {
            "id": 77,
            "department_id": 12,
            "name": "Abay Ave",
            "city": "Almaty",
            "address": "12 Abay Ave",
            "visits": 2,
            "note": null
        }
    ],
    "problems": {},
    "progress": {
        "done": 0,
        "avg": null,
        "planned": 6
    },
    "submitted_at": "2026-11-03T10:00:00+05:00",
    "confirmed_at": null,
    "launched_at": null,
    "completed_at": null,
    "created_at": "2026-11-02T18:00:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/orders/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
}
```

### POST /mystery/orders/{uuid}/cancel — Cancel a turnkey order

MCP tool: `mystery_orders_cancel` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Order UUID |
| `reason` | body | string | no |  |

Example response `data`:

```json
{
    "uuid": "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
    "number": "MS-00042",
    "status": "quoted",
    "business_type": "cafe",
    "scenarios": [
        "service",
        "cleanliness"
    ],
    "goal": "Check the new menu launch",
    "checklist_id": 310,
    "needs_questionnaire": false,
    "questionnaire_brief": null,
    "visits_per_location": 2,
    "period_from": "2026-11-10",
    "period_to": "2026-11-30",
    "time_slots": [
        "weekday",
        "evening"
    ],
    "requirements": {
        "audio": false,
        "receipt": true,
        "place_photos": false,
        "staff": true,
        "express": false
    },
    "shopper_profile": {
        "gender": "any",
        "age_from": 20,
        "age_to": 35
    },
    "legend": null,
    "reimbursement": 5000,
    "currency": "KZT",
    "quote": {
        "currency": "KZT",
        "visits": 6,
        "lines": [
            {
                "key": "visit",
                "qty": 6,
                "unit": 9000,
                "amount": 54000
            }
        ],
        "total": 84000,
        "per_visit": 14000
    },
    "offer": null,
    "total": 84000,
    "contact_name": "Daniyar Omarov",
    "contact_phone": "+7 701 000 00 01",
    "comment": null,
    "payment_status": "none",
    "decline_reason": null,
    "locations": [
        {
            "id": 77,
            "department_id": 12,
            "name": "Abay Ave",
            "city": "Almaty",
            "address": "12 Abay Ave",
            "visits": 2,
            "note": null
        }
    ],
    "problems": {},
    "progress": {
        "done": 0,
        "avg": null,
        "planned": 6
    },
    "submitted_at": "2026-11-03T10:00:00+05:00",
    "confirmed_at": null,
    "launched_at": null,
    "completed_at": null,
    "created_at": "2026-11-02T18:00:00+05:00",
    "url": "https://app.jecheck.io/ui/mystery/orders/0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
}
```

### POST /mystery/orders/{uuid}/messages — Write to the jeCheck manager

MCP tool: `mystery_orders_messages_create` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Order UUID |
| `body` | body | string | yes |  |

Example response `data`:

```json
{
    "id": 2,
    "type": "message",
    "side": "client",
    "author": "Daniyar Omarov",
    "body": "Can we add one more location?",
    "payload": null,
    "at": "2026-11-03T12:00:00+05:00"
}
```

### DELETE /mystery/orders/{uuid} — Delete a turnkey order draft

Only drafts can be deleted; submitted orders are cancelled.

MCP tool: `mystery_orders_delete` · Permission: `mystery.orders`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Order UUID |

Example response `data`:

```json
{
    "deleted": true
}
```

## Dashboards

Custom dashboards of cards built from the metrics catalog (audits, tasks, HACCP, schedule…). List and read dashboards, get computed card data (always limited by the viewer's permissions and locations), create and edit dashboards and cards, share them with colleagues or the whole company (`dashboards.company`).

### GET /dashboards — List dashboards

Dashboards visible to the acting user: own, shared with them and published to the whole company, in the user's tab order.

MCP tool: `dashboards_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `include_hidden` | query | boolean | no | Include dashboards the user hid from tabs |

Example response `data`:

```json
[
    {
        "uuid": "2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "title": "Location overview",
        "icon": "dashboard",
        "description": "Weekly picture of all coffee shops",
        "filters": {
            "period": "30d",
            "date_from": null,
            "date_to": null,
            "location_ids": [
                12,
                14
            ],
            "checklist_ids": [],
            "auditor_ids": []
        },
        "visibility": "company",
        "company_can_edit": false,
        "refresh_sec": 0,
        "template": "location",
        "owner": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "is_owner": false,
        "permissions": {
            "edit": false,
            "share": false,
            "delete": false
        },
        "updated_at": "2026-11-03T12:10:00+05:00",
        "url": "https://app.jecheck.io/ui/dashboards/2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "hidden": false
    }
]
```

### GET /dashboards/{uuid} — Retrieve a dashboard

Dashboard settings, saved filters, the acting user's permissions and the cards with their layout on a 12-column grid. Card values — `dashboards.data`.

MCP tool: `dashboards_get`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |

Example response `data`:

```json
{
    "uuid": "2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "title": "Location overview",
    "icon": "dashboard",
    "description": "Weekly picture of all coffee shops",
    "filters": {
        "period": "30d",
        "date_from": null,
        "date_to": null,
        "location_ids": [
            12,
            14
        ],
        "checklist_ids": [],
        "auditor_ids": []
    },
    "visibility": "company",
    "company_can_edit": false,
    "refresh_sec": 0,
    "template": "location",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "is_owner": false,
    "permissions": {
        "edit": false,
        "share": false,
        "delete": false
    },
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/dashboards/2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "widgets": [
        {
            "uuid": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "spec": {
                "v": 1,
                "type": "kpi",
                "metrics": [
                    "audits.avg_result"
                ],
                "compare": true,
                "title": "Average score"
            },
            "x": 0,
            "y": 0,
            "w": 3,
            "h": 2,
            "source": "library",
            "preset": "kpi_avg"
        }
    ]
}
```

### GET /dashboards/{uuid}/data — Get computed data of all cards

Values of every card (or of `widget_uuids`), keyed by card UUID. Computed with the dashboard filters, or with the filters passed here (temporary, not saved). Data is always limited by the acting user's permissions; a card the user may not see returns `{"error": "...", "message": "..."}`.

MCP tool: `dashboards_data`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `widget_uuids` | query | array<string> | no | Only these cards |
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` needs `date_from` and `date_to` |
| `date_from` | query | string | no | Start of a custom period |
| `date_to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations (within the viewer's permissions) |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only these auditors |

Example response `data`:

```json
{
    "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d": {
        "kind": "value",
        "value": 82.4,
        "previous": 79.1,
        "unit": "%",
        "meta": {
            "from": "2026-10-04",
            "to": "2026-11-03"
        }
    }
}
```

### GET /dashboards/{uuid}/widgets/{widget_uuid}/data — Get computed data of a card

MCP tool: `dashboards_widgets_data`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `widget_uuid` | path | string | yes | Card UUID |
| `period` | query | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` needs `date_from` and `date_to` |
| `date_from` | query | string | no | Start of a custom period |
| `date_to` | query | string | no | End of a custom period |
| `location_ids` | query | array<integer> | no | Only these locations (within the viewer's permissions) |
| `checklist_ids` | query | array<integer> | no | Only these checklists |
| `auditor_ids` | query | array<integer> | no | Only these auditors |

Example response `data`:

```json
{
    "kind": "value",
    "value": 82.4,
    "previous": 79.1,
    "unit": "%",
    "meta": {
        "from": "2026-10-04",
        "to": "2026-11-03"
    }
}
```

### POST /dashboards/preview — Compute a card without saving it

Runs a card definition with the given filters — use it to try a `spec` before adding the card.

MCP tool: `dashboards_preview`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `spec` | body | object | yes | Card definition: {"type", "metrics", "dimension", "dimension2", "list", "period", "title", "target", "compare", "limit", "color", "text"} — see `dashboards.catalog` for metric, dimension, list and type keys |
| `period` | body | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` needs `date_from` and `date_to` |
| `date_from` | body | string | no | Start of a custom period |
| `date_to` | body | string | no | End of a custom period |
| `location_ids` | body | array<integer> | no | Only these locations (within the viewer's permissions) |
| `checklist_ids` | body | array<integer> | no | Only these checklists |
| `auditor_ids` | body | array<integer> | no | Only these auditors |

Example response `data`:

```json
{
    "kind": "value",
    "value": 82.4,
    "previous": 79.1,
    "unit": "%",
    "meta": {
        "from": "2026-10-04",
        "to": "2026-11-03"
    }
}
```

### GET /dashboards/catalog — Get the metrics catalog

Everything a card can be built from: metric groups, metrics (unit, which direction is better, allowed dimensions, availability for the acting user), dimensions, card types and their rules, lists, periods, ready-made cards (`presets`) and dashboard templates.

MCP tool: `dashboards_catalog`

Example response `data`:

```json
{
    "groups": [
        {
            "key": "audits",
            "label": "Audits",
            "icon": "fact_check"
        }
    ],
    "dimensions": [
        {
            "key": "location",
            "label": "Location",
            "kind": "category"
        }
    ],
    "types": [
        {
            "key": "kpi",
            "label": "Number",
            "icon": "pin",
            "metrics": [
                1,
                1
            ],
            "dimension": false
        }
    ],
    "metrics": [
        {
            "key": "audits.avg_result",
            "group": "audits",
            "label": "Average score",
            "description": "Average audit result",
            "unit": "%",
            "better": "up",
            "dimensions": [
                "time",
                "location",
                "checklist"
            ],
            "snapshot": false,
            "available": true,
            "reason": null
        }
    ],
    "lists": [
        {
            "key": "tasks.overdue",
            "group": "tasks",
            "label": "Overdue tasks",
            "description": "Open tasks past due",
            "available": true,
            "reason": null
        }
    ],
    "periods": [
        {
            "key": "30d",
            "label": "30 days"
        }
    ],
    "presets": [
        {
            "key": "kpi_avg",
            "group": "audits",
            "label": "Average score",
            "description": "Average audit result with trend",
            "type": "kpi",
            "spec": {
                "v": 1,
                "type": "kpi",
                "metrics": [
                    "audits.avg_result"
                ],
                "compare": true
            },
            "w": 3,
            "h": 2,
            "available": true,
            "reason": null
        }
    ],
    "templates": [
        {
            "key": "location",
            "label": "Location overview",
            "description": "Key figures of locations",
            "icon": "storefront",
            "presets": [
                "kpi_avg"
            ]
        }
    ]
}
```

### POST /dashboards — Create a dashboard

Creates a private dashboard of the acting user — empty, from a template (`dashboards.catalog` → templates) and/or with the given cards. Publish it with `dashboards.share`.

MCP tool: `dashboards_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `title` | body | string | no | Title |
| `icon` | body | string | no | Material Symbols icon name |
| `description` | body | string | no |  |
| `template` | body | string (network, quality, operations, location, haccp, training, mystery) | no | Template key |
| `widgets` | body | array<object> | no | Cards to add (layout is packed automatically when x/y are omitted) |

Example response `data`:

```json
{
    "uuid": "2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "title": "Location overview",
    "icon": "dashboard",
    "description": "Weekly picture of all coffee shops",
    "filters": {
        "period": "30d",
        "date_from": null,
        "date_to": null,
        "location_ids": [
            12,
            14
        ],
        "checklist_ids": [],
        "auditor_ids": []
    },
    "visibility": "company",
    "company_can_edit": false,
    "refresh_sec": 0,
    "template": "location",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "is_owner": false,
    "permissions": {
        "edit": false,
        "share": false,
        "delete": false
    },
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/dashboards/2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "widgets": [
        {
            "uuid": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "spec": {
                "v": 1,
                "type": "kpi",
                "metrics": [
                    "audits.avg_result"
                ],
                "compare": true,
                "title": "Average score"
            },
            "x": 0,
            "y": 0,
            "w": 3,
            "h": 2,
            "source": "library",
            "preset": "kpi_avg"
        }
    ]
}
```

### PATCH /dashboards/{uuid} — Update a dashboard

Title, icon, description, saved filters and auto-refresh. Requires edit access.

MCP tool: `dashboards_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `title` | body | string | no |  |
| `icon` | body | string | no |  |
| `description` | body | string | no |  |
| `filters` | body | object | no | Saved filters: period, date_from, date_to, location_ids, checklist_ids, auditor_ids |
| `refresh_sec` | body | integer | no | Auto-refresh in the web app, seconds (0 — off, minimum 60) |

Example response `data`:

```json
{
    "uuid": "2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "title": "Location overview",
    "icon": "dashboard",
    "description": "Weekly picture of all coffee shops",
    "filters": {
        "period": "30d",
        "date_from": null,
        "date_to": null,
        "location_ids": [
            12,
            14
        ],
        "checklist_ids": [],
        "auditor_ids": []
    },
    "visibility": "company",
    "company_can_edit": false,
    "refresh_sec": 0,
    "template": "location",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "is_owner": false,
    "permissions": {
        "edit": false,
        "share": false,
        "delete": false
    },
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/dashboards/2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "widgets": [
        {
            "uuid": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "spec": {
                "v": 1,
                "type": "kpi",
                "metrics": [
                    "audits.avg_result"
                ],
                "compare": true,
                "title": "Average score"
            },
            "x": 0,
            "y": 0,
            "w": 3,
            "h": 2,
            "source": "library",
            "preset": "kpi_avg"
        }
    ]
}
```

### DELETE /dashboards/{uuid} — Delete a dashboard

Allowed to the owner; a company dashboard — also with `dashboards.company`.

MCP tool: `dashboards_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /dashboards/{uuid}/duplicate — Duplicate a dashboard

Creates a private copy owned by the acting user.

MCP tool: `dashboards_duplicate`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |

Example response `data`:

```json
{
    "uuid": "2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "title": "Location overview",
    "icon": "dashboard",
    "description": "Weekly picture of all coffee shops",
    "filters": {
        "period": "30d",
        "date_from": null,
        "date_to": null,
        "location_ids": [
            12,
            14
        ],
        "checklist_ids": [],
        "auditor_ids": []
    },
    "visibility": "company",
    "company_can_edit": false,
    "refresh_sec": 0,
    "template": "location",
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "is_owner": false,
    "permissions": {
        "edit": false,
        "share": false,
        "delete": false
    },
    "updated_at": "2026-11-03T12:10:00+05:00",
    "url": "https://app.jecheck.io/ui/dashboards/2a1b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    "widgets": [
        {
            "uuid": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "spec": {
                "v": 1,
                "type": "kpi",
                "metrics": [
                    "audits.avg_result"
                ],
                "compare": true,
                "title": "Average score"
            },
            "x": 0,
            "y": 0,
            "w": 3,
            "h": 2,
            "source": "library",
            "preset": "kpi_avg"
        }
    ]
}
```

### PUT /dashboards/order — Reorder and hide dashboard tabs

Personal tab order of the acting user; `hidden` — dashboards to hide from the tabs.

MCP tool: `dashboards_reorder`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuids` | body | array<string> | yes | Dashboard UUIDs in the new order |
| `hidden` | body | array<string> | no | Hidden dashboard UUIDs |

Example response `data`:

```json
{
    "ok": true
}
```

### GET /dashboards/{uuid}/share — Get dashboard sharing

Who has access: visibility (`private` / `company`), whether everyone may edit a company dashboard, and colleagues with personal access. For the owner (and `dashboards.company` on company dashboards).

MCP tool: `dashboards_sharing`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |

Example response `data`:

```json
{
    "visibility": "private",
    "company_can_edit": false,
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "shares": [
        {
            "user": {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            },
            "can_edit": true
        }
    ],
    "can_publish_to_company": true
}
```

### PUT /dashboards/{uuid}/share — Share a dashboard

Replaces the sharing settings. Publishing to the whole company requires `dashboards.company`.

MCP tool: `dashboards_share`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `visibility` | body | string (private, company) | yes |  |
| `company_can_edit` | body | boolean | no | Everyone in the company may edit (company dashboards) |
| `users` | body | array<object> | no | Personal access (replaces the list) |

Example response `data`:

```json
{
    "visibility": "company",
    "company_can_edit": false,
    "owner": {
        "id": 120,
        "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "name": "Daniyar Omarov"
    },
    "shares": [],
    "can_publish_to_company": true
}
```

### PUT /dashboards/{uuid}/layout — Change the card layout

Positions and sizes of cards on the 12-column grid. Requires edit access.

MCP tool: `dashboards_layout`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `items` | body | array<object> | yes |  |

Example response `data`:

```json
[
    {
        "uuid": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
        "spec": {
            "v": 1,
            "type": "kpi",
            "metrics": [
                "audits.avg_result"
            ],
            "compare": true,
            "title": "Average score"
        },
        "x": 0,
        "y": 0,
        "w": 3,
        "h": 2,
        "source": "library",
        "preset": "kpi_avg"
    }
]
```

### POST /dashboards/{uuid}/widgets — Add a card

Adds a card (custom `spec` or a ready-made `preset` spec from the catalog). Without x/y it goes below the existing cards. Up to 60 cards per dashboard. Requires edit access.

MCP tool: `dashboards_widgets_create`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `spec` | body | object | yes | Card definition: {"type", "metrics", "dimension", "dimension2", "list", "period", "title", "target", "compare", "limit", "color", "text"} — see `dashboards.catalog` for metric, dimension, list and type keys |
| `x` | body | integer | no |  |
| `y` | body | integer | no |  |
| `w` | body | integer | no |  |
| `h` | body | integer | no |  |
| `preset` | body | string | no | Ready-made card key (for reference) |

Example response `data`:

```json
{
    "uuid": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
    "spec": {
        "v": 1,
        "type": "kpi",
        "metrics": [
            "audits.avg_result"
        ],
        "compare": true,
        "title": "Average score"
    },
    "x": 0,
    "y": 0,
    "w": 3,
    "h": 2,
    "source": "library",
    "preset": "kpi_avg"
}
```

### PUT /dashboards/{uuid}/widgets/{widget_uuid} — Replace a card definition

MCP tool: `dashboards_widgets_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `widget_uuid` | path | string | yes | Card UUID |
| `spec` | body | object | yes | Card definition: {"type", "metrics", "dimension", "dimension2", "list", "period", "title", "target", "compare", "limit", "color", "text"} — see `dashboards.catalog` for metric, dimension, list and type keys |

Example response `data`:

```json
{
    "uuid": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
    "spec": {
        "v": 1,
        "type": "kpi",
        "metrics": [
            "audits.avg_result"
        ],
        "compare": true,
        "title": "Average score"
    },
    "x": 0,
    "y": 0,
    "w": 3,
    "h": 2,
    "source": "library",
    "preset": "kpi_avg"
}
```

### DELETE /dashboards/{uuid}/widgets/{widget_uuid} — Delete a card

MCP tool: `dashboards_widgets_delete`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `widget_uuid` | path | string | yes | Card UUID |

Example response `data`:

```json
{
    "deleted": true
}
```

### POST /dashboards/{uuid}/widgets/{widget_uuid}/copy — Copy a card to another dashboard

MCP tool: `dashboards_widgets_copy`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `widget_uuid` | path | string | yes | Card UUID |
| `target_uuid` | body | string | yes | Target dashboard (edit access required) |

Example response `data`:

```json
{
    "uuid": "7c6b5a4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
    "spec": {
        "v": 1,
        "type": "kpi",
        "metrics": [
            "audits.avg_result"
        ],
        "compare": true,
        "title": "Average score"
    },
    "x": 0,
    "y": 0,
    "w": 3,
    "h": 2,
    "source": "library",
    "preset": "kpi_avg",
    "dashboard_uuid": "9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f"
}
```

### POST /dashboards/ai/card — Build a card from a description (AI)

AI turns a request in plain words ("average score by location for 90 days") into a card definition and computes it. Nothing is saved — add it with `dashboards.widgets.create`. Daily AI limits apply.

MCP tool: `dashboards_ai_card` · Plan feature: `ai`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `prompt` | body | string | yes |  |
| `spec` | body | object | no | Current card to adjust |
| `period` | body | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` needs `date_from` and `date_to` |
| `date_from` | body | string | no | Start of a custom period |
| `date_to` | body | string | no | End of a custom period |
| `location_ids` | body | array<integer> | no | Only these locations (within the viewer's permissions) |
| `checklist_ids` | body | array<integer> | no | Only these checklists |
| `auditor_ids` | body | array<integer> | no | Only these auditors |

Example response `data`:

```json
{
    "spec": {
        "v": 1,
        "type": "kpi",
        "metrics": [
            "audits.avg_result"
        ],
        "compare": true,
        "title": "Average score"
    },
    "explanation": "Average audit score with comparison to the previous period",
    "size": {
        "w": 3,
        "h": 2
    },
    "data": {
        "kind": "value",
        "value": 82.4,
        "previous": 79.1,
        "unit": "%",
        "meta": {
            "from": "2026-10-04",
            "to": "2026-11-03"
        }
    }
}
```

### POST /dashboards/ai/dashboard — Draft a dashboard from a description (AI)

Returns a draft (title, cards and layout) — nothing is saved; pass `widgets` to `dashboards.create`.

MCP tool: `dashboards_ai_dashboard` · Plan feature: `ai`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `prompt` | body | string | yes |  |

Example response `data`:

```json
{
    "title": "Kitchen quality",
    "icon": "restaurant",
    "explanation": "Scores, failed questions and overdue tasks",
    "widgets": [
        {
            "spec": {
                "v": 1,
                "type": "kpi",
                "metrics": [
                    "audits.avg_result"
                ],
                "compare": true,
                "title": "Average score"
            },
            "x": 0,
            "y": 0,
            "w": 3,
            "h": 2,
            "source": "ai"
        }
    ]
}
```

### POST /dashboards/{uuid}/widgets/{widget_uuid}/explain — Explain card data (AI)

A short explanation of what the card shows for the acting user's data and filters.

MCP tool: `dashboards_ai_explain` · Plan feature: `ai`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `uuid` | path | string | yes | Dashboard UUID |
| `widget_uuid` | path | string | yes | Card UUID |
| `period` | body | string (7d, 30d, 90d, 12m, custom) | no | Period; `custom` needs `date_from` and `date_to` |
| `date_from` | body | string | no | Start of a custom period |
| `date_to` | body | string | no | End of a custom period |
| `location_ids` | body | array<integer> | no | Only these locations (within the viewer's permissions) |
| `checklist_ids` | body | array<integer> | no | Only these checklists |
| `auditor_ids` | body | array<integer> | no | Only these auditors |

Example response `data`:

```json
{
    "text": "The average score grew from 79% to 82%, mostly thanks to Abay Ave."
}
```

## Notifications

In-app notifications of the acting user (the bell in the web app and the Notifications screen in the mobile app): list, unread count, mark as read, and personal notification settings. A company key sees the notifications of its service user.

### GET /notifications — List notifications

Notifications of the acting user, newest first. `meta.unread` — the number of unread notifications.

MCP tool: `notifications_list`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `unread` | query | boolean | no | Only unread notifications |
| `since` | query | string | no | Only notifications created after this moment |

Example response `data`:

```json
[
    {
        "id": 88123,
        "title": "New task: Replace the broken fridge seal",
        "body": "Abay Ave · due Nov 10",
        "url": "https://app.jecheck.io/ui/tasks?task=3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b",
        "data": {
            "type": "task",
            "task_id": "3f2b8c1e-7a4d-4e2f-9b1a-6c5d4e3f2a1b"
        },
        "is_read": false,
        "read_at": null,
        "created_at": "2026-11-03T10:24:00+05:00"
    }
]
```

### GET /notifications/unread-count — Count unread notifications

MCP tool: `notifications_unread_count`

Example response `data`:

```json
{
    "unread": 3
}
```

### POST /notifications/read — Mark notifications as read

Pass `ids` to mark specific notifications, or `all: true` to mark everything as read. Returns the remaining unread count.

MCP tool: `notifications_read`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `ids` | body | array<integer> | no | Notification ids |
| `all` | body | boolean | no | Mark all notifications as read |

Example response `data`:

```json
{
    "marked": 2,
    "unread": 1
}
```

### GET /notifications/preferences — Get notification settings

Personal settings by channel (`push`, `email`) and event group: `assigned` — a task is assigned, `comments`, `updates` — status and due-date changes, `reminders` — due and overdue reminders, `digest` — daily email digest, `brief` — morning AI-control brief, `urgent` — urgent signals during the day. The in-app history is always kept.

MCP tool: `notifications_preferences_get`

Example response `data`:

```json
{
    "push": {
        "assigned": true,
        "comments": true,
        "updates": true,
        "reminders": true,
        "brief": true,
        "urgent": true
    },
    "email": {
        "assigned": true,
        "digest": false,
        "brief": true
    }
}
```

### PATCH /notifications/preferences — Update notification settings

Partial update: pass only the switches to change, e.g. `{"email": {"digest": false}}`. Unknown keys are ignored.

MCP tool: `notifications_preferences_update`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `push` | body | object | no | Push notifications to the mobile app |
| `email` | body | object | no | Emails (only for users with web-cabinet access) |

Example response `data`:

```json
{
    "push": {
        "assigned": true,
        "comments": true,
        "updates": true,
        "reminders": true,
        "brief": true,
        "urgent": true
    },
    "email": {
        "assigned": true,
        "digest": false,
        "brief": true
    }
}
```

## Guest feedback

Guest reviews of locations collected from map services (Yandex Maps, 2GIS, TripAdvisor, Delivery Club) with sentiment analysis, and customer appeals from the company Telegram / WhatsApp bot with replies. Reviews need `reviews.view` (limited to its locations); appeals need `appeals.view`.

### GET /feedback/reviews — List guest reviews

Reviews of the locations where `reviews.view` applies, newest first, with stars and sentiment (`positive`, `neutral`, `negative`, `mixed`).

MCP tool: `feedback_reviews_list` · Permission: `reviews.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `location_ids` | query | array<integer> | no | Only these locations |
| `date_from` | query | string | no | Created from (inclusive) |
| `date_to` | query | string | no | Created to (inclusive) |
| `source` | query | string | no | Review source: YandexMaps, TwoGis, TripAdvisor, DeliveryClub, GoogleMaps |
| `stars_min` | query | integer | no | Minimum stars |
| `stars_max` | query | integer | no | Maximum stars |
| `sentiment` | query | string (positive, neutral, negative, mixed) | no | Overall sentiment |
| `search` | query | string | no | Search in review text and author name |

Example response `data`:

```json
[
    {
        "id": 1683,
        "location": {
            "id": 12,
            "name": "Abay Ave"
        },
        "stars": 2,
        "text": "Coffee was great but we waited 25 minutes for a table.",
        "author_name": "Olga V.",
        "source": "TwoGis",
        "sentiment": "mixed",
        "sentiment_scores": {
            "positive": 0.41,
            "neutral": 0.05,
            "negative": 0.54
        },
        "created_at": "2026-11-02T19:01:00+05:00"
    }
]
```

### GET /feedback/reviews/{id} — Retrieve a guest review

Review with sentence-level sentiment.

MCP tool: `feedback_reviews_get` · Permission: `reviews.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Review id |

Example response `data`:

```json
{
    "id": 1683,
    "location": {
        "id": 12,
        "name": "Abay Ave"
    },
    "stars": 2,
    "text": "Coffee was great but we waited 25 minutes for a table.",
    "author_name": "Olga V.",
    "source": "TwoGis",
    "sentiment": "mixed",
    "sentiment_scores": {
        "positive": 0.41,
        "neutral": 0.05,
        "negative": 0.54
    },
    "created_at": "2026-11-02T19:01:00+05:00",
    "sentences": [
        {
            "text": "Coffee was great",
            "sentiment": "positive"
        },
        {
            "text": "but we waited 25 minutes for a table.",
            "sentiment": "negative"
        }
    ]
}
```

### GET /feedback/reviews/stats — Get review statistics by location

Number of reviews, average stars and sentiment split per location for the period, plus the total.

MCP tool: `feedback_reviews_stats` · Permission: `reviews.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `location_ids` | query | array<integer> | no | Only these locations |
| `date_from` | query | string | no | Created from (inclusive) |
| `date_to` | query | string | no | Created to (inclusive) |
| `source` | query | string | no | Review source: YandexMaps, TwoGis, TripAdvisor, DeliveryClub, GoogleMaps |

Example response `data`:

```json
{
    "total": {
        "reviews": 132,
        "avg_stars": 4.3,
        "positive": 88,
        "neutral": 12,
        "negative": 21,
        "mixed": 11
    },
    "locations": [
        {
            "location": {
                "id": 12,
                "name": "Abay Ave"
            },
            "reviews": 70,
            "avg_stars": 4.5,
            "positive": 51,
            "neutral": 6,
            "negative": 8,
            "mixed": 5
        }
    ]
}
```

### GET /feedback/appeals — List customer appeals

Conversations started by customers in the company Telegram or WhatsApp bot, newest first, with the last message. Contains customer contacts, so `appeals.view` is required.

MCP tool: `feedback_appeals_list` · Permission: `appeals.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `status` | query | string (open, closed) | no | Filter by status |

Example response `data`:

```json
[
    {
        "id": 340,
        "title": "Lost umbrella",
        "status": "open",
        "contact": {
            "id": 77,
            "name": "Marat",
            "phone": null,
            "email": null,
            "channel": "telegram"
        },
        "assignee": null,
        "last_message": {
            "id": 5521,
            "author": {
                "type": "customer",
                "id": 77,
                "uuid": null,
                "name": "Marat"
            },
            "type": "text",
            "text": "Hello! I left my umbrella at Abay Ave yesterday.",
            "file": null,
            "created_at": "2026-11-03T09:12:00+05:00"
        },
        "created_at": "2026-11-03T09:12:00+05:00"
    }
]
```

### GET /feedback/appeals/{id} — Retrieve a customer appeal

The appeal with the whole conversation, oldest message first.

MCP tool: `feedback_appeals_get` · Permission: `appeals.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Appeal id |

Example response `data`:

```json
{
    "id": 340,
    "title": "Lost umbrella",
    "status": "open",
    "contact": {
        "id": 77,
        "name": "Marat",
        "phone": null,
        "email": null,
        "channel": "telegram"
    },
    "assignee": null,
    "last_message": {
        "id": 5521,
        "author": {
            "type": "customer",
            "id": 77,
            "uuid": null,
            "name": "Marat"
        },
        "type": "text",
        "text": "Hello! I left my umbrella at Abay Ave yesterday.",
        "file": null,
        "created_at": "2026-11-03T09:12:00+05:00"
    },
    "created_at": "2026-11-03T09:12:00+05:00",
    "messages": [
        {
            "id": 5521,
            "author": {
                "type": "customer",
                "id": 77,
                "uuid": null,
                "name": "Marat"
            },
            "type": "text",
            "text": "Hello! I left my umbrella at Abay Ave yesterday.",
            "file": null,
            "created_at": "2026-11-03T09:12:00+05:00"
        }
    ]
}
```

### POST /feedback/appeals/{id}/messages — Reply to a customer appeal

Sends a text message to the customer through the channel of the appeal (Telegram bot or WhatsApp) on behalf of the acting user. Returns the updated conversation.

MCP tool: `feedback_appeals_reply` · Permission: `appeals.view`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Appeal id |
| `text` | body | string | yes | Message text |

Example response `data`:

```json
{
    "id": 340,
    "title": "Lost umbrella",
    "status": "open",
    "contact": {
        "id": 77,
        "name": "Marat",
        "phone": null,
        "email": null,
        "channel": "telegram"
    },
    "assignee": null,
    "last_message": {
        "id": 5521,
        "author": {
            "type": "customer",
            "id": 77,
            "uuid": null,
            "name": "Marat"
        },
        "type": "text",
        "text": "Hello! I left my umbrella at Abay Ave yesterday.",
        "file": null,
        "created_at": "2026-11-03T09:12:00+05:00"
    },
    "created_at": "2026-11-03T09:12:00+05:00",
    "messages": [
        {
            "id": 5521,
            "author": {
                "type": "customer",
                "id": 77,
                "uuid": null,
                "name": "Marat"
            },
            "type": "text",
            "text": "Hello! I left my umbrella at Abay Ave yesterday.",
            "file": null,
            "created_at": "2026-11-03T09:12:00+05:00"
        }
    ]
}
```

## Roles & permissions

The permission catalog, company roles, effective permissions of an employee and the access log. Changes follow the delegation rules of the web app: a non-administrator can grant only permissions they hold themselves, only on their own locations, and cannot change administrators.

### GET /access/permissions — List the permission catalog

Every permission key with its group, label, description, whether it can be limited to locations (`scoped`), whether views are logged (`sensitive`) and the permissions it requires.

MCP tool: `access_permissions_list`

Example response `data`:

```json
[
    {
        "key": "users.contacts",
        "group": "users",
        "group_label": "Employees",
        "label": "Employee phone and email",
        "hint": "See email and phone in the list and card",
        "description": "Email and phone of employees in the list, card and export.",
        "scoped": true,
        "sensitive": false,
        "requires": []
    }
]
```

### GET /access/roles — List roles

Company roles with their permissions and members. System roles (`system_key`: `employee` — everyone, `location_manager` — managers of locations and regions) are applied automatically and cannot be assigned or deleted.

MCP tool: `access_roles_list` · Permission: `access.manage` or `access.roles` or `access.log`

Example response `data`:

```json
[
    {
        "id": 41,
        "system_key": null,
        "name": "Auditor",
        "description": "Runs audits and reviews results",
        "color": "#5249e6",
        "permissions": [
            {
                "key": "audits.view",
                "scope": "all",
                "location_ids": []
            },
            {
                "key": "tasks.view_all",
                "scope": "list",
                "location_ids": [
                    12,
                    14
                ]
            }
        ],
        "members": [
            {
                "id": 311,
                "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
                "name": "Aigerim Sadykova"
            }
        ]
    }
]
```

### GET /access/roles/{id} — Retrieve a role

MCP tool: `access_roles_get` · Permission: `access.manage` or `access.roles` or `access.log`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Role id |

Example response `data`:

```json
{
    "id": 41,
    "system_key": null,
    "name": "Auditor",
    "description": "Runs audits and reviews results",
    "color": "#5249e6",
    "permissions": [
        {
            "key": "audits.view",
            "scope": "all",
            "location_ids": []
        },
        {
            "key": "tasks.view_all",
            "scope": "list",
            "location_ids": [
                12,
                14
            ]
        }
    ],
    "members": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ]
}
```

### POST /access/roles — Create a role

A non-administrator can include only permissions they hold, within their own locations. Written to the access log.

MCP tool: `access_roles_create` · Permission: `access.roles`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `name` | body | string | yes | Role name (system roles cannot be renamed) |
| `description` | body | string | no |  |
| `color` | body | string | no | Hex color |
| `permissions` | body | array<object> | no | Full list of the role permissions (replaces the current list). Required dependencies are added automatically. `scope`: `all` — all locations, `own` — the member's own locations, `managed` — locations the member manages, `list` — `location_ids` |

Example response `data`:

```json
{
    "id": 41,
    "system_key": null,
    "name": "Auditor",
    "description": "Runs audits and reviews results",
    "color": "#5249e6",
    "permissions": [
        {
            "key": "audits.view",
            "scope": "all",
            "location_ids": []
        },
        {
            "key": "tasks.view_all",
            "scope": "list",
            "location_ids": [
                12,
                14
            ]
        }
    ],
    "members": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ]
}
```

### PATCH /access/roles/{id} — Update a role

Partial update. `permissions`, when passed, replaces the whole list. Members get the new permissions immediately.

MCP tool: `access_roles_update` · Permission: `access.roles`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Role id |
| `name` | body | string | no | Role name (system roles cannot be renamed) |
| `description` | body | string | no |  |
| `color` | body | string | no | Hex color |
| `permissions` | body | array<object> | no | Full list of the role permissions (replaces the current list). Required dependencies are added automatically. `scope`: `all` — all locations, `own` — the member's own locations, `managed` — locations the member manages, `list` — `location_ids` |

Example response `data`:

```json
{
    "id": 41,
    "system_key": null,
    "name": "Auditor",
    "description": "Runs audits and reviews results",
    "color": "#5249e6",
    "permissions": [
        {
            "key": "audits.view",
            "scope": "all",
            "location_ids": []
        },
        {
            "key": "tasks.view_all",
            "scope": "list",
            "location_ids": [
                12,
                14
            ]
        }
    ],
    "members": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ]
}
```

### DELETE /access/roles/{id} — Delete a role

Members lose the permissions of the role. System roles cannot be deleted.

MCP tool: `access_roles_delete` · Permission: `access.roles`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Role id |

Example response `data`:

```json
{
    "deleted": true
}
```

### PUT /access/roles/{id}/members — Set role members

Replaces the members of a (non-system) role. Each assignment and removal is checked against the delegation rules and logged.

MCP tool: `access_roles_members_set` · Permission: `access.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Role id |
| `user_ids` | body | array<integer> | yes | Employee ids |

Example response `data`:

```json
{
    "id": 41,
    "system_key": null,
    "name": "Auditor",
    "description": "Runs audits and reviews results",
    "color": "#5249e6",
    "permissions": [
        {
            "key": "audits.view",
            "scope": "all",
            "location_ids": []
        },
        {
            "key": "tasks.view_all",
            "scope": "list",
            "location_ids": [
                12,
                14
            ]
        }
    ],
    "members": [
        {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        }
    ]
}
```

### GET /access/users/{id} — Get permissions of an employee

Roles, personal grants and denials, and effective permissions with the locations where each applies (`location_ids: null` — all locations) and where it comes from. With only `users.access` the response contains just `user` and `account` (administrator and web-cabinet flags).

MCP tool: `access_users_get` · Permission: `access.manage` or `access.roles` or `access.log` or `users.access`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |

Example response `data`:

```json
{
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "account": {
        "is_admin": false,
        "is_owner": false,
        "web_access": true,
        "works_anywhere": false,
        "manages_locations": [
            "Abay Ave"
        ],
        "manages_regions": [],
        "can_change_admin": true,
        "can_change_web_access": true
    },
    "role_ids": [
        41
    ],
    "system_role_ids": [
        1,
        2
    ],
    "personal": [
        {
            "key": "users.contacts",
            "effect": "grant",
            "scope": "list",
            "location_ids": [
                12
            ]
        }
    ],
    "effective": [
        {
            "key": "audits.view",
            "location_ids": null,
            "sources": [
                {
                    "source": "role",
                    "role_id": 41,
                    "role": "Auditor",
                    "scope": "all",
                    "location_ids": []
                }
            ]
        }
    ],
    "can_edit": true
}
```

### PUT /access/users/{id} — Set roles and personal permissions of an employee

Replaces the employee's roles (`role_ids`) and/or personal permissions (`personal`); an omitted field keeps its current value. Administrators and yourself cannot be changed. Every change is logged.

MCP tool: `access_users_update` · Permission: `access.manage`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |
| `role_ids` | body | array<integer> | no | Ids of company (non-system) roles |
| `personal` | body | array<object> | no | Personal grants and denials |

Example response `data`:

```json
{
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "account": {
        "is_admin": false,
        "is_owner": false,
        "web_access": true,
        "works_anywhere": false,
        "manages_locations": [
            "Abay Ave"
        ],
        "manages_regions": [],
        "can_change_admin": true,
        "can_change_web_access": true
    },
    "role_ids": [
        41
    ],
    "system_role_ids": [
        1,
        2
    ],
    "personal": [
        {
            "key": "users.contacts",
            "effect": "grant",
            "scope": "list",
            "location_ids": [
                12
            ]
        }
    ],
    "effective": [
        {
            "key": "audits.view",
            "location_ids": null,
            "sources": [
                {
                    "source": "role",
                    "role_id": 41,
                    "role": "Auditor",
                    "scope": "all",
                    "location_ids": []
                }
            ]
        }
    ],
    "can_edit": true
}
```

### PATCH /access/users/{id}/account — Change administrator or web-cabinet access

`is_admin` — only an administrator can appoint or remove administrators (not themselves, not the company owner). `web_access: false` makes the employee "app only" (requires `users.access` for the employee). Logged.

MCP tool: `access_users_account_update` · Permission: `access.manage` or `access.roles` or `access.log` or `users.access`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | integer | yes | Employee id |
| `is_admin` | body | boolean | no |  |
| `web_access` | body | boolean | no |  |

Example response `data`:

```json
{
    "user": {
        "id": 311,
        "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
        "name": "Aigerim Sadykova"
    },
    "account": {
        "is_admin": false,
        "is_owner": false,
        "web_access": true,
        "works_anywhere": false,
        "manages_locations": [
            "Abay Ave"
        ],
        "manages_regions": [],
        "can_change_admin": true,
        "can_change_web_access": true
    },
    "role_ids": [
        41
    ],
    "system_role_ids": [
        1,
        2
    ],
    "personal": [
        {
            "key": "users.contacts",
            "effect": "grant",
            "scope": "list",
            "location_ids": [
                12
            ]
        }
    ],
    "effective": [
        {
            "key": "audits.view",
            "location_ids": null,
            "sources": [
                {
                    "source": "role",
                    "role_id": 41,
                    "role": "Auditor",
                    "scope": "all",
                    "location_ids": []
                }
            ]
        }
    ],
    "can_edit": true
}
```

### GET /access/log — List access log entries

Changes of roles and permissions, administrators and managers, views of sensitive data, exports and denied attempts — newest first.

MCP tool: `access_log_list` · Permission: `access.log`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `page` | query | integer | no | Page number, starting from 1 |
| `per_page` | query | integer | no | Items per page (max 100) |
| `user_id` | query | integer | no | Entries where this employee is the actor or the subject |
| `kind` | query | string (changes, view, export, denied) | no | Kind of entries |
| `permission` | query | string | no | Permission key |
| `date_from` | query | string | no | From date (inclusive) |
| `date_to` | query | string | no | To date (inclusive) |

Example response `data`:

```json
[
    {
        "id": 90211,
        "event": "role_assign",
        "event_label": "Role assigned",
        "actor": {
            "id": 120,
            "uuid": "7a6b5c4d-3e2f-4a1b-9c8d-7e6f5a4b3c2d",
            "name": "Daniyar Omarov"
        },
        "subject": {
            "id": 311,
            "uuid": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b",
            "name": "Aigerim Sadykova"
        },
        "role": {
            "id": 41,
            "name": "Auditor"
        },
        "permission": null,
        "location": null,
        "data": {
            "name": "Auditor"
        },
        "ip": "10.0.0.4",
        "created_at": "2026-11-03T10:15:00+05:00"
    }
]
```

## Help center

Search and read the jeCheck help center (jecheck.io/docs): how features work, where settings are, rules for scores, permissions and plans. Articles are returned in Markdown, in English by default. Available to every token.

### GET /help/search — Search the help center

Hybrid keyword and semantic search over help articles. Returns the best matching fragments (at most two per article) with a link to the exact section of the article.

MCP tool: `help_search`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `query` | query | string | yes | Question or keywords, e.g. "how to require a photo after fixing a violation" |
| `limit` | query | integer | no | Number of fragments |
| `locale` | query | string (en, ru, kk, uz, ka, hy, sr, de) | no | Language of the articles; falls back to English when the language has no articles |

Example response `data`:

```json
[
    {
        "article": "Corrective-action loop",
        "section": "automation",
        "slug": "closed-loop",
        "heading": "Photo after the fix",
        "excerpt": "…turn on \"Photo after\" so the assignee must attach a photo before sending the task for review…",
        "content": "## Photo after the fix\n\nTurn on 'Photo after'…",
        "url": "https://jecheck.io/en/docs/automation/closed-loop#photo-after-the-fix",
        "score": 0.03226
    }
]
```

### GET /help/sections — List help sections and articles

The table of contents of the help center: sections in display order with their articles.

MCP tool: `help_sections`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `locale` | query | string (en, ru, kk, uz, ka, hy, sr, de) | no | Language of the articles; falls back to English when the language has no articles |

Example response `data`:

```json
[
    {
        "key": "tasks",
        "title": "Tasks",
        "app_url": "https://app.jecheck.io/ui/tasks",
        "articles": [
            {
                "section": "tasks",
                "slug": "statuses",
                "title": "Task statuses",
                "description": "What each task status means and who can move a task.",
                "updated": "2026-10-17",
                "reading_time_minutes": 3,
                "url": "https://jecheck.io/en/docs/tasks/statuses"
            }
        ]
    }
]
```

### GET /help/articles/{section}/{slug} — Read a help article

Full article in Markdown. `section` and `slug` come from `help.sections` or `help.search` results.

MCP tool: `help_article`

| Parameter | In | Type | Required | Description |
|---|---|---|---|---|
| `section` | path | string | yes | Section key, e.g. `tasks` |
| `slug` | path | string | yes | Article slug, e.g. `statuses` |
| `locale` | query | string (en, ru, kk, uz, ka, hy, sr, de) | no | Language of the articles; falls back to English when the language has no articles |

Example response `data`:

```json
{
    "section": "tasks",
    "slug": "statuses",
    "title": "Task statuses",
    "description": "What each task status means and who can move a task.",
    "updated": "2026-10-17",
    "reading_time_minutes": 3,
    "url": "https://jecheck.io/en/docs/tasks/statuses",
    "markdown": "Every task moves through statuses…\n\n## New\n\nThe task is created…"
}
```
