API для интеграции: запись на входе, JSON и PDF на выходе
Отправьте ЭКГ по HTTPS — через несколько секунд заберите структурированное заключение и готовый PDF-отчёт на 9 страниц. Подходит для МИС, телемедицинских платформ и кардиографов: один запрос на запись, ответ — в формате, который можно сразу сохранить в карту пациента.
Как получить доступ
Тестовый токен выдаём по запросу: напишите в Telegram @silamisli, на почту info@kardiopilot.ru или оставьте заявку. Токен персональный: у него есть срок действия и суточный лимит заданий, его можно отозвать в любой момент.
Адрес и авторизация
Базовый адрес: https://kardiopilot.ru/api/v1
Заголовок: Authorization: Bearer kp_…
Токен не встраивают в браузерный JavaScript и не передают в адресной строке — только
с вашего сервера. В клинике тот же API работает внутри сети, без интернета.
Методы
Все адреса — относительно https://kardiopilot.ru/api/v1. Каждый клиент видит только свои задания.
| Метод | Адрес | Что делает |
|---|---|---|
GET | /me | Проверка токена: имя клиента, срок действия, суточный лимит и сколько уже израсходовано. |
POST | /jobs | Отправить запись. Обязательно только file. Частота и единицы CSV определяются по файлу, поля fs и units нужны лишь для исправления. Данные пациента по желанию: sex, age, height, weight, bp, name. interview=true — с опросом пациента. Ответ 202 с id. |
POST | /quality | Быстрая проверка записи из 6 или 6 + 1 отведений до анализа: годится ли она, и если нет — что поправить. Без очереди, не расходует лимит. Подробнее. |
GET | /jobs | Список ваших заданий, последние 100. |
GET | /jobs/{id} | Статус задания. completed — готовы и JSON, и PDF. |
GET | /jobs/{id}/result | Заключение в JSON: светофор, срочность, диагнозы с баллами, причины, качество записи. Плюс criteria — находки по формульным критериям, посчитанные по измерениям, и traffic_light_scope: ensemble+criteria, когда они учтены, или ensemble, пока criteria_status равен pending (отчёт ещё собирается). |
GET | /jobs/{id}/pdf | PDF-отчёт на 9 страниц: кривые 12 отведений, измерения, заключение, ответы опроса. |
GET | /jobs/{id}/interview | Текущий вопрос опроса: id, text, признак safety. |
POST | /jobs/{id}/answers | Ответ на текущий вопрос: JSON {"question_id": …, "answer": …}. |
POST | /jobs/{id}/finish | Завершить опрос досрочно — отчёт строится по уже данным ответам. |
Жизненный цикл задания
- queued → processingЗапись в очереди и в анализе. Обычно 3–5 секунд.
- awaiting_interview ⇄ queued_answerТолько при
interview=true: система задаёт от 3 до 15 вопросов, первыми — вопросы безопасности. - queued_reportАнализ готов, формируется PDF на 9 страниц — ещё 1–2 секунды.
- completed или failedМожно забирать
/resultи/pdf. До этого оба метода отвечают 409; приfailedпричина в полеerror.
Примеры кода
Полный цикл: отправить запись, дождаться готовности, забрать JSON и PDF. Ответы проверены на рабочем сервере.
curl
export BASE=https://kardiopilot.ru/api/v1
export TOKEN=kp_ваш_токен
# 0. проверить токен и лимиты
curl -H "Authorization: Bearer $TOKEN" "$BASE/me"
# 1. отправить запись (.npz, .csv, .edf или PDF кардиографа GE MUSE)
curl -H "Authorization: Bearer $TOKEN" -F "file=@ecg.npz" "$BASE/jobs"
# {"id":"xs7wVdOR…","status":"queued","links":{"self":…,"result":…,"pdf":…}}
# частоту и единицы CSV система определяет сама; укажите, только если она ошиблась:
curl -H "Authorization: Bearer $TOKEN" -F "file=@ecg.csv" -F fs=500 -F units=mV "$BASE/jobs"
# данные пациента — по желанию. Без них отчёт пишет «не указано» и берёт строгие пороги:
curl -H "Authorization: Bearer $TOKEN" -F "file=@ecg.csv" -F "name=Пациент №42" -F sex=male -F age=40 -F height=182 -F weight=88 -F "bp=125/80" "$BASE/jobs"
# 2. статус — повторять раз в 1–2 секунды до completed или failed
curl -H "Authorization: Bearer $TOKEN" "$BASE/jobs/$JOB"
# 3. заключение в JSON и PDF-отчёт
curl -H "Authorization: Bearer $TOKEN" "$BASE/jobs/$JOB/result"
curl -H "Authorization: Bearer $TOKEN" "$BASE/jobs/$JOB/pdf" -o report.pdf
Python (requests)
import time
import requests
BASE = "https://kardiopilot.ru/api/v1"
HEADERS = {"Authorization": "Bearer kp_ваш_токен"}
def analyse(path: str, patient: dict | None = None) -> dict:
# 1. Отправить запись. Частота и единицы CSV определяются по файлу;
# передайте fs и units, только если определение ошиблось.
# patient: sex, age, height, weight, bp, name — всё по желанию.
with open(path, "rb") as f:
response = requests.post(f"{BASE}/jobs", headers=HEADERS, files={"file": f},
data=patient or {}, timeout=60)
response.raise_for_status()
job_id = response.json()["id"]
# 2. Дождаться готовности: обычно 5–10 секунд.
while True:
status = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=30).json()
if status["status"] == "completed":
break
if status["status"] == "failed":
raise RuntimeError(f"Анализ не выполнен: {status['error']}")
time.sleep(1.5)
# 3. JSON-заключение.
result = requests.get(f"{BASE}/jobs/{job_id}/result", headers=HEADERS, timeout=30).json()
# 4. PDF-отчёт на 9 страниц.
pdf = requests.get(f"{BASE}/jobs/{job_id}/pdf", headers=HEADERS, timeout=60)
pdf.raise_for_status()
with open(f"report-{job_id[:8]}.pdf", "wb") as out:
out.write(pdf.content)
return result
result = analyse("ecg.npz")
print(result["traffic_light"], result["urgency"])
for diagnosis in result["top_diagnoses"]:
print(f"{diagnosis['name_ru']}: {diagnosis['score']:.2f}")
JavaScript (Node.js)
// Node.js 18+ (встроенные fetch, FormData и Blob)
import { readFile, writeFile } from "node:fs/promises";
const BASE = "https://kardiopilot.ru/api/v1";
const headers = { Authorization: "Bearer kp_ваш_токен" };
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const form = new FormData();
form.append("file", new Blob([await readFile("ecg.csv")]), "ecg.csv");
// частота и единицы CSV определяются по файлу — эти поля нужны только для правки:
// form.append("fs", "500"); form.append("units", "mV");
form.append("sex", "male"); // данные пациента — по желанию
form.append("age", "40");
const created = await fetch(`${BASE}/jobs`, { method: "POST", headers, body: form });
if (!created.ok) throw new Error((await created.json()).detail);
const { id } = await created.json();
let status;
do {
await sleep(1500);
status = await (await fetch(`${BASE}/jobs/${id}`, { headers })).json();
} while (!["completed", "failed"].includes(status.status));
if (status.status === "failed") throw new Error(status.error);
const result = await (await fetch(`${BASE}/jobs/${id}/result`, { headers })).json();
console.log(result.traffic_light, result.primary_diagnosis?.name_ru);
const pdf = await fetch(`${BASE}/jobs/${id}/pdf`, { headers });
await writeFile("report.pdf", Buffer.from(await pdf.arrayBuffer()));
Опрос пациента
С параметром interview=true задание после анализа ждёт ответов.
Варианты ответа: yes, probably_yes, unsure,
probably_no, no. Ответы меняют оценку срочности и печатаются в PDF;
баллы ЭКГ опрос не меняет. Брошенный опрос закрывается автоматически через 30 минут.
ANSWERS = ("yes", "probably_yes", "unsure", "probably_no", "no")
with open("ecg.npz", "rb") as f:
job_id = requests.post(f"{BASE}/jobs", headers=HEADERS, files={"file": f},
data={"interview": "true"}, timeout=60).json()["id"]
while True:
status = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=30).json()["status"]
if status in ("completed", "failed"):
break
if status != "awaiting_interview": # queued, processing, queued_answer, queued_report
time.sleep(1)
continue
state = requests.get(f"{BASE}/jobs/{job_id}/interview", headers=HEADERS, timeout=30).json()
question = state["question"]
# question["safety"] == True — вопрос безопасности (боль в груди, одышка, обморок)
answer = ask_patient(question["text"]) # ваш интерфейс: одно из ANSWERS
requests.post(f"{BASE}/jobs/{job_id}/answers", headers=HEADERS, timeout=30,
json={"question_id": question["id"], "answer": answer})
# Завершить опрос досрочно (отчёт построится по уже данным ответам):
# requests.post(f"{BASE}/jobs/{job_id}/finish", headers=HEADERS)
Что приходит в ответе
Сокращённый ответ GET /jobs/{id}/result. traffic_light —
green, yellow или red; score — внутренний балл
модели от 0 до 1, а не вероятность болезни. Значение assessment_status=complete
означает пригодность анализа, а не отсутствие заболеваний; пропущенный интервал не означает норму.
{
"id": "xs7wVdOR4hmT80NOZvWo08LOjqHdiGp_",
"traffic_light": "yellow",
"urgency": "abnormal_non_urgent",
"assessment_status": "complete",
"primary_diagnosis": {
"code": "AF",
"name_ru": "Фибрилляция предсердий",
"score": 0.9918,
"confidence": "requires_review",
"traffic_light": "yellow",
"confirming_models": ["inception1d", "lstm_bidir", "resnet1d101", "…"]
},
"top_diagnoses": [
{"code": "AF", "name_ru": "Фибрилляция предсердий", "score": 0.9918, "…": "…"},
{"code": "LVH", "name_ru": "Гипертрофия левого желудочка", "score": 0.0138, "…": "…"}
],
"reasons": ["Результат локального ансамбля из шести моделей и XGBoost."],
"interview_answered_count": 0,
"quality_flags": ["…"],
"criteria": [
{"code": "LVH_CRITERIA",
"name": "Признаки гипертрофии левого желудочка по вольтажным критериям",
"detail": "Соколов–Лайон 64 мм"}
],
"criteria_status": "ready",
"traffic_light_scope": "ensemble+criteria",
"calibration": {"version": "georgia_isotonic_v1", "engine_traffic_light": "yellow"},
"measurement_consistency": {"…": "…"},
"analysis_details": {"…": "…"},
"pdf_url": "/api/v1/jobs/xs7wVdOR4hmT80NOZvWo08LOjqHdiGp_/pdf"
}
PDF отдаётся методом GET /jobs/{id}/pdf как файл
application/pdf — его можно сохранить в карту пациента или отправить врачу без доработки.
Формат записи
NPZ
signal — матрица 12×N без NaN и бесконечностей.
fs — целая частота дискретизации, 100–1000 Гц.
lead_names — 12 названий отведений.
units — mV, uV или V.
CSV и EDF
CSV: 12 столбцов (по отведению на столбец), первая строка — названия
I, II, III, aVR, aVL, aVF, V1…V6; разделитель — запятая, точка с запятой или табуляция.
Частоту и единицы система определяет по файлу сама и пишет об этом в поле notice;
поля fs и units нужны, только если определение ошиблось.
EDF/EDF+: 12 стандартных отведений, частота и единицы берутся из заголовка файла.
Данные пациента
В самой записи ЭКГ их нет, поэтому отчёт берёт их из полей формы:
sex (male или female), age в годах,
height в сантиметрах, weight в килограммах,
bp строкой вида 125/80 и name — имя или идентификатор
для шапки отчёта.
Все поля необязательны. От пола и возраста зависят пороги удлинения QT и подъёма ST,
от роста и веса — площадь тела и критерии гипертрофии. Без них в отчёте стоит
«не указано», а пороги берутся по более чувствительному варианту.
До 10 МиБ на файл, длительность 2–60 секунд (для 6 и 6 + 1 отведений — от 10 секунд, подробнее). Фотографии и сканы бумажной ленты, PDF и произвольные архивы не принимаются. Не передавайте ФИО и другие персональные данные: для анализа нужен только сигнал.
ЭКГ из 6 и 6 + 1 отведений
Те же вызовы и тот же формат. Режим определяется по сигналу: если записаны только I и II, это 6 отведений; если ещё одно грудное — 6 + 1. Незаписанные отведения передаются нулями. Рекомендуемая частота 500 Гц, длина записи 20 секунд, не меньше 10: анализ идёт по самому чистому отрезку 10 секунд. Что умеет этот режим →
Дополнительные поля NPZ
chest_lead — куда просили поставить грудной электрод, например
V3; сигнал кладётся в эту же строку матрицы. Без поля плоское грудное отведение
считается незаписанным, с ним — потерявшим контакт.
attempt — номер попытки записи: на третьей неудачной ответ «позовите сотрудника».
Оба поля можно передать и полями формы.
Проверка записи до анализа
POST /quality с тем же файлом отвечает за доли секунды, без очереди и без
расхода лимита: status — ok, repeat или staff,
reason — причина, electrode — какой электрод поправить,
screen_text — готовый текст для экрана прибора. Анализ POST /jobs проверяет
запись и сам: плохая запись не получает зелёный цвет.
{
"status": "repeat",
"reason": "contact",
"electrode": "LA",
"attempt": 1, "max_attempts": 3, "lead_mode": "6L+V3",
"screen_text": "Нет контакта с левой рукой. Положите левую ладонь на датчик целиком и не отрывайте её.",
"analysis_window_s": [4.0, 14.0],
"chest_declared": "V3",
"chest_candidates": ["V4"]
}
В chest_candidates — позиция грудного
электрода, которую система признала по сигналу и по которой идёт анализ (заявленная или
соседняя). Отчёт для 6 и 6 + 1 отведений — 8 страниц.
Лимиты и коды ответов
Лимиты
Суточный лимит заданий — по токену (для теста обычно 50).
Не больше 3 незавершённых заданий одновременно.
До 120 запросов в минуту с одного IP-адреса.
При превышении — ответ 429 и заголовок Retry-After.
Безопасность
Только HTTPS. Токен хранится на сервере в виде хеша, идентификаторы заданий случайные,
чужие задания недоступны. Повторная отправка создаёт новое задание — после ошибки сети
сначала проверьте GET /jobs.
| Код | Когда |
|---|---|
| 200 / 202 | Успех; 202 — задание принято в очередь |
| 400 | Файл не разобран или неверные параметры — причина в поле detail |
| 401 | Нет токена, токен неверен, отозван или истёк |
| 404 | Задание не найдено (или принадлежит другому клиенту) |
| 409 | Результат или PDF запрошен до готовности; ответ не на текущий вопрос |
| 413 | Файл больше 10 МиБ |
| 415 | Неподдерживаемый формат файла |
| 429 | Исчерпан лимит или очередь заполнена |
| 503 | Сервис анализа временно недоступен |