Версия: v1
Аудитория: разработчики на стороне лаборатории/интегратора (REST).
Назначение: единая инструкция «с нуля до первого результата» — как подключиться, авторизоваться, забрать заявку из очереди клиники и вернуть результат анализа.
Документ обезличен. Все секреты, имена сервисов, домены клиник и идентификаторы заявок приведены как плейсхолдеры:
{service_name},{rest_api_key},{billing_authkey},{clinic_host},{request_id},{analyze_id}. Реальные значения выдаются при онбординге.
| Плейсхолдер | Что это | Где используется |
|---|---|---|
{service_name} | Идентификатор вашего сервиса-провайдера | Заголовок X-SERVICE-NAME |
{rest_api_key} | REST-ключ Provider API | Заголовок X-SERVICE-REST-API-KEY |
{billing_authkey} | Ключ для billing-api (получение списка доменов) | GET lab-domains/{billing_authkey} |
{clinic_host} | Хост конкретной клиники (CRM-инстанс) | Базовый URL всех вызовов Provider API |
{request_id} | ID заявки на исследование | Тело запросов take/waiting/result-* |
{analyze_id} | ID анализа внутри заявки | Тело запросов result-* |
Заявка (request) — единица работы: один питомец, один провайдер, набор анализов. Анализ (analyze) — отдельная позиция внутри заявки. Результат отправляется по каждому анализу отдельно; когда заполнены все анализы заявки — заявка переходит в done.
Для интеграции используются два разных ключа:
{billing_authkey} — выдаётся billing-сервисом. Нужен только для того, чтобы получить список клиник (хостов), подключивших вашу лабораторию.{rest_api_key} — REST-ключ вашего сервиса. Используется во всех вызовах Provider API вместе с {service_name}.Оба ключа и {service_name} выдаются при онбординге. Храните их как секреты, не публикуйте в репозиториях и клиентских приложениях.
Перед вызовом Provider API нужно узнать, на каком {clinic_host} работать. Список доступных вам клиник возвращает billing-api:
GET https://{billing_host}/lab-domains/{billing_authkey}Ответ — перечень доменов клиник, которые подключили вашу лабораторию и активировали интеграцию. Каждый элемент содержит хост клиники, который далее подставляется как {clinic_host}.
{
"success": true,
"data": [
{ "host": "{clinic_host}", "title": "..." }
// ...другие клиники
]
}Выбор
{clinic_host}: дальнейшая работа (list → take → result) ведётся отдельно по каждой клинике. Заявки одной клиники недоступны под хостом другой.
Все запросы к Provider API авторизуются двумя HTTP-заголовками:
X-SERVICE-NAME: {service_name}
X-SERVICE-REST-API-KEY: {rest_api_key}Базовый URL для всех эндпоинтов:
https://{clinic_host}/rest/api/provider-lab-requests/...Если хотя бы один заголовок отсутствует или ключ неверный — 401 UNAUTHORIZED. Если для клиники не подключён тариф/аддон или не включена интеграция — ответ маскируется под 404 NOT_FOUND (см. раздел 8).
lab-domains → получить список {clinic_host}
│
▼
GET list → увидеть очередь заявок клиники (taken/tranzit/waiting)
│
▼
POST take → взять заявку в работу: taken → tranzit
│
▼
POST waiting (опц.) → пометить «ожидаем результат»: tranzit → waiting
│
▼
POST result-html → отправить результат по анализу
или result-values (повторять для каждого analyze_id заявки)
│
▼
когда заполнены все анализы → заявка автоматически переходит в doneМинимальный happy path: list → take → result-\*. Шаг waiting нужен, когда результат готовится не сразу (длинная аналитика): он явно фиксирует, что заявка взята и ожидает результата.
| Статус | Значение | Виден в list? |
|---|---|---|
taken | Создана клиникой, ещё не взята провайдером | Да |
tranzit | Взята провайдером в работу (take) | Да |
waiting | Ожидает результата (waiting) | Да |
done | Все анализы заполнены — завершена | Нет |
cancelled / deleted / error | Терминальные | Нет (для провайдера = «не найдена») |
Правила переходов:
take: taken → tranzit. Идемпотентен — повторный вызов для заявки в tranzit или waiting вернёт текущий статус без ошибки. Для done → 409 ALREADY_TAKEN. Других целевых статусов у take нет — из taken можно попасть только в tranzit.waiting: tranzit → waiting. Идемпотентен для waiting. Из taken или done → 409 INVALID_STATUS_TRANSITION (сначала нужно take). Других переходов в waiting нет — попасть в этот статус можно только из tranzit.result-html/result-values) принимаются только из tranzit или waiting. Из других статусов → 409 INVALID_STATUS_TRANSITION.done не вызывается отдельным эндпоинтом — он выставляется автоматически, как только заполнен последний анализ заявки. Шаг waiting для этого не обязателен: заявка может дойти до done напрямую из tranzit (если результаты отправлены без промежуточной пометки ожидания) либо из waiting. Итоговый путь всегда один из двух: tranzit → done или tranzit → waiting → done.done гарантированно имеет все свои analyze_id заполненными (это и есть условие перехода в done). Поэтому повторная отправка результата по любому анализу такой заявки всегда вернёт 409 ALREADY_FILLED, а не 409 INVALID_STATUS_TRANSITION — проверка «анализ уже заполнен» выполняется раньше проверки статуса заявки. На практике INVALID_STATUS_TRANSITION для результатов возникает только у заявки в статусе taken (в ней по определению нет заполненных анализов, так как заполнение возможно лишь из tranzit/waiting).404 NOT_FOUND.Результат по анализу можно отправить в одном из двух форматов:
result-html — готовый HTML-фрагмент (например, отрендеренный бланк/таблица результатов). Подходит, когда лаборатория формирует визуальное представление сама.result-values — структурированный набор «параметр → значение → ед. измерения». Подходит для машиночитаемых показателей.Важно: формат фиксируется за заявкой по первому результату. Если первый результат отправлен как html, остальные анализы этой же заявки тоже должны идти html (и наоборот). Смешение → 409 INCOMPATIBLE_RESULT_FORMAT.
Оба формата поддерживают необязательные поля file_link, measured_at, comment.
Общие правила:
Content-Type: application/json)."success": true. Ошибка: "success": false + error_code + messages[].7.1. GET /list — очередь заявок
Параметры строки запроса (все необязательные):
| Параметр | Тип | По умолчанию | Ограничения |
|---|---|---|---|
status | string | все из taken,tranzit,waiting | только одно из taken, tranzit, waiting |
limit | int | 50 | от 1 до 200 |
offset | int | 0 | ≥ 0 |
Запрос:
GET /rest/api/provider-lab-requests/list?status=taken&limit=50&offset=0
Host: {clinic_host}
X-SERVICE-NAME: {service_name}
X-SERVICE-REST-API-KEY: {rest_api_key}Ответ 200:
{
"success": true,
"total": 1,
"data": {
"labanalysisrequest": [
{
"id": {request_id},
"status": "taken",
"sample_number": 12345,
"create_date": "2024-01-15 10:30:00",
"comment": "...",
"pet": {
"id": 0,
"alias": "...",
"type": "...",
"breed": "...",
"sex": "...",
"birthdate": "...",
"weight": "..."
},
"analyzes": [
{ "id": {analyze_id}, "code": "...", "title": "...", "filled": 0 }
]
}
]
}
}total — общее число заявок по фильтру (для пагинации), независимо от limit/offset.analyzes[].filled — 1, если по анализу уже принят результат, иначе 0.7.2. POST /take — взять заявку в работу
Тело:
{ "request_id": {request_id} }Ответ 200:
{
"success": true,
"messages": ["Request accepted"],
"data": { "request_id": {request_id}, "request_status": "tranzit" }
}7.3. POST /waiting — пометить ожидание результата
Тело:
{ "request_id": {request_id} }Ответ 200:
{
"success": true,
"messages": ["Request moved to waiting"],
"data": { "request_id": {request_id}, "request_status": "waiting" }
}7.4. POST /result-html — результат в формате HTML
Тело:
| Поле | Тип | Обяз. | Ограничения |
|---|---|---|---|
request_id | int | да | > 0 |
analyze_id | int | да | анализ должен принадлежать заявке |
html | string | да | не пусто; ≤ 1 МБ; без <script>/<iframe>/on*=-атрибутов |
file_link | string | нет | URL только с разрешённого файлового хоста (см. 8) |
measured_at | string | нет | ISO 8601, напр. 2024-01-15T10:30:00+02:00 |
comment | string | нет | произвольный текст |
HTML дополнительно санитизируется на сервере (вырезаются неразрешённые теги и атрибуты). Разрешён ограниченный набор форматирующих тегов и таблиц.
Запрос:
POST /rest/api/provider-lab-requests/result-html
Host: {clinic_host}
Content-Type: application/json
X-SERVICE-NAME: {service_name}
X-SERVICE-REST-API-KEY: {rest_api_key}
{
"request_id": {request_id},
"analyze_id": {analyze_id},
"html": "<table><tr><td>Hemoglobin</td><td>140 g/L</td></tr></table>",
"measured_at": "2024-01-15T10:30:00+02:00",
"comment": "..."
}Ответ 200:
{
"success": true,
"messages": ["Result saved"],
"data": {
"request_id": {request_id},
"analyze_id": {analyze_id},
"medcard_id": 0,
"is_all_filled": false,
"request_status": "waiting"
}
}is_all_filled — true, когда это был последний незаполненный анализ заявки.request_status — станет done, как только is_all_filled = true.7.5. POST /result-values — структурированный результат
Тело:
| Поле | Тип | Обяз. | Ограничения |
|---|---|---|---|
request_id | int | да | > 0 |
analyze_id | int | да | анализ должен принадлежать заявке |
values | array | да | непустой массив объектов (см. ниже) |
file_link | string | нет | URL только с разрешённого файлового хоста |
measured_at | string | нет | ISO 8601 |
comment | string | нет | произвольный текст |
Каждый элемент values:
| Поле | Тип | Обяз. | Ограничения |
|---|---|---|---|
name | string | да | ≤ 50 симв.; приводится к верхнему регистру; уникален в пределах values |
value | string | да | ≤ 255 симв. |
unit | string | нет | ≤ 50 симв. |
Запрос:
POST /rest/api/provider-lab-requests/result-values
Host: {clinic_host}
Content-Type: application/json
X-SERVICE-NAME: {service_name}
X-SERVICE-REST-API-KEY: {rest_api_key}
{
"request_id": {request_id},
"analyze_id": {analyze_id},
"values": [
{ "name": "HGB", "value": "140", "unit": "g/L" },
{ "name": "WBC", "value": "8.5", "unit": "10^9/L" }
],
"measured_at": "2024-01-15T10:30:00+02:00"
}Ответ 200: структура идентична result-html (см. 7.4).
Формат тела ошибки одинаков для всех эндпоинтов:
{
"success": false,
"error_code": "NOT_FOUND",
"messages": ["Заявка не найдена"]
}8.1. По HTTP-кодам
| HTTP | error_code | Когда возникает | Что делать |
|---|---|---|---|
401 | UNAUTHORIZED | Нет одного из заголовков или неверный {rest_api_key} | Проверить заголовки и ключ |
404 | NOT_FOUND | Аддон/интеграция выключены; провайдер не найден; заявка не найдена/в терминальном статусе; анализ не принадлежит заявке | Проверить статус заявки и настройки клиники (раздел 9) |
400 | INVALID_PAYLOAD | Невалидные/отсутствующие поля, нарушены длины/формат measured_at, недопустимый status/limit/offset | Исправить тело/параметры |
400 | INVALID_HTML | В html есть запрещённые элементы/атрибуты | Убрать <script>/<iframe>/on*= |
400 | INVALID_FILE_LINK | file_link не URL или хост не из разрешённого списка | Использовать допустимый файловый хост |
400 | DUPLICATE_KEYS | Повтор name внутри values | Сделать ключи уникальными |
409 | ALREADY_TAKEN | take для уже завершённой (done) заявки | Заявка закрыта, действий не требуется |
409 | INVALID_STATUS_TRANSITION | Недопустимый переход (waiting из taken/done; результат — из taken). Для заявки в done вместо этого кода приходит ALREADY_FILLED (см. ниже) | Привести заявку к нужному статусу (take → waiting) |
409 | INCOMPATIBLE_RESULT_FORMAT | Смешение html и values в одной заявке | Использовать формат первого результата |
409 | ALREADY_FILLED | По анализу результат уже отправлен, в т.ч. любой повторный результат по завершённой (done) заявке — все её анализы гарантированно заполнены, и эта проверка выполняется раньше проверки статуса | Не отправлять повторно |
8.2. Особый случай: 200 + total = 0
GET /list вернул success: true, но total: 0 и пустой labanalysisrequest. Это не ошибка авторизации — ключи приняты. Причины:
status;{clinic_host} идёт запрос;Проверьте: правильный ли {clinic_host}, создана ли заявка на провайдера, включена ли интеграция per-клиника.
Чтобы заявки клиники стали видны через Provider API, на стороне клиники должны выполняться все условия:
taken.Если интеграция выключена или заявок нет — провайдер увидит 404 NOT_FOUND на адресные операции и total = 0 в list.
Готовая коллекция: provider-api-v1.postman_collection.json.
Импортируйте коллекцию и задайте переменные окружения (Environment):
| Переменная | Значение |
|---|---|
clinic_host | хост клиники (без протокола), например {clinic_host} |
service_name | {service_name} |
rest_api_key | {rest_api_key} |
billing_host | хост billing-api |
billing_authkey | {billing_authkey} |
request_id | ID заявки для take/waiting/result |
analyze_id | ID анализа для result |
Секреты в коллекцию не зашиты — все значения берутся из переменных окружения.
# 0. Список доменов клиник
curl -s "https://{billing_host}/lab-domains/{billing_authkey}"
# 1. Очередь заявок клиники
curl -s "https://{clinic_host}/rest/api/provider-lab-requests/list?status=taken&limit=50" \
-H "X-SERVICE-NAME: {service_name}" \
-H "X-SERVICE-REST-API-KEY: {rest_api_key}"
# 2. Взять заявку в работу
curl -s -X POST "https://{clinic_host}/rest/api/provider-lab-requests/take" \
-H "X-SERVICE-NAME: {service_name}" \
-H "X-SERVICE-REST-API-KEY: {rest_api_key}" \
-H "Content-Type: application/json" \
-d '{"request_id": {request_id}}'
# 3. (опционально) Пометить ожидание результата
curl -s -X POST "https://{clinic_host}/rest/api/provider-lab-requests/waiting" \
-H "X-SERVICE-NAME: {service_name}" \
-H "X-SERVICE-REST-API-KEY: {rest_api_key}" \
-H "Content-Type: application/json" \
-d '{"request_id": {request_id}}'
# 4a. Результат в формате HTML
curl -s -X POST "https://{clinic_host}/rest/api/provider-lab-requests/result-html" \
-H "X-SERVICE-NAME: {service_name}" \
-H "X-SERVICE-REST-API-KEY: {rest_api_key}" \
-H "Content-Type: application/json" \
-d '{
"request_id": {request_id},
"analyze_id": {analyze_id},
"html": "<table><tr><td>HGB</td><td>140 g/L</td></tr></table>",
"measured_at": "2024-01-15T10:30:00+02:00"
}'
# 4b. Результат в формате values
curl -s -X POST "https://{clinic_host}/rest/api/provider-lab-requests/result-values" \
-H "X-SERVICE-NAME: {service_name}" \
-H "X-SERVICE-REST-API-KEY: {rest_api_key}" \
-H "Content-Type: application/json" \
-d '{
"request_id": {request_id},
"analyze_id": {analyze_id},
"values": [
{"name": "HGB", "value": "140", "unit": "g/L"},
{"name": "WBC", "value": "8.5", "unit": "10^9/L"}
],
"measured_at": "2024-01-15T10:30:00+02:00"
}'По вопросам подключения, выдачи ключей и работы Provider API обращайтесь в поддержку Vetmanager по официальным каналам, указанным в вашем договоре/онбординге.