Справочник API

Базовый URL: https://api.hotdoc.io. Тела запросов и ответов — JSON, имена полей в camelCase. Исключения: POST /v1/jobs/upload (ответ) и тело вебхук-запроса — snake_case.

Эндпоинты обработки

МетодЭндпоинтНазначение
POST/v1/jobsсоздать задачу
GET/v1/jobs/{id}статус задачи и список файлов (без результатов распознавания)
GET/v1/jobs/{id}/resultполный результат: распознанный текст и ответы модели по файлам
GET/v1/jobsсписок задач аккаунта (пагинация: pageSize, pageToken, фильтр statusEq)
POST/v1/jobs/uploadзагрузка одного файла (multipart/form-data)

POST /v1/jobs — создание задачи

Тело запроса:

ПолеТипОбязательноеОписание
sourceUrlsstring[]дассылки на файлы для обработки
promptsstring[]нетинструкции для модели (выполняется только первый промпт); без промптов LLM не вызывается
neuralobjectдаконфигурация модели (см. «Подключение модели»)
ocrobjectдаконфигурация OCR-провайдера (BYOK); обязателен всегда (см. «Конфигурация OCR»)
extractionModeenumнетEXTRACTION_MODE_UNSPECIFIED | EXTRACTION_MODE_HYBRID | EXTRACTION_MODE_OCR_ALWAYS; не задано / UNSPECIFIED наследует дефолт аккаунта (сам по себе по умолчанию HYBRID)
responseSchemastringнетопциональная JSON Schema (сырая строка), ограничивающая JSON-ответ модели; самодостаточная (только внутренние #/$defs), ≤128 KiB, глубина ≤64, ≤10000 узлов; невалидная схема отклоняется с 400 при создании (см. «Структурированный вывод» в документации Job API)
mergeobjectнетопциональное серверное слияние ответов по частям в единый результат по всему документу; по умолчанию выключено; см. «Опции слияния» ниже и «Слияние результатов по частям» в документации Job API
consensusobjectнетопциональное голосование по k прогонам на каждую часть; требует responseSchema (иначе 400); по умолчанию выключено; см. «Опции консенсуса» ниже и «Консенсус по нескольким запускам» в документации Job API
titlestringнетпроизвольное название задачи
metadatamap<string,string>нетпроизвольные строковые пары «ключ-значение»
webhookUrlstringнетабсолютный http/https-адрес эндпоинта для уведомления о завершении задачи (см. «Вебхуки»)
webhookSecretstringнетопциональный HMAC-секрет для подписи вебхук-запросов; принимается только на вход, в ответах не возвращается
idempotencyKeystringнетклиентский ключ для безопасных повторных запросов; максимум 255 символов (см. «Идемпотентность»)

neural и ocr обязательны всегда, даже если prompts пуст. При пустом prompts модель не вызывается: каждому файлу проставляется JOB_LLM_STATUS_SKIPPED со skipReason=no_prompt, и при успешном OCR задача завершается JOB_STATUS_COMPLETE.

Ответ — созданная задача в статусе JOB_STATUS_NEW; responseSchema возвращается в этом объекте (пусто, если не задавалась); consensus возвращается как заполненный объект — mode: "CONSENSUS_MODE_UNSPECIFIED", если не задавался, а не как отсутствующее или null-поле.

Конфигурация OCR

ocr задаёт бэкенд распознавания (OCR) для каждого запроса (BYOK):

ПолеТипОбязательноеОписание
ocr.providerenumдаодин из NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI
ocr.modelstringдаидентификатор модели провайдера, напр. mistral-ocr-latest (Mistral) или vision-модель выбранного провайдера
ocr.providerKeystringдаBYOK-ключ для OCR-провайдера; только на вход, в ответах не возвращается

Mistral — типовой OCR-бэкенд (mistral-ocr-latest); остальные провайдеры выполняют vision-chat OCR. Ключ хранится в зашифрованном виде и удаляется вместе с задачей.

Примечание. Режим извлечения — это поле верхнего уровня extractionMode, а не часть ocr: оно определяет, запускается ли OCR вообще (EXTRACTION_MODE_OCR_ALWAYS) или применяется по каждому файлу на усмотрение конвертера (EXTRACTION_MODE_HYBRID, по умолчанию).

Какую модель использовать? Сравните интеллект, цену за токен и провайдеров в сравнении LLM и укажите подходящую модель.

Опции слияния (merge)

merge опционален и по умолчанию выключен — установите merge.enabled, чтобы включить его. Пока enabled равно false/не задано, остальные поля merge.* игнорируются.

ПолеТипОбязательноеОписание
merge.enabledboolнетвключает серверное слияние для этой задачи
merge.scopeenumнетMERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (по умолчанию) | MERGE_SCOPE_JOB; одна запись merged[] на файл либо одна по всем файлам
merge.conflictPolicyenumнетMERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (по умолчанию) | MERGE_CONFLICT_POLICY_MAJORITY
merge.dedupeBystring[]нетимена JSON-полей, однозначно идентифицирующих запись, для дедупликации по содержимому между файлами; только JSON + responseSchema; не более 32 имён полей — при превышении запрос отклоняется с 400 при создании задачи; пусто/не задано → только техническая дедупликация
merge.formatenumнетMERGE_FORMAT_UNSPECIFIED | MERGE_FORMAT_AUTO (по умолчанию) | MERGE_FORMAT_JSON | MERGE_FORMAT_XML | MERGE_FORMAT_HTML | MERGE_FORMAT_MARKDOWN | MERGE_FORMAT_TEXT; переопределяет автоматическое определение формата по каждому файлу

Подробный разбор (область слияния, разрешение конфликтов, два вида дедупликации) — в «Слияние результатов по частям» в документации Job API.

Опции консенсуса (consensus)

consensus опционален и по умолчанию выключен. Если он задан, consensus.mode должен быть распознаваемым значением, и в задаче должен быть задан responseSchema — задача с заданным consensus.mode, но без responseSchema, отклоняется с 400 при создании.

ПолеТипОбязательноеОписание
consensus.modeenumнетCONSENSUS_MODE_UNSPECIFIED (выключено, по умолчанию) | CONSENSUS_MODE_TWO_RUNS | CONSENSUS_MODE_THREE_RUNS | CONSENSUS_MODE_FIVE_RUNS; нераспознанное строковое значение шлюз молча игнорирует — задача выполняется без консенсуса, а не с 400

Подробный разбор (режимы, чтение minAgreement/disagreements, важные оговорки) — в «Консенсус по нескольким запускам» в документации Job API.

Идемпотентность

Передайте idempotencyKey, чтобы сделать POST /v1/jobs безопасным для повтора. Повторный запрос с тем же ключом и идентичными параметрами вернёт исходную задачу (дубликат не создаётся). Тот же ключ с другими параметрами отклоняется с кодом 400 "idempotency key reused with different request parameters". Ключ — максимум 255 символов; генерируйте новый UUID для каждого логического создания задачи.

GET /v1/jobs/{id}/result — результат

Возвращает { "result": { "job": …, "ocr": [...], "llm": [...] } }. Пример (сокращён):

{
  "result": {
    "job": {
      "id": "15b07304-…", "accountId": "…", "title": "Invoice #42",
      "metadata": {}, "status": "JOB_STATUS_COMPLETE", "error": "",
      "sourceUrls": ["https://example.com/invoice.pdf"],
      "files": ["https://…/files/15b07304-…/invoice.pdf"],
      "prompts": ["…"], "neural": { "type": "NEURAL_CLIENT_TYPE_XIAOMI", "model": "mimo-v2-flash", "chunkBudgetTokens": 240000 },
      "created": "2026-06-17T16:57:49Z", "updated": "2026-06-17T16:58:05Z"
    },
    "ocr": [
      { "jobId": "15b07304-…", "file": "https://…/invoice.pdf",
        "status": "JOB_OCR_STATUS_DONE", "content": "<p><b>…</b></p>",
        "outputFormat": "html", "model": "pdf_fitz", "error": "", "duration": "149508116",
        "created": "…", "updated": "…" }
    ],
    "llm": [
      { "jobId": "15b07304-…", "file": "https://…/invoice.pdf",
        "promptIndex": 0, "chunkIndex": 0, "chunkTotal": 1,
        "status": "JOB_LLM_STATUS_DONE", "skipReason": "",
        "model": "mimo-v2-flash", "content": "{\"total\": 15000}",
        "error": "", "duration": "601925111", "created": "…", "updated": "…" }
    ]
  }
}

neural в ответе не содержит apiKey; объект Job не содержит поля ocr — ключ OCR не возвращается никогда. ocr[].content — это распознанный текст в формате ocr[].outputFormat; формат зависит от типа файла: PDF и HTML → html, .xmlxml, .txtplain, остальное (включая изображения и офисные файлы) → markdown. llm[].content — текст ответа модели как есть (структуру держит ваш промпт; серверной валидации нет, если задача не задавала responseSchema — тогда соответствие сообщают поля llm[].schemaValid/schemaErrors). В примере выше пустые/нулевые поля ("", {}, 0) показаны для полноты — protojson опускает их, поэтому в реальном ответе они могут отсутствовать.

Поля ocr[]:

ПолеТипОписание
jobId / filestringid задачи / URL файла
statusenumJOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
contentstringраспознанный текст в формате outputFormat
outputFormatstringформат content: markdown / html / xml / plain. Определяет стратегию структурно-осведомлённого разбиения на части на LLM-стадии
modelstringметод/движок OCR (напр. pdf_fitz)
request / rawDatastringотладочные: запрос к OCR и сырой ответ
errorstringошибка стадии (пусто, если успех)
durationstringдлительность, нс (число строкой — protojson отдаёт int64 как строку)
created / updatedstringRFC3339

Поля llm[]:

ПолеТипОписание
jobId / filestringid задачи / URL файла
promptIndexintиндекс промпта (сейчас всегда 0)
chunkIndex / chunkTotalintномер части / всего частей (если текст делился)
statusenumJOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
skipReasonstringпри SKIPPED: no_prompt / ocr_failed / no_ocr_content / empty_ocr_content
contentstringответ модели (текст как есть)
modelstringстрока модели из запроса
request / rawDatastringотладочные
errorstringошибка стадии (напр. provider error (category=…, status=400))
durationstringдлительность, нс (число строкой — protojson отдаёт int64 как строку)
created / updatedstringRFC3339
schemaValidboolприсутствует, только если в задаче задан responseSchema и файл не делился на части (chunkTotal = 1): соответствует ли content схеме
schemaErrorsstring[]нарушения соответствия схеме, когда schemaValid равно false (≤10, до 512 байт каждое)

Поля llm[].consensus (присутствуют, только если в задаче задан consensus.mode; в задачах с консенсусом пропущенные/сбойные строки всё равно несут consensus: null):

ПолеТипОписание
kintзапрошено прогонов (2 / 3 / 5)
runsVotableintпрогоны, давшие разбираемый JSON-ответ в пределах лимитов и участвовавшие в голосовании
minAgreementnumberнаименьшее согласие по полям в этой строке, в виде доли; 0 — зарезервированный признак: голосования не было (runsVotable < 2) либо оно деградировало, а не «0% согласия»
incompletebooltrue, когда runsVotable < k, либо голосование деградировало
disagreementsarrayполя, по которым прогоны не пришли к полному согласию, от наименьшего согласия к наибольшему; каждая запись: fieldPath (путь через точку; "" — корень документа; может повторяться между записями, если по одному пути есть и спор о наличии, и спор об элементах массива — рендерите каждую запись отдельно, не дедуплицируйте по пути), variants[] (value — компактный JSON, усечённый rune-safe до 512 байт; runs — сколько прогонов, участвовавших в голосовании, дали это значение; included — победил в голосовании / присутствует в согласованном ответе, независимо от того, присутствует ли его ключ текстово — победивший null имеет included: true, даже если ключ опущен), variantsDropped (варианты, обрезанные из этой записи, только счётчик)
disagreementsDroppedintзаписи расхождений, обрезанные из строки (только счётчик; сохраняется ≤100 записей)

Поля merged[] (присутствуют, только если в задаче задан merge.enabled):

ПолеТипОписание
filestringURL файла, к которому относится этот объединённый ответ; пусто ("") при scope=job
formatstringформат content: json / xml / html / markdown / text
contentstringобъединённый ответ по всему документу (или файлу)
schemaValidboolприсутствует, только если в задаче задан responseSchema: соответствует ли ей объединённый content
conflictsarrayполя, расходившиеся между частями (не более 100 записей, не более 10 конкурирующих значений в каждой, не более 512 байт на значение); каждая запись: field, конкурирующие values[], sources[] (ссылки на file + chunk) — values[i] соответствует sources[i] (индексы совпадают)
dedupeRemovedTechnicalintзаписи, удалённые автоматической дедупликацией по перекрытию границ
dedupeRemovedContentintзаписи, удалённые благодаря вашим полям dedupeBy
mergeIncompletebooltrue, если хотя бы одну часть не удалось безопасно слить и она была добавлена как есть
incompleteChunksarrayчасти, которые не удалось слить; каждая запись: file, индекс chunk

Примечание. Машиночитаемое описание Jobs API в формате OpenAPI 3.0.3 опубликовано по адресу /openapi.yaml. Оно генерируется из protobuf-определений сервиса и потому не расходится с API; эта страница остаётся текстовым справочником. Файл можно передать любому генератору клиентов, понимающему OpenAPI.

POST /v1/jobs/upload — загрузка файла

Отдельный HTTP-эндпоинт (не grpc-gateway). Принимает файл через multipart/form-data.

Ответ (snake_case, исключение из общего camelCase):

{"url": "https://…/files/…/document.pdf", "name": "document.pdf", "size_bytes": 204800}

Используйте полученный url в sourceUrls при создании задачи.

Тело ошибки этого эндпоинта — {"code": <int>, "message": "…"} без массива details. Лимит размера файла задаётся конфигом (grpc.maxRecvMsgBytes; в текущем деплое — 20 MiB), не код-дефолтом.

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

Путь /v1 стабилен. Обратно несовместимые изменения выходят под новым путём (/v2). Аддитивные изменения (новые опциональные поля и эндпоинты) совместимость не ломают и анонсируются в Changelog.

Ресурсы для разработчиков

  • Спецификация OpenAPIOpenAPI 3.0.3, генерируется из определений сервиса. Подходит любому генератору клиентов.