Быстрый старт
- 1
Получите ключ у команды ФармМонитор — платформенный ключ выдаётся вручную (панель
/admin/integrations) и показывается один раз при создании. Формат:brf_live_<случайная строка>. - 2
Список опросов, доступных ключу:
curl -H "Authorization: Bearer brf_live_ВАШ_КЛЮЧ" \ https://surveys.pharmmonitor.ru/api/v1/surveys - 3
Структура анкеты — чтобы сопоставить
block_idиз ответов с текстом вопроса и вариантами:curl -H "Authorization: Bearer brf_live_ВАШ_КЛЮЧ" \ https://surveys.pharmmonitor.ru/api/v1/surveys/42/structure - 4
Первая страница ответов — про постраничную и инкрементальную догрузку целиком см. раздел «Пагинация и инкрементал» ниже:
curl -H "Authorization: Bearer brf_live_ВАШ_КЛЮЧ" \ "https://surveys.pharmmonitor.ru/api/v1/surveys/42/responses?limit=500"
Аутентификация
Каждый запрос к /api/v1/* требует заголовок Authorization: Bearer brf_live_<ключ>. Ключ — платформенный (не привязан к пользовательской сессии), выдаётся командой ФармМонитор вручную в панели /admin/integrations и показывается один раз в момент создания.
- Один ключ может быть выдан на конкретный список организаций или на все организации сразу («мультитенантный» доступ) — эндпоинт
GET /api/v1/organizationsвозвращает, какие именно организации видит ваш ключ. - Отсутствие заголовка, схема, отличная от
Bearer, пустой токен, а также неизвестный/отозванный/истёкший ключ — все эти случаи дают ОДИН и тот же ответ401 {"code": "unauthorized"}. Наружу не раскрывается, какая именно из причин сработала — это осознанное решение, чтобы не давать атакующему различить «ключа не существует» от «ключ истёк». - Запрос к ресурсу чужой организации — это НЕ ошибка авторизации, а
404: платформа не подтверждает и не опровергает существование чужого опроса.
Эндпоинты
Объект прохождения и типы value
Форма прохождения (ResponseObject) и каждого отдельного ответа (AnswerEntry) — ниже, вместе с полным словарём схем. Форма поля value зависит от типа блока (ChoiceValue, MatrixRowValue, DiaryAnswerValue и т.д.) — см. AnswerValue в словаре.
OrganizationRef
Организация, доступная ключу.
| Поле | Тип | Описание | |
|---|---|---|---|
| id | integer | обязательно | |
| name | string | обязательно |
SurveySummary
Карточка опроса (GET /api/v1/surveys, GET /api/v1/surveys/{id}).
| Поле | Тип | Описание | |
|---|---|---|---|
| id | integer | обязательно | |
| title | string | обязательно | |
| status | string | обязательно | |
| created_at | string | обязательно | ISO 8601 UTC |
| launched_at | stringлибоnull | обязательно | ISO 8601 UTC; null — опрос ещё не запускался |
| completed_at | stringлибоnull | обязательно | ISO 8601 UTC; null — опрос ещё не завершён |
| archived_at | stringлибоnull | обязательно | ISO 8601 UTC; null — опрос не в архиве |
| responses_count | integer | обязательно | Живой счётчик завершённых прохождений (complete|over_quota, без удалённых) |
| organization | OrganizationRef | обязательно |
StructureOption
Один вариант ответа/строка матрицы в структуре анкеты.
| Поле | Тип | Описание | |
|---|---|---|---|
| id | string | обязательно | opt_<index> (choice-семейство) либо row_<index> (строки статической matrix_scale) |
| label | string | обязательно |
StructureBlock
Один блок анкеты (GET /api/v1/surveys/{id}/structure). ПД-блоки (contacts/email/phone/respondent_passport/consent) и блоки без ответа (info_screen, presentation) ИСКЛЮЧЕНЫ из выдачи целиком — тот же предикат применён и к responses[].answers, поэтому block_id из structure всегда соответствует block_id, который может встретиться в ответах. Удалённые из анкеты вопросы (soft-delete) НЕ исключаются — остаются в structure с archived: true, после активных блоков анкеты.
| Поле | Тип | Описание | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| block_id | integer | обязательно | |||||||||||||
| block_type | string | обязательно | Тип блока платформы (single_choice, checkboxes, matrix_scale, diary, followup, …) | ||||||||||||
| question | string | обязательно | |||||||||||||
| options | array of StructureOptionлибоnull | обязательно | Варианты/строки для choice-семейства и matrix_scale; null для остальных типов | ||||||||||||
| settings | objectлибоnull | обязательно | Курированное подмножество настроек, влияющих на интерпретацию value (НЕ сырой дамп block.settings — редакторские поля вроде visibilityRule/media исключены). Примеры по семействам: шкалы — scale_min/scale_max; slider — min/max/step/unit; number — min_value/max_value/integer_only; matrix_scale — scale_min/scale_max/column_labels/require_all_rows/scale_kind ("nominal" — колонки — варианты ответа: label в ответах — название колонки, среднее по кодам не имеет смысла)/na_last_column (true — последняя колонка — «нет ответа»: код ответа scale_max, в среднее не входит); datetime_picker — include_time/date_min/date_max/date_restriction; file_upload — allowed_file_types/max_size_mb; choice-семейство — has_other; справочники — dictionary_mode/list_mode/allow_freeform; followup — questions (под-вопросы как есть, публично-безопасны); city_picker — city_mode ("single"|"multi"); null — для типов без settings, влияющих на интерпретацию. | ||||||||||||
| group |
null | обязательно | Группа блоков графа; null — блок вне группы | ||||||||||||
| archived | boolean | обязательно | true — блок удалён из анкеты автором (soft-delete), но остаётся в structure, потому что ответы на него продолжают отдаваться в responses[].answers (исторические данные не теряются) — без этого поля block_id из старых ответов не находился бы в structure вовсе. |
ChoiceValue
Одиночный выбор (single_choice*, ab_text, ab_image) и элемент множественного выбора (checkboxes). См. раздел «Стабильность идентификаторов» в доке — opt_<index> может сдвинуться между выгрузками, разделёнными правкой анкеты.
| Поле | Тип | Описание | |
|---|---|---|---|
| option_id | string | обязательно | opt_<index> по ТЕКУЩЕЙ позиции варианта в block.options на момент запроса (не заморожен на момент ответа респондента). Сентинелы: opt_other — свободный текст в «Другое»; opt_undecided/opt_refuse — системные варианты «Затрудняюсь ответить»/«Отказ от ответа»; opt_unknown — вариант удалён из анкеты ПОСЛЕ сбора ответа (label сохраняет исторический текст, но опция уже не резолвится к позиции в structure). |
| label | string | обязательно |
RankingItem
Один элемент ответа на ranking-блок, в порядке rank.
| Поле | Тип | Описание | |
|---|---|---|---|
| option_id | string | обязательно | |
| label | string | обязательно | |
| rank | integer | обязательно | 1-based позиция в ранжировании респондента |
MatrixRowValue
Один ответ строки matrix_scale.
| Поле | Тип | Описание | |
|---|---|---|---|
| row_id | string | обязательно | row_<index> — позиция строки В ОТВЕТЕ (answerJson.rows), совпадает со structure для статических матриц. Для матриц с динамическими строками (settings.rowsFrom, options-piping) row_id стабилен ТОЛЬКО в рамках одного ответа — сопоставляйте строки между респондентами по row_label, не по row_id. |
| row_label | string | обязательно | |
| option_id | string | обязательно | opt_<index> выбранной колонки либо opt_<value> для нестандартного значения |
| label | string | обязательно |
DistributionRowValue
Один ответ строки distribution («Распределение 100%»). В отличие от MatrixRowValue здесь нет option_id/label — value уже сама доля в процентах, а не индекс выбранной колонки шкалы.
| Поле | Тип | Описание | |
|---|---|---|---|
| row_id | string | обязательно | row_<index> — позиция строки в block.options (берётся из answerJson.displayOrder): совпадает с row_<index> в structure и стабильна между респондентами даже при включённом перемешивании строк. |
| row_label | string | обязательно | |
| value | number | обязательно | Доля строки в процентах (0–100). Сумма value по всем строкам ОДНОГО ответа равна 100. |
CityValue
Ответ блока city_picker («Город»): город из подсказки справочника адресов (регион, федеральный округ, код ФИАС) или введённый вручную (freeform: true, остальные поля null).
| Поле | Тип | Описание | |
|---|---|---|---|
| name | string | обязательно | |
| region | stringлибоnull | обязательно | |
| federal_district | stringлибоnull | обязательно | |
| fias_id | stringлибоnull | обязательно | |
| freeform | boolean | обязательно |
DictionaryItem
Элемент ответа на блок-справочник (drug_picker, brand_picker, inn_picker, icd_picker, …).
| Поле | Тип | Описание | |
|---|---|---|---|
| id | integerлибоnull | обязательно | id записи в справочнике ФармМонитора; null — свободный текст вне справочника («Другое») |
| label | string | обязательно | |
| source | string | обязательно | Код справочника (drug/brand/inn/icd/…) |
FileUploadValue
Метаданные загруженного файла — БЕЗ url (v1-решение спеки, файл по API не отдаётся).
| Поле | Тип | Описание | |
|---|---|---|---|
| name | stringлибоnull | обязательно | |
| size | numberлибоnull | обязательно | Размер в байтах |
| mime | stringлибоnull | обязательно |
DiaryAnswerValue
Структура дневника назначений (diary) как есть — валидированная v2-форма, каждая запись содержит одно/несколько назначений. Поля entries[] — свободная медицинская структура (диагноз, назначения, дозировки); значения полей freeform-словарей (МКБ/МНН/«Другое») могут содержать вольный текст, вписанный врачом от руки, не только выбор из справочника.
| Поле | Тип | Описание | |
|---|---|---|---|
| entries | array of object | обязательно | Записи дневника назначений (v2), поля см. в доке блока «Дневник назначений» |
FollowupAnswerValue
Структура ответа на блок «Уточнения по ответам» (followup) как есть. answers[questionId] — string | string[] | number по типу под-вопроса; для open_text-под-вопросов и isOther-карточек значение — вольный текст респондента, может содержать произвольный ввод врача.
| Поле | Тип | Описание | |
|---|---|---|---|
| items | array of object | обязательно | По одной карточке на каждый выбранный вариант источника: {option, isOther, answers: {questionId: значение}} |
AnswerValue
Полиморфная форма — конкретная форма зависит от block_type соседнего поля (см. описание AnswerEntry и раздел «Типы блоков» в доке). null — блок не был отвечен (пропущен ветвлением/необязательный) либо ответ не распознан. open_text/final_comment/datetime_picker — string|null; шкалы/number — number|null; single_choice-семейство — ChoiceValue|null; checkboxes — ChoiceValue[]; ranking — RankingItem[]; matrix_scale — MatrixRowValue[]; distribution — DistributionRowValue[]; city_picker — CityValue[]; справочники — DictionaryItem[]; file_upload — FileUploadValue|null; diary — DiaryAnswerValue|null; followup — FollowupAnswerValue|null.
numberлибоstringлибоFileUploadValueлибоDiaryAnswerValueлибоFollowupAnswerValueлибоnullRespondent
Аналитический сегмент респондента. ФИО/email/телефон НЕ отдаются нигде в этом объекте.
| Поле | Тип | Описание | |
|---|---|---|---|
| id | string | обязательно | Детерминированный псевдоним (rsp_<hash>) — HMAC respondentId + соль из env, необратим. Стабилен во всех опросах ОДНОЙ организации; между организациями псевдонимы одного и того же респондента не совпадают. |
| role | stringлибоnull | обязательно | Respondent.subtype — аналитический сегмент роли (doctor/pharmacist/veterinarian/nurse/other_med), не ПД |
| specialization | stringлибоnull | обязательно | Respondent.speciality |
| city | stringлибоnull | обязательно | |
| verified | boolean | обязательно |
AnswerEntry
Один ответ прохождения. ПД-блоки и блоки без ответа (info_screen, presentation) отсутствуют как записи (не value: null — записи нет вовсе).
| Поле | Тип | Описание | |
|---|---|---|---|
| block_id | integer | обязательно | |
| block_type | string | обязательно | |
| question | string | обязательно | |
| value | AnswerValue | обязательно |
ResponseObject
Одно завершённое прохождение опроса (GET /api/v1/surveys/{id}/responses). Контракт иммутабельности: после завершения прохождение не меняется, кроме respondent.id при анонимизации респондента.
| Поле | Тип | Описание | |
|---|---|---|---|
| id | integer | обязательно | |
| survey_id | integer | обязательно | |
| respondent | Respondentлибоnull | обязательно | null — анонимное прохождение (респондент не привязан) |
| started_at | string | обязательно | ISO 8601 UTC |
| completed_at | string | обязательно | ISO 8601 UTC |
| status | enum: "complete""over_quota" | обязательно | |
| answers | array of AnswerEntry | обязательно |
ErrorResponse
Единая форма ошибки для всех эндпоинтов /api/v1/*.
| Поле | Тип | Описание | |||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| error |
| обязательно |
ListSurveysResponse
Опросы всех организаций, доступных ключу (без пагинации — см. §9 спеки).
| Поле | Тип | Описание | |
|---|---|---|---|
| data | array of SurveySummary | обязательно |
ListOrganizationsResponse
Организации, доступные ключу.
| Поле | Тип | Описание | |
|---|---|---|---|
| data | array of OrganizationRef | обязательно |
ListStructureResponse
Плоский список блоков анкеты в порядке структуры (не граф переходов — v1 не отдаёт ветвление).
| Поле | Тип | Описание | |
|---|---|---|---|
| data | array of StructureBlock | обязательно |
ListResponsesResponse
Страница завершённых прохождений (GET /api/v1/surveys/{id}/responses).
| Поле | Тип | Описание | |
|---|---|---|---|
| data | array of ResponseObject | обязательно | |
| next_cursor | string | обязательно | Opaque base64url keyset-курсор (completed_at, id). Возвращается ВСЕГДА, включая has_more=false — resume-курсор для инкрементальной догрузки. |
| has_more | boolean | обязательно |
Стабильность идентификаторов
option_id (opt_<index>) вычисляется по ТЕКУЩЕЙ позиции варианта в block.options на момент КАЖДОГО запроса, а не замораживается на момент сбора ответа. Если автор анкеты переименовывает вариант — option_id остаётся стабильным (по внутренней истории rename). Если автор удаляет вариант из середины списка — option_id всех вариантов ПОСЛЕ удалённого сдвигается: один и тот же исторический ответ на неизменившийся лейбл может получить РАЗНЫЙ opt_<index> в двух выгрузках, сделанных до и после правки анкеты.
Гарантия, которую API даёт: structure[].options[].id и responses[].answers[].value.option_id взаимно согласованы В РАМКАХ ОДНОГО запроса (один и тот же opt_<index> в обеих частях одного ответа API всегда указывает на один и тот же лейбл). Гарантии стабильности opt_<index> МЕЖДУ выгрузками, разделёнными редактированием анкеты, НЕТ.
Если вашей интеграции нужно сопоставлять ответы одного и того же варианта между выгрузками за разное время — используйте label, а не option_id, как ключ сопоставления, либо запрашивайте structure заново перед каждой сверкой и не кэшируйте option_id дольше одного запроса.
То же самое относится к row_id строк matrix_scale с динамическими строками (settings.rowsFrom, ответ на другой блок определяет набор строк в рантайме): row_id стабилен только в рамках одного ответа — между респондентами и между выгрузками сопоставляйте строки по row_label.
row_id строк distribution («Распределение 100%») динамических строк не знает — набор строк всегда статичен (block.options), как у matrix_scale без rowsFrom, поэтому row_id здесь стабилен между респондентами и между выгрузками (совпадает с row_<index> в structure). Тем не менее для сопоставления строк рекомендуем row_label — тот же принцип, что и для остальных типов с построчными ответами.
Иммутабельность и анонимизация: завершённое прохождение не меняется после completed_at — за одним исключением. Анонимизация респондента меняет respondent.id (тот же псевдоним, что был у прохождений этого респондента, перестаёт совпадать между выгрузками «до» и «после» анонимизации). Если для вашей интеграции важна такая консистентность — периодически делайте полный пересбор выгрузки, а не полагайтесь на инкрементал бесконечно.
respondent.id стабилен во всех опросах ОДНОЙ организации — один и тот же псевдоним у всех прохождений одного человека внутри опросов одного владельца. МЕЖДУ организациями псевдонимы одного и того же респондента НЕ совпадают: если ваш ключ видит несколько организаций, не полагайтесь на respondent.id как на кросс-организационный идентификатор — это намеренное ограничение, а не артефакт.
Пагинация и инкрементал
Эндпоинт GET /api/v1/surveys/{id}/responses отдаёт завершённые прохождения через keyset-пагинацию: страница ответа содержит has_more (есть ли ещё данные) и next_cursor — opaque base64url-курсор, который нужно подставить в следующий запрос параметром cursor.
Важный нюанс: next_cursor приходит в ответе ВСЕГДА, даже когда has_more: false — это resume-курсор «после последней уже отданной строки». Именно его нужно сохранять для следующего запуска интеграции, а не полагаться на дату/время локально.
curl -H "Authorization: Bearer brf_live_ВАШ_КЛЮЧ" \
"https://surveys.pharmmonitor.ru/api/v1/surveys/42/responses?limit=500"
# сохраните next_cursor из тела ответа и подставьте в следующий запрос:
curl -H "Authorization: Bearer brf_live_ВАШ_КЛЮЧ" \
"https://surveys.pharmmonitor.ru/api/v1/surveys/42/responses?limit=500&cursor=<next_cursor>"
# повторяйте, пока has_more не станет falseИнкрементальная догрузка (следующий запуск загрузчика — например раз в час): сохраните ПОСЛЕДНИЙ полученный next_cursor и на следующем запуске продолжите с него, без ручного управления датами:
curl -H "Authorization: Bearer brf_live_ВАШ_КЛЮЧ" \
"https://surveys.pharmmonitor.ru/api/v1/surveys/42/responses?cursor=<сохранённый_next_cursor>"Альтернатива для первого запуска/пересбора данных за период — параметр completed_after (ISO 8601, строго «после»); её нельзя комбинировать с cursor в одном запросе.
Лимиты
Лимиты считаются на ключ API. При превышении сервер отвечает 429 с заголовком Retry-After (секунд до следующей попытки).
GET /api/v1/surveys и GET /api/v1/surveys/{id}/structure кэшируются 60 секунд на стороне сервера (в разрезе ключа) — повторный запрос в это окно не расходует лимит на новую выборку из БД, но по-прежнему учитывается в rate limit.
Изменение доступа ключа (сужение или расширение списка организаций) применяется не мгновенно — до 60 секунд, пока не истечёт кэш surveys/structure для этого ключа: организация, только что добавленная/убранная в панели «Интеграции», может ещё минуту оставаться видимой/невидимой по-старому в этих двух эндпоинтах. Отзыв ключа — исключение: он действует мгновенно (следующий же запрос получает 401), кэш здесь ни при чём — авторизация проверяется на каждый запрос заново, до чтения из кэша.
Changelog
v12026-08-24
- Псевдонимы respondent.id стали пер-организационными (PMS-370): стабильны в рамках одной организации, между организациями не совпадают. Значения всех псевдонимов изменились разово (внешних потребителей на момент перехода не было).
v12026-08-22
- Первая версия публичного API: surveys, surveys/{id}, surveys/{id}/structure, surveys/{id}/responses, organizations.
- Keyset-пагинация с resume-курсором, rate limit 60 rpm / burst 120 / 20 000 в сутки на ключ.