Justmedit

Анемии и скрытые дефициты по анализам крови

Демо. Не вводите персональные данные. Не медицинское изделие.

REST · JSON · OpenAPI 3.0

API для встраивания

Расшифровка анализов, разбор бланков и кабинет пациента — в вашей МИС, на сайте клиники или в приложении. Запрос можно отправить прямо с этой страницы.

  1. Получите ключ или токен

    Ключ X-API-Key — для запросов с вашего сервера, его задаёт администратор сервиса (DL_API_KEYS). Токен пользователя — ответ POST /api/v1/auth/login: для браузера и чужих сайтов, только в памяти страницы.

  2. Отправьте запрос

    JSON на /api/v1/analyze: пол, возраст и значения показателей. Заголовок Authorization: Bearer <токен> или X-API-Key.

  3. Получите ответ

    Анемия по ВОЗ, вероятный тип, скрытый дефицит, что досдать и два отчёта — врачу и пациенту. Ошибка — с понятным текстом и полем, которое нужно исправить.

Консоль

Выберите маршрут и авторизацию, поправьте тело и отправьте запрос. Ключ и токен живут только в этой вкладке: страница их не сохраняет и не отправляет cookie сайта.

POST/api/v1/analyze

Авторизация

Тело запроса
или Ctrl + Enter в поле тела

Ответ

Здесь появится ответ сервера: код, время, важные заголовки и тело.

Примеры кода

Код повторяет запрос из консоли и меняется вместе с ней. Настоящих ключа и пароля в коде нет: вместо них — $JUSTMEDIT_TOKEN, $JUSTMEDIT_API_KEY, $JUSTMEDIT_PASSWORD или <ваш ключ>.

Загружаем описание API…

Справочник

Все маршруты по группам: кто может вызывать, что делает, пример запроса и ответа. Машиночитаемое описание — /api/v1/openapi.json (OpenAPI 3.0: Swagger UI, Postman, генераторы клиентов).

Загружаем описание API…

Встраивание в свой сайт

Форма расшифровки на сайте клиники, кабинет пациента в вашем приложении — на тех же маршрутах, что и консоль выше.

Ключ X-API-Key — только «сервер к серверу»

Ключ не привязан к человеку и открывает расчёты от имени вашей организации. В коде страницы его увидит любой посетитель. В браузере используйте токен пользователя (Authorization: Bearer): он принадлежит одному человеку, его можно отозвать. Или отправляйте запросы через свой сервер, как в examples/clinic_site.

Разрешите свой домен

Браузер отправит запрос на другой домен, только если сервис разрешил этот источник. Список — переменная окружения DL_CORS_ORIGINS, через запятую, со схемой и без косой черты в конце:

.env или docker compose
DL_CORS_ORIGINS=https://clinic.example,https://www.clinic.example

CORS действует для /api/v1/* и без credentials: cookie сайта Justmedit чужой странице не передаются, авторизация — только заголовком Authorization.

Получите токен пользователя

Пользователь входит через ваш сайт: POST /api/v1/auth/login с логином и паролем, в ответе — token. Храните его только на время сеанса и передавайте в заголовке Authorization: Bearer <токен>. Для своих программ — постоянный токен: POST /api/v1/me/tokens.

Пароли и токены не кладите в адрес страницы и в журналы.

Форма расшифровки на вашем сайте

Готовый фрагмент HTML и JavaScript: пользователь входит своим логином Justmedit прямо на сайте клиники, затем пол, возраст, гемоглобин и ферритин → главный вывод для пациента. Токен из ответа входа живёт только в памяти вкладки: закрыл вкладку — вошёл заново. Ответ выводится через textContent — никакого HTML из ответа на странице.

Не вставляйте токен в код страницы

Токен в HTML или JavaScript страницы увидит любой посетитель и сможет читать и менять чужой кабинет. Не кладите его и в localStorage, cookie или адрес страницы. Нужна форма без входа пользователя — отправляйте запросы через сервер клиники с ключом X-API-Key, как в examples/clinic_site.

form.html
<!-- Форма расшифровки анализа на сайте клиники (домен сайта — в DL_CORS_ORIGINS сервиса).
     Пользователь входит своим логином Justmedit; токен из ответа — только в памяти вкладки (переменная token).
     Токен не вставляйте в код страницы. Ключ X-API-Key в браузер не кладите. -->
<form id="jm-login">
  <label>Логин Justmedit <input name="login" autocomplete="username" required></label>
  <label>Пароль <input name="password" type="password" autocomplete="current-password" required></label>
  <button>Войти</button>
</form>
<form id="jm-form" hidden>
  <label>Пол
    <select name="sex"><option value="F">женский</option><option value="M">мужской</option></select>
  </label>
  <label>Возраст, лет <input name="age" type="number" min="18" max="120" required></label>
  <label>Гемоглобин, г/л <input name="hemoglobin" type="number" step="0.1" required></label>
  <label>Ферритин, мкг/л <input name="ferritin" type="number" step="0.1"></label>
  <button>Расшифровать</button>
</form>
<p id="jm-out" aria-live="polite"></p>

<script>
const JUSTMEDIT = "https://justmedit.example";      // адрес сервиса Justmedit
let token = null;                                    // токен — только в памяти вкладки, нигде не сохраняется
const loginForm = document.getElementById("jm-login");
const form = document.getElementById("jm-form");
const out = document.getElementById("jm-out");

function signedIn(on) { loginForm.hidden = on; form.hidden = !on; }

loginForm.addEventListener("submit", async (event) => {
  event.preventDefault();
  const f = new FormData(loginForm);
  out.textContent = "Входим…";
  try {
    const res = await fetch(JUSTMEDIT + "/api/v1/auth/login", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ login: f.get("login"), password: f.get("password") })
    });
    const data = await res.json();
    loginForm.reset();                                 // пароль не остаётся в форме
    if (!res.ok) { out.textContent = data.error.message; return; }
    token = data.token;
    signedIn(true);
    out.textContent = "Вы вошли как " + data.user.login + ".";
  } catch (e) {
    out.textContent = "Сервис недоступен. Попробуйте позже.";
  }
});

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const f = new FormData(form);
  const values = { hemoglobin: Number(f.get("hemoglobin")) };
  if (f.get("ferritin")) values.ferritin = Number(f.get("ferritin"));
  out.textContent = "Считаем…";
  try {
    const res = await fetch(JUSTMEDIT + "/api/v1/analyze", {
      method: "POST",
      headers: { "Authorization": "Bearer " + token, "Content-Type": "application/json" },
      body: JSON.stringify({ sex: f.get("sex"), age_years: Number(f.get("age")), values })
    });
    const data = await res.json();
    if (res.status === 401) {                          // токен истёк или отозван — войти заново
      token = null;
      signedIn(false);
      out.textContent = "Сеанс завершён — войдите снова.";
      return;
    }
    out.textContent = res.ok ? data.reports.patient.headline : data.error.message;
  } catch (e) {
    out.textContent = "Сервис недоступен. Попробуйте позже.";
  }
});
</script>

Кабинет пациента через API

Пять запросов: регистрация, вход, сохранить анализ, получить динамику, открыть доступ врачу. Тела — JSON с заголовком Content-Type: application/json. Код 201 — у регистрации и новой записи, остальные — 200. Логин и пароль — свои, в переменных окружения $JUSTMEDIT_LOGIN и $JUSTMEDIT_PASSWORD: пароль не пишите в команду и в код. Анализы в кабинете хранятся зашифрованными; удалить всё — DELETE /api/v1/me с паролем.

  1. POST /api/v1/auth/register — регистрация

    # свой логин и пароль: read -s не покажет пароль и не оставит его в истории команд
    export JUSTMEDIT_LOGIN="dev-$(openssl rand -hex 3)"
    read -rs -p "Пароль (от 8 символов): " JUSTMEDIT_PASSWORD; echo; export JUSTMEDIT_PASSWORD
    curl -sS -X POST "https://justmedit.example/api/v1/auth/register" \
      -H "Content-Type: application/json" \
      -d "{\"login\": \"$JUSTMEDIT_LOGIN\", \"password\": \"$JUSTMEDIT_PASSWORD\", \"role\": \"patient\", \"consent\": true}"
  2. POST /api/v1/auth/login — вход, токен в переменную

    export JUSTMEDIT_TOKEN=$(curl -sS -X POST "https://justmedit.example/api/v1/auth/login" \
      -H "Content-Type: application/json" \
      -d "{\"login\": \"$JUSTMEDIT_LOGIN\", \"password\": \"$JUSTMEDIT_PASSWORD\"}" \
      | python3 -c 'import json, sys; print(json.load(sys.stdin)["token"])')
  3. POST /api/v1/me/records — сохранить анализ и получить расшифровку

    curl -sS -X POST "https://justmedit.example/api/v1/me/records" \
      -H "Authorization: Bearer $JUSTMEDIT_TOKEN" -H "Content-Type: application/json" \
      -d '{"date": "2026-04-16", "source": "form",
           "input": {"sex": "F", "age_years": 34, "values": {"hemoglobin": 124, "MCV": 82, "RDW": 15.2}}}'
  4. GET /api/v1/me/history — динамика и что давно не сдавалось

    curl -sS "https://justmedit.example/api/v1/me/history" -H "Authorization: Bearer $JUSTMEDIT_TOKEN"
  5. POST /api/v1/me/shares — доступ врачу на 30 дней

    По токену API ссылку врачу создают только с паролем аккаунта в поле password — так же новый токен (POST /api/v1/me/tokens) и «выйти везде» (POST /api/v1/me/logout-all): украденный токен не откроет историю постороннему. Без пароля — 403 password_required, с неверным — 403 wrong_password.

    curl -sS -X POST "https://justmedit.example/api/v1/me/shares" \
      -H "Authorization: Bearer $JUSTMEDIT_TOKEN" -H "Content-Type: application/json" \
      -d "{\"days\": 30, \"password\": \"$JUSTMEDIT_PASSWORD\"}"
    # ответ 200: {"share": {…, "status": "pending"}, "link_token": "…", "link": "/share#…"}
    # ссылка для врача: https://justmedit.example/share#<link_token> (живёт 48 ч, одноразовая)
    # врач со своим токеном: POST /api/v1/shares/accept {"token": "<link_token>"}

Ошибки и лимиты

Тело ошибки всегда одного вида: {"error": {"code", "message", "field"}}. Сообщение по-русски говорит, что исправить; значений анализов в нём нет.

КодКогдаЧто делать
401 unauthorized нет ключа или токена, токен просрочен передайте X-API-Key или войдите заново через /api/v1/auth/login
401 invalid_credentials вход: неверный логин или пароль (ответ одинаковый) проверьте логин и пароль; аккаунта нет — /api/v1/auth/register
403 forbidden, csrf ключ не подходит; не та роль (пациент вызвал маршрут врача); нет доступа к пациенту; запрос с cookie без X-Justmedit: 1 проверьте ключ, роль и срок доступа
403 password_required по токену API: новый токен, ссылка врачу или «выйти везде» без поля password добавьте в тело "password" — пароль аккаунта; с cookie сайта он не нужен
403 wrong_password неверный пароль: удаление аккаунта, смена пароля (поле old), токен, ссылка врачу, «выйти везде» по токену передайте текущий пароль; действие не выполнено (при удалении — ничего не удалено)
404 not_found нет такой записи или маршрута проверьте путь и id (32 шестнадцатеричных знака)
404 share_invalid ссылка врачу уже использована, отозвана или истекла (48 ч) попросите пациента создать новую ссылку
409 login_taken регистрация: логин занят (регистр не важен) выберите другой логин или войдите
413 payload_too_large тело или файл больше 10 МБ разбейте пакет на части
413 record_too_large запись кабинета: вход анализа больше 16 КБ уберите лишние показатели, референсы или patient_ref
415 unsupported_media_type тело запроса кабинета без Content-Type: application/json отправьте JSON с этим заголовком
422 invalid_*, file_required… ошибка ввода: возраст вне 18–120, нет гемоглобина, неизвестная единица, не тот файл исправьте поле из field по тексту message
422 unknown_analyte кабинет и референсы врача: показатель не из справочника коды и названия — GET /api/v1/reference
422 missing_field PATCH /api/v1/doctor/lab-refs/{id} с пустым телом передайте хотя бы одно из полей name, default, ranges
422 too_many_tokens, quota_exceeded именованных токенов уже 20; данных пользователя больше 5 МБ отзовите ненужный токен; удалите старые записи
429 rate_limited превышен лимит запросов в минуту повторите через Retry-After секунд
503 service_busy, storage_unavailable заняты оба слота тяжёлых расчётов; хранилище кабинета недоступно повторите через Retry-After секунд
503 signups_paused регистрация: достигнут суточный предел новых аккаунтов повторите позже (Retry-After); вход работает
507 storage_full хранилище кабинета заполнено: новые записи, наборы, ссылки и регистрации не принимаются повторите позже; чтение, выгрузка и удаление работают
500 internal внутренняя ошибка; подробностей и трассировки в ответе нет повторите позже; если ошибка повторяется — сообщите время запроса
  • 10 МБтело запроса и файл (DL_MAX_UPLOAD_MB); один анализ JSON — до 256 КБ
  • 10 000записей в пакете и строк в файле (DL_MAX_BATCH); 200 столбцов; бланк — до 400 строк
  • 2тяжёлых расчёта одновременно (batch, benchmark, columns, parse); иначе 503
  • 30 в минутутяжёлых запросов на ключ или токен по умолчанию (DL_UI_RATE_PER_MIN); иначе 429
  • 25 Мп · 20 сфото бланка: размер снимка и время распознавания