Интеграция с модулем Лаборатория

Как подключиться, авторизоваться, забрать заявку из очереди клиники и вернуть результат анализа через Provider API v1

Версия: v1 

Аудитория: разработчики на стороне лаборатории/интегратора (REST). 

Назначение: единая инструкция «с нуля до первого результата» — как подключиться, авторизоваться, забрать заявку из очереди клиники и вернуть результат анализа.

Документ обезличен. Все секреты, имена сервисов, домены клиник и идентификаторы заявок приведены как плейсхолдеры: {service_name}, {rest_api_key}, {billing_authkey}, {clinic_host}, {request_id}, {analyze_id}. Реальные значения выдаются при онбординге.


Содержание

  1. Термины и плейсхолдеры
  2. Онбординг и ключи
  3. Авторизация
  4. Общий сценарий работы
  5. Статусы заявки
  6. Типы результата: html и values
  7. Эндпоинты — контракт и примеры
  8. Ошибки
  9. Требования на стороне клиники
  10. Postman
  11. Приложение A. Примеры curl
  12. Приложение B. Контакты поддержки

1. Термины и плейсхолдеры

ПлейсхолдерЧто этоГде используется
{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.


2. Онбординг и ключи

Для интеграции используются два разных ключа:

  1. {billing_authkey} — выдаётся billing-сервисом. Нужен только для того, чтобы получить список клиник (хостов), подключивших вашу лабораторию.
  2. {rest_api_key} — REST-ключ вашего сервиса. Используется во всех вызовах Provider API вместе с {service_name}.

Оба ключа и {service_name} выдаются при онбординге. Храните их как секреты, не публикуйте в репозиториях и клиентских приложениях.

2.1. Получение списка доменов клиник

Перед вызовом 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) ведётся отдельно по каждой клинике. Заявки одной клиники недоступны под хостом другой.


3. Авторизация

Все запросы к 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).


4. Общий сценарий работы

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 нужен, когда результат готовится не сразу (длинная аналитика): он явно фиксирует, что заявка взята и ожидает результата.


5. Статусы заявки

СтатусЗначениеВиден в list?
takenСоздана клиникой, ещё не взята провайдеромДа
tranzitВзята провайдером в работу (take)Да
waitingОжидает результата (waiting)Да
doneВсе анализы заполнены — завершенаНет
cancelled / deleted / errorТерминальныеНет (для провайдера = «не найдена»)

Правила переходов:

  • take: taken → tranzit. Идемпотентен — повторный вызов для заявки в tranzit или waiting вернёт текущий статус без ошибки. Для done409 ALREADY_TAKEN. Других целевых статусов у take нет — из taken можно попасть только в tranzit.
  • waiting: tranzit → waiting. Идемпотентен для waiting. Из taken или done409 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.

6. Типы результата: html и values

Результат по анализу можно отправить в одном из двух форматов:

  • result-html — готовый HTML-фрагмент (например, отрендеренный бланк/таблица результатов). Подходит, когда лаборатория формирует визуальное представление сама.
  • result-values — структурированный набор «параметр → значение → ед. измерения». Подходит для машиночитаемых показателей.

Важно: формат фиксируется за заявкой по первому результату. Если первый результат отправлен как html, остальные анализы этой же заявки тоже должны идти html (и наоборот). Смешение → 409 INCOMPATIBLE_RESULT_FORMAT.

Оба формата поддерживают необязательные поля file_link, measured_at, comment.


7. Эндпоинты — контракт и примеры

Общие правила:

  • Тело POST-запросов — JSON (Content-Type: application/json).
  • Все ответы — JSON. Успех: "success": true. Ошибка: "success": false + error_code + messages[].

7.1. GET /list — очередь заявок

Параметры строки запроса (все необязательные):

ПараметрТипПо умолчаниюОграничения
statusstringвсе из taken,tranzit,waitingтолько одно из taken, tranzit, waiting
limitint50от 1 до 200
offsetint00

Запрос:

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[].filled1, если по анализу уже принят результат, иначе 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_idintда> 0
analyze_idintдаанализ должен принадлежать заявке
htmlstringдане пусто; ≤ 1 МБ; без <script>/<iframe>/on*=-атрибутов
file_linkstringнетURL только с разрешённого файлового хоста (см. 8)
measured_atstringнетISO 8601, напр. 2024-01-15T10:30:00+02:00
commentstringнетпроизвольный текст

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_filledtrue, когда это был последний незаполненный анализ заявки.
  • request_status — станет done, как только is_all_filled = true.

7.5. POST /result-values — структурированный результат

Тело:

ПолеТипОбяз.Ограничения
request_idintда> 0
analyze_idintдаанализ должен принадлежать заявке
valuesarrayданепустой массив объектов (см. ниже)
file_linkstringнетURL только с разрешённого файлового хоста
measured_atstringнетISO 8601
commentstringнетпроизвольный текст

Каждый элемент values:

ПолеТипОбяз.Ограничения
namestringда≤ 50 симв.; приводится к верхнему регистру; уникален в пределах values
valuestringда≤ 255 симв.
unitstringнет≤ 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).


8. Ошибки

Формат тела ошибки одинаков для всех эндпоинтов:

{
  "success": false,
  "error_code": "NOT_FOUND",
  "messages": ["Заявка не найдена"]
}

8.1. По HTTP-кодам

HTTPerror_codeКогда возникаетЧто делать
401UNAUTHORIZEDНет одного из заголовков или неверный {rest_api_key}Проверить заголовки и ключ
404NOT_FOUNDАддон/интеграция выключены; провайдер не найден; заявка не найдена/в терминальном статусе; анализ не принадлежит заявкеПроверить статус заявки и настройки клиники (раздел 9)
400INVALID_PAYLOADНевалидные/отсутствующие поля, нарушены длины/формат measured_at, недопустимый status/limit/offsetИсправить тело/параметры
400INVALID_HTMLВ html есть запрещённые элементы/атрибутыУбрать <script>/<iframe>/on*=
400INVALID_FILE_LINKfile_link не URL или хост не из разрешённого спискаИспользовать допустимый файловый хост
400DUPLICATE_KEYSПовтор name внутри valuesСделать ключи уникальными
409ALREADY_TAKENtake для уже завершённой (done) заявкиЗаявка закрыта, действий не требуется
409INVALID_STATUS_TRANSITIONНедопустимый переход (waiting из taken/done; результат — из taken). Для заявки в done вместо этого кода приходит ALREADY_FILLED (см. ниже)Привести заявку к нужному статусу (takewaiting)
409INCOMPATIBLE_RESULT_FORMATСмешение html и values в одной заявкеИспользовать формат первого результата
409ALREADY_FILLEDПо анализу результат уже отправлен, в т.ч. любой повторный результат по завершённой (done) заявке — все её анализы гарантированно заполнены, и эта проверка выполняется раньше проверки статусаНе отправлять повторно

8.2. Особый случай: 200total = 0

GET /list вернул success: true, но total: 0 и пустой labanalysisrequest. Это не ошибка авторизации — ключи приняты. Причины:

  • по клинике нет заявок в выбранном status;
  • интеграция включена не для той клиники, по чьему {clinic_host} идёт запрос;
  • на стороне клиники не активирована интеграция в настройках (раздел 9).

Проверьте: правильный ли {clinic_host}, создана ли заявка на провайдера, включена ли интеграция per-клиника.


9. Требования на стороне клиники

Чтобы заявки клиники стали видны через Provider API, на стороне клиники должны выполняться все условия:

  1. Подключён тариф/аддон вашей лаборатории (биллинг).
  2. Включена интеграция в настройках клиники (раздел «Лаборатория» → ваш провайдер). Технически это глобальный флаг сервиса и per-клиника флаг интеграции.
  3. Создана заявка в модуле «Лаборатория» на вашего провайдера — она появляется в очереди в статусе taken.

Если интеграция выключена или заявок нет — провайдер увидит 404 NOT_FOUND на адресные операции и total = 0 в list.


10. Postman

Готовая коллекция: 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_idID заявки для take/waiting/result
analyze_idID анализа для result

Секреты в коллекцию не зашиты — все значения берутся из переменных окружения.


Приложение A. Примеры curl

# 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"
      }'

Приложение B. Контакты поддержки

По вопросам подключения, выдачи ключей и работы Provider API обращайтесь в поддержку Vetmanager по официальным каналам, указанным в вашем договоре/онбординге.

Powered by