{"openapi":"3.0.0","info":{"title":"Toolkeeper Public API","description":"Public REST API for [Toolkeeper](https://toolkeeper.io) — onboarding-focused v1.\n\n**Authentication.** All endpoints (except `/health`, `/openapi.json`, `/docs`) require an API key passed as `Authorization: Bearer <key>`. Keys are minted by an admin in the client admin panel and scoped to a single client (and optionally a single workspace).\n\n**Errors.** 4xx/5xx responses follow [RFC 7807](https://www.rfc-editor.org/rfc/rfc7807) with content-type `application/problem+json`.\n\n**Idempotency.** POST endpoints accept an optional `Idempotency-Key` header. Repeating the same key with the same body replays the cached response (with `Idempotent-Replay: true`); replaying with a different body returns `409`.\n\n**Rate limits.** 60 req/60s on most endpoints; 5 req/60s on `*/bulk` endpoints. Every response carries `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (plus the legacy `X-Rate-Limit-*` names); a `429` also carries `Retry-After`. Back off on it — a burst that ignores the header just earns more `429`s.","contact":{"name":"Toolkeeper Support","email":"sales@toolkeeper.io"},"version":"1.4.0"},"paths":{"/dictionaries/storages":{"get":{"tags":["dictionaries"],"summary":"Active storages (склады) of the client.","operationId":"08cfdd73260c02e4eaae1fbda70b7575","parameters":[{"name":"workspace_id","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer"},"title":{"type":"string"},"workspace_id":{"type":"integer"}},"type":"object"}}},"type":"object"}}}}},"security":[{"bearerAuth":[]}]}},"/dictionaries/building-sites":{"get":{"tags":["dictionaries"],"summary":"Building sites / objects (объекты) of the client.","operationId":"e6128c32d13d09583c6b9f07bd6a9882","parameters":[{"name":"workspace_id","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer"},"name":{"type":"string"},"workspace_id":{"type":"integer"}},"type":"object"}}},"type":"object"}}}}},"security":[{"bearerAuth":[]}]}},"/dictionaries/statuses":{"get":{"tags":["dictionaries"],"summary":"Taxonomy: categories, brands, item statuses and job titles.","description":"All four bundles live in one table. Filter with `bundle` to get just one list.","operationId":"4d2992eb3eb4a2e23ef5a1c88fb3e770","parameters":[{"name":"bundle","in":"query","required":false,"schema":{"type":"string","enum":["category","brand","status","post"]}},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"properties":{"id":{"type":"integer"},"title":{"type":"string"},"bundle":{"type":"string"},"workspace_id":{"type":"integer"}},"type":"object"}}},"type":"object"}}}},"422":{"description":"Unknown bundle","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"bearerAuth":[]}]}},"/employees":{"get":{"tags":["employees"],"summary":"List employees with filters and pagination.","description":"GET /openapi/v1/employees\n\nFilters: ?workspace_id=&role=&phone=&q=&page=&per_page= (max 200).\nScoped to the API key's client (and key's workspace_id if set).","operationId":"e2dbece7dbe88cd01dd480514974ac72","parameters":[{"name":"workspace_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"role","in":"query","required":false,"schema":{"type":"string"}},{"name":"phone","in":"query","description":"Digit substring match","required":false,"schema":{"type":"string"}},{"name":"q","in":"query","description":"Free-text on name/email","required":false,"schema":{"type":"string"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/EmployeeOutput"}},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}},"type":"object"}}}}},"security":[{"bearerAuth":[]}]},"post":{"tags":["employees"],"summary":"Create one employee (no SMS).","description":"Phone is unique system-wide. Server-generated password is returned ONCE if `password` is omitted. Combine with `Idempotency-Key` for retry safety.","operationId":"2a8c3763a8f97f54c5c483be408a6ad2","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmployeeInput"}}}},"responses":{"200":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmployeeOutput"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Missing scope: employees:write"},"409":{"description":"Idempotency-Key reuse with different body"},"429":{"description":"Rate limit exceeded"}},"security":[{"bearerAuth":[]}]}},"/employees/bulk":{"post":{"tags":["employees"],"summary":"Create up to 500 employees, partial-success aware.","description":"Each row runs in its own DB transaction. Returns 200 with `results[]` per row + `summary{total,created,failed}`. Combine with `Idempotency-Key` to make retries safe.","operationId":"5eca388d19ad4f632e45d3cd5a4b4d2c","requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"employees":{"type":"array","items":{"$ref":"#/components/schemas/EmployeeInput"},"maxItems":500}},"type":"object"}}}},"responses":{"200":{"description":"Per-row outcomes","content":{"application/json":{"schema":{"properties":{"results":{"type":"array","items":{"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["created","error"]},"id":{"type":"integer","nullable":true},"password":{"type":"string","nullable":true},"errors":{"type":"array","items":{"type":"object"},"nullable":true}},"type":"object"}},"summary":{"properties":{"total":{"type":"integer"},"created":{"type":"integer"},"failed":{"type":"integer"}},"type":"object"}},"type":"object"}}}},"422":{"description":"Body too large or all rows invalid","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded (5 calls / 60s for /bulk)"}},"security":[{"bearerAuth":[]}]}},"/employees/{id}":{"get":{"tags":["employees"],"summary":"Fetch one employee.","description":"GET /openapi/v1/employees/{id}","operationId":"aa53f4b9de918b48235c337ba25b9f16","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmployeeOutput"}}}},"404":{"description":"Not found / outside scope"}},"security":[{"bearerAuth":[]}]},"delete":{"tags":["employees"],"summary":"Soft-delete an employee (full offboarding).","description":"Mirrors the cabinet flow: reassigns the employee's items to each workspace's owner (audited History records), removes workspace memberships, frees the phone number for future re-registration, decrements the client's billing counter. Workspace owners (wsCreator) cannot be deleted.","operationId":"0a536b3f306f30d96628a9000df63beb","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"responses":{"204":{"description":"No Content"},"404":{"description":"Not found / outside scope"},"422":{"description":"Cannot delete a workspace owner","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"bearerAuth":[]}]},"patch":{"tags":["employees"],"summary":"Partial update of name/email/post/password.","description":"Workspaces and roles are NOT modifiable here — use dedicated endpoints to avoid accidental wipes.","operationId":"d0116c5d88bc0a2c066323b1d0b91ef6","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"first_name":{"type":"string"},"last_name":{"type":"string"},"middle_name":{"type":"string"},"email":{"type":"string","format":"email"},"post":{"type":"string"},"password":{"type":"string"}},"type":"object"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmployeeOutput"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"bearerAuth":[]}]}},"/employees/{id}/roles":{"post":{"tags":["employees"],"summary":"Replace the employee's role/permission set.","description":"Computes the diff and only revokes/assigns deltas via authManager. Names must be from `/roles`.","operationId":"396ca232c2e3ab74c00f195920af9aa7","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"roles":{"type":"array","items":{"type":"string"}}},"type":"object"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmployeeOutput"}}}},"422":{"description":"Unknown role names","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"bearerAuth":[]}]}},"/inventories":{"get":{"tags":["inventories"],"summary":"List inventory campaigns.","description":"Campaigns across the key's active workspaces. Heavier than DB-backed lists: one gRPC call per workspace — pass `workspace_id` to narrow it.","operationId":"45e5ce6177d9d7b67f4295fdff5dc50e","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["INVENTORY_CREATE","INVENTORY_COMPLETED","INVENTORY_VERIFICATION","INVENTORY_CANCEL"]}},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/InventorySummary"}},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}},"type":"object"}}}},"403":{"description":"Missing scope: inventories:read"},"503":{"description":"Inventory service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"bearerAuth":[]}]}},"/inventories/{id}":{"get":{"tags":["inventories"],"summary":"Fetch one inventory campaign in full.","description":"Includes warehouse, objects, categories, responsibles and the nested users[].items[] with per-item check results.","operationId":"842034c112f8095ed86aac5db5238a23","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK"},"404":{"description":"Not found / outside scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Inventory service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"bearerAuth":[]}]}},"/inventories/{id}/report":{"get":{"tags":["inventories"],"summary":"Inventory result report as JSON (found / not found / discrepancies / cost).","description":"The same numbers the cabinet PDF shows: totals, per-result counts, found vs missing cost, and a per-employee breakdown with every checked item.","operationId":"370c40da655116e914d4f708b9256348","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/InventoryReport"}}}},"404":{"description":"Not found / outside scope","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"503":{"description":"Inventory service unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}},"security":[{"bearerAuth":[]}]}},"/items":{"get":{"tags":["items"],"summary":"List inventory items with filters, pagination and enriched names.","description":"GET /openapi/v1/items — list items scoped to the key's client (active\nworkspaces only). Filters: workspace_id, responsible_user_id, category_id,\nbrand_id, status_id, storage_id, building_site_id, quantitative(0|1),\nq (title/serial/internal_id substring). Pagination: page, per_page(≤200).\nEnriched with taxonomy NAMES so a dashboard needs no extra calls.","operationId":"65e1e90852c29df6ff91085df4022c45","parameters":[{"name":"workspace_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"responsible_user_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"category_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"brand_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"status_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"storage_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"building_site_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"quantitative","in":"query","required":false,"schema":{"type":"integer","enum":[0,1]}},{"name":"q","in":"query","description":"Substring on title/serialnumber/internal_id","required":false,"schema":{"type":"string"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ItemOutput"}},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}},"type":"object"}}}},"403":{"description":"Missing scope: items:read"}},"security":[{"bearerAuth":[]}]},"post":{"tags":["items"],"summary":"Create one item with name-based taxonomy auto-create.","description":"brand/category/status/building_site/storage are STRING names; missing entries are auto-created in the workspace (mirrors the Excel import).","operationId":"8cf4f9abb5b0ed4e0a49e692baed641f","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemInput"}}}},"responses":{"200":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemOutput"}}}},"422":{"description":"Validation failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"403":{"description":"Missing scope: items:write"}},"security":[{"bearerAuth":[]}]}},"/items/bulk":{"post":{"tags":["items"],"summary":"Create up to 500 items with auto-taxonomy.","description":"POST /openapi/v1/items/bulk\n\nBody: {\"items\":[{...},{...}]} up to 500 rows. Per-row transactions,\npartial success visible. 200 with summary.","operationId":"cb9d808e95ac4b72abec7d379d098ec4","requestBody":{"required":true,"content":{"application/json":{"schema":{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemInput"},"maxItems":500}},"type":"object"}}}},"responses":{"200":{"description":"Per-row outcomes","content":{"application/json":{"schema":{"properties":{"results":{"type":"array","items":{"properties":{"index":{"type":"integer"},"status":{"type":"string","enum":["created","error"]},"id":{"type":"integer","nullable":true},"w_id":{"type":"integer","nullable":true},"errors":{"type":"array","items":{"type":"object"},"nullable":true}},"type":"object"}},"summary":{"properties":{"total":{"type":"integer"},"created":{"type":"integer"},"failed":{"type":"integer"}},"type":"object"}},"type":"object"}}}},"422":{"description":"Body too large","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"429":{"description":"Rate limit exceeded"}},"security":[{"bearerAuth":[]}]}},"/items/{id}":{"get":{"tags":["items"],"summary":"Fetch one item (enriched with taxonomy names).","description":"GET /openapi/v1/items/{id}","operationId":"0cfb1db1aa30ff704679f024a89cab4c","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ItemOutput"}}}},"404":{"description":"Not found / outside scope"}},"security":[{"bearerAuth":[]}]}},"/items/{id}/history":{"get":{"tags":["items"],"summary":"Full audit trail of an item: field changes and transfers, chronologically.","description":"The same unified history the cabinet card shows — every field change plus every transfer, with names resolved and timestamps in the workspace timezone.","operationId":"8be7abd56e0cb234b88a206875f0e5f8","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"description":"Chronological entries (type=change|transfer) with timestamp, user and description","type":"array","items":{"type":"object"}}},"type":"object"}}}},"404":{"description":"Not found / outside scope"}},"security":[{"bearerAuth":[]}]}},"/health":{"get":{"tags":["meta"],"summary":"Liveness probe (unauthenticated).","operationId":"4144346458867a9ba6791e94c47882c5","responses":{"200":{"description":"OK","content":{"application/json":{"schema":[],"example":{"ok":true,"time":"2026-05-08T16:00:00+03:00"}}}}}}},"/me":{"get":{"tags":["meta"],"summary":"Who am I: identifies the key, its client, its workspace binding and its scopes.","description":"The first call to make with a new key. Requires authentication but no scope, so it\nalways answers if the key is valid — which makes it the one endpoint that can tell a\ncaller *why* their other calls fail (wrong client, workspace-bound key, missing scope,\nexpiring soon). It never returns any part of the secret.","operationId":"731cd5211d738a24d2c1799e2f06a9c8","responses":{"200":{"description":"OK"},"401":{"description":"Missing or invalid key"}},"security":[{"bearerAuth":[]}]}},"/openapi.json":{"get":{"tags":["meta"],"summary":"Machine-readable API spec (OpenAPI 3.1).","operationId":"ff526f1d057dc96538bb44164340d651","responses":{"200":{"description":"OK"}}}},"/docs":{"get":{"tags":["meta"],"summary":"Swagger UI for the spec at /openapi.json.","operationId":"354c6f84f68245b0528d126960427631","responses":{"200":{"description":"HTML"}}}},"/roles":{"get":{"tags":["roles"],"summary":"List assignable RBAC role and permission names.","description":"Whitelist mirrors `app\\models\\Role::class` constants — internal `admin_ws_<id>` roles are filtered out. Returned names are valid for `roles[]` in employee endpoints.","operationId":"948d9554b04dc6fb1f6552d01f85d164","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Role"}}},"type":"object"}}}},"403":{"description":"Missing scope: roles:read"}},"security":[{"bearerAuth":[]}]}},"/stats":{"get":{"tags":["stats"],"summary":"Aggregate dashboard counters for the client.","description":"Totals + breakdowns (items per workspace, per category, per status) scoped to the key's active workspaces. Read-only, safe for polling a dashboard.","operationId":"dd1b99d7ac2734cc5a0054cb9566cef8","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"totals":{"properties":{"employees":{"type":"integer"},"workspaces":{"type":"integer"},"storages":{"type":"integer"},"items":{"type":"integer"},"items_quantitative":{"type":"integer"},"inventory_value":{"type":"number","format":"float"}},"type":"object"},"items_by_workspace":{"type":"array","items":{"type":"object"}},"items_by_category":{"type":"array","items":{"type":"object"}},"items_by_status":{"type":"array","items":{"type":"object"}}},"type":"object"}}}},"403":{"description":"Missing scope: stats:read"}},"security":[{"bearerAuth":[]}]}},"/transfers":{"get":{"tags":["transfers"],"summary":"List transfers, including stuck (sent but not accepted) ones.","operationId":"fa9c209a864f598491564f246c7c449f","parameters":[{"name":"stuck","in":"query","description":"stuck=1 → only sent-but-unaccepted transfers (confirmed=1, accepted=0)","required":false,"schema":{"type":"integer","enum":[0,1]}},{"name":"status","in":"query","required":false,"schema":{"type":"integer","enum":[0,1,2,3]}},{"name":"workspace_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"item_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"user_from","in":"query","required":false,"schema":{"type":"integer"}},{"name":"user_to","in":"query","required":false,"schema":{"type":"integer"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Transfer"}},"page":{"type":"integer"},"per_page":{"type":"integer"},"total":{"type":"integer"}},"type":"object"}}}},"403":{"description":"Missing scope: transfers:read"}},"security":[{"bearerAuth":[]}]}},"/transfers/{id}":{"get":{"tags":["transfers"],"summary":"Fetch one transfer.","operationId":"24c29a583185012b655ac71e895797e4","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transfer"}}}},"404":{"description":"Not found / outside scope"}},"security":[{"bearerAuth":[]}]}},"/workspaces":{"get":{"tags":["workspaces"],"summary":"List workspaces visible to the API key.","description":"Scoped to the key's client (and to the key's workspace_id if set when minted).","operationId":"a8c5a492012a0411ceae72a7ea49802d","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Workspace"}}},"type":"object"}}}},"401":{"description":"Unauthorized"},"403":{"description":"Missing scope: workspaces:read"}},"security":[{"bearerAuth":[]}]}},"/workspaces/{id}":{"get":{"tags":["workspaces"],"summary":"Fetch a single workspace by id.","operationId":"c2c4ea502b906fd55de50db2d6b6f04b","parameters":[{"name":"id","in":"path","schema":{"type":"integer"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Workspace"}}}},"404":{"description":"Not found / outside scope"}},"security":[{"bearerAuth":[]}]}}},"components":{"schemas":{"EmployeeInput":{"required":["phone","country_code","first_name","last_name"],"properties":{"phone":{"description":"Digits only, length per country code (10 for +7).","type":"string","example":"9999998877"},"country_code":{"type":"string","enum":["+7","+380","+375"]},"first_name":{"type":"string"},"last_name":{"type":"string"},"middle_name":{"type":"string","nullable":true},"email":{"type":"string","format":"email","nullable":true},"post":{"description":"Job title — auto-created in workspace if new","type":"string","nullable":true},"workspace_ids":{"type":"array","items":{"type":"integer"}},"roles":{"description":"Role/permission names from /roles","type":"array","items":{"type":"string"}},"password":{"description":"If omitted, server generates one and returns it ONCE in the response.","type":"string","nullable":true}},"type":"object","additionalProperties":false},"EmployeeOutput":{"properties":{"id":{"type":"integer"},"phone":{"type":"string"},"country_code":{"type":"string"},"first_name":{"type":"string"},"last_name":{"type":"string"},"middle_name":{"type":"string"},"email":{"type":"string"},"workspace_ids":{"type":"array","items":{"type":"integer"}},"roles":{"type":"array","items":{"type":"string"}},"status":{"type":"integer","enum":[0,1]},"created_at":{"type":"string"},"password":{"description":"Present only when server-generated.","type":"string","nullable":true},"password_note":{"type":"string","nullable":true}},"type":"object"},"Problem":{"description":"RFC 7807 problem+json error.","properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"errors":{"type":"array","items":{"properties":{"field":{"type":"string","nullable":true},"code":{"type":"string"},"message":{"type":"string"}},"type":"object"}}},"type":"object"},"InventorySummary":{"properties":{"id":{"description":"Read-only inventory (\"инвентаризация\") endpoints.\n\nData lives in inventory_ms and is reached over gRPC — there is no local table.\nThe RPC only returns a full campaign when the acting user is the campaign\nOWNER, so we act as the client's wsCreator admin (clientOwnerUserId) and loop\nthe client's active workspaces, mirroring the admin panel\n(modules/admin/controllers/ClientsController::actionInventoryList).","type":"integer"},"workspace_id":{"type":"integer"},"name":{"type":"string","nullable":true},"status":{"type":"string","enum":["INVENTORY_CREATE","INVENTORY_COMPLETED","INVENTORY_VERIFICATION","INVENTORY_CANCEL"]},"start_date":{"type":"string","nullable":true},"end_date":{"type":"string","nullable":true},"total_item":{"type":"integer"},"count_verify":{"type":"integer"},"owner_name":{"type":"string","nullable":true},"users_count":{"type":"integer"}},"type":"object"},"InventoryReport":{"description":"Result stats — the same numbers the cabinet PDF report shows.","properties":{"totalPositions":{"description":"Позиций в инвентаризации","type":"integer"},"foundCount":{"description":"Найдено","type":"integer"},"notFoundCount":{"description":"Не найдено","type":"integer"},"attentionCount":{"description":"Найдено с замечаниями","type":"integer"},"foundCost":{"description":"Стоимость найденного","type":"number","format":"float"},"missingCost":{"description":"Стоимость недостачи","type":"number","format":"float"},"perUser":{"type":"array","items":{"properties":{"id":{"type":"integer"},"name":{"type":"string"},"total":{"type":"integer"},"found":{"type":"integer"},"notFound":{"type":"integer"},"attention":{"type":"integer"},"items":{"type":"array","items":{"type":"object"}}},"type":"object"}}},"type":"object"},"ItemInput":{"required":["workspace_id","title"],"properties":{"workspace_id":{"type":"integer"},"title":{"type":"string"},"responsible_user_id":{"type":"integer","nullable":true},"brand":{"description":"Auto-created in workspace if new","type":"string","nullable":true},"category":{"description":"Auto-created","type":"string","nullable":true},"status":{"description":"Auto-created","type":"string","nullable":true},"building_site":{"description":"Auto-created if responsible_user_id is set","type":"string","nullable":true},"storage":{"description":"Auto-created if responsible_user_id is set","type":"string","nullable":true},"serialnumber":{"type":"string","nullable":true},"internal_id":{"type":"string","nullable":true},"price":{"type":"number","format":"float","nullable":true},"tags":{"type":"string","maxLength":399,"nullable":true},"quantity":{"description":"Quantity ≠ 1 (or an explicit unit) makes the item quantitative, like the Excel import","type":"number","format":"float","minimum":0,"nullable":true},"units":{"description":"Unit code; defaults to pcs when only quantity is given","type":"string","enum":["pcs","t","kg","g","m","m2","m3","l"],"nullable":true}},"type":"object","additionalProperties":false},"ItemOutput":{"properties":{"id":{"type":"integer"},"w_id":{"description":"Workspace-internal sequential id","type":"integer"},"workspace_id":{"type":"integer"},"title":{"type":"string"},"responsible_user_id":{"type":"integer","nullable":true},"responsible_user_name":{"type":"string","nullable":true},"category_id":{"type":"integer","nullable":true},"category_name":{"type":"string","nullable":true},"brand_id":{"type":"integer","nullable":true},"brand_name":{"type":"string","nullable":true},"status_id":{"type":"integer","nullable":true},"status_name":{"type":"string","nullable":true},"building_site_id":{"type":"integer","nullable":true},"building_site_name":{"type":"string","nullable":true},"storage_id":{"type":"integer","nullable":true},"storage_name":{"type":"string","nullable":true},"serialnumber":{"type":"string","nullable":true},"internal_id":{"type":"string","nullable":true},"price":{"type":"number","format":"float","nullable":true},"tags":{"type":"string","nullable":true},"quantitative":{"type":"integer","enum":[0,1]},"quantity":{"type":"number","format":"float","nullable":true},"units":{"type":"string","nullable":true},"created_at":{"type":"string","nullable":true}},"type":"object"},"Role":{"properties":{"name":{"description":"Lists assignable RBAC roles + permissions so API consumers know what\nrole names to pass when creating/updating employees.\n\nThe Toolkeeper RBAC model is flat — there's no real hierarchy. Both\nYii2 \"roles\" (e.g. wsCreator, wsGoodsEditor) and \"permissions\" (e.g.\ncanAddUsers, qrAutoAccept) are assigned to users interchangeably via\nauthManager->assign(). We return both so the caller can pick.","type":"string","example":"wsCreator"},"description":{"type":"string","nullable":true},"type":{"type":"string","enum":["role","permission"]}},"type":"object"},"Transfer":{"properties":{"id":{"description":"Read-only transfers (\"передачи\").\n\nCreating transfers stays in the app/cabinet on purpose — the API only reads.\n\nSCOPING GOTCHA: items_transfers has NO workspace_id column; a transfer's\nworkspace lives on the transferred item. Everything is therefore scoped with\njoinWith('items') → items.workspace_id IN (client's active workspaces),\nthe same way the cabinet does it (models/ItemsTransfers.php, TransferController).","type":"integer"},"w_id":{"description":"Workspace-internal transfer number","type":"integer","nullable":true},"workspace_id":{"description":"Taken from the transferred item","type":"integer"},"item_id":{"type":"integer"},"item_name":{"type":"string","nullable":true},"quantity":{"type":"number","format":"float","nullable":true},"status":{"description":"0 в процессе · 1 завершён · 2 отменён · 3 отклонён","type":"integer","enum":[0,1,2,3]},"status_label":{"type":"string"},"confirmed":{"description":"0 = draft in sender basket, 1 = sent","type":"integer","enum":[0,1]},"is_stuck":{"description":"Sent but still not accepted","type":"boolean"},"pending_seconds":{"description":"Set only when is_stuck","type":"integer","nullable":true},"from_user":{"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object","nullable":true},"to_user":{"properties":{"id":{"type":"integer"},"name":{"type":"string"}},"type":"object","nullable":true},"from_object":{"type":"string","nullable":true},"to_object":{"type":"string","nullable":true},"storage":{"type":"string","nullable":true},"comments":{"type":"string","nullable":true},"time":{"description":"Unix ts of the last change","type":"integer","nullable":true}},"type":"object"},"Workspace":{"properties":{"id":{"type":"integer","example":5883},"title":{"type":"string","example":"Main warehouse"},"name":{"type":"string"},"address":{"type":"string"},"timezone":{"type":"string","example":"Europe/Moscow"},"creator_user_id":{"type":"integer","nullable":true}},"type":"object"}},"securitySchemes":{"bearerAuth":{"type":"http","bearerFormat":"tk_live_*","scheme":"bearer"}}},"tags":[{"name":"meta","description":"Health, spec, docs"},{"name":"workspaces"},{"name":"roles"},{"name":"employees"},{"name":"items"},{"name":"stats"},{"name":"inventories","description":"Inventory campaigns, results and discrepancies (read-only)"},{"name":"transfers","description":"Item transfers incl. stuck ones (read-only)"},{"name":"dictionaries","description":"Reference lists for dashboard filters"}],"servers":[{"url":"https://toolkeeper.io/openapi/v1","description":"This host"}]}