Справочник 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 — создание задачи
Тело запроса:
| Поле | Тип | Обязательное | Описание |
|---|---|---|---|
sourceUrls | string[] | да | ссылки на файлы для обработки |
prompts | string[] | нет | инструкции для модели (выполняется только первый промпт); без промптов LLM не вызывается |
neural | object | да | конфигурация модели (см. «Подключение модели») |
ocr | object | да | конфигурация OCR-провайдера (BYOK); обязателен всегда (см. «Конфигурация OCR») |
extractionMode | enum | нет | EXTRACTION_MODE_UNSPECIFIED | EXTRACTION_MODE_HYBRID | EXTRACTION_MODE_OCR_ALWAYS; не задано / UNSPECIFIED наследует дефолт аккаунта (сам по себе по умолчанию HYBRID) |
responseSchema | string | нет | опциональная JSON Schema (сырая строка), ограничивающая JSON-ответ модели; самодостаточная (только внутренние #/$defs), ≤128 KiB, глубина ≤64, ≤10000 узлов; невалидная схема отклоняется с 400 при создании (см. «Структурированный вывод» в документации Job API) |
merge | object | нет | опциональное серверное слияние ответов по частям в единый результат по всему документу; по умолчанию выключено; см. «Опции слияния» ниже и «Слияние результатов по частям» в документации Job API |
consensus | object | нет | опциональное голосование по k прогонам на каждую часть; требует responseSchema (иначе 400); по умолчанию выключено; см. «Опции консенсуса» ниже и «Консенсус по нескольким запускам» в документации Job API |
title | string | нет | произвольное название задачи |
metadata | map<string,string> | нет | произвольные строковые пары «ключ-значение» |
webhookUrl | string | нет | абсолютный http/https-адрес эндпоинта для уведомления о завершении задачи (см. «Вебхуки») |
webhookSecret | string | нет | опциональный HMAC-секрет для подписи вебхук-запросов; принимается только на вход, в ответах не возвращается |
idempotencyKey | string | нет | клиентский ключ для безопасных повторных запросов; максимум 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.provider | enum | да | один из NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI |
ocr.model | string | да | идентификатор модели провайдера, напр. mistral-ocr-latest (Mistral) или vision-модель выбранного провайдера |
ocr.providerKey | string | да | 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.enabled | bool | нет | включает серверное слияние для этой задачи |
merge.scope | enum | нет | MERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (по умолчанию) | MERGE_SCOPE_JOB; одна запись merged[] на файл либо одна по всем файлам |
merge.conflictPolicy | enum | нет | MERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (по умолчанию) | MERGE_CONFLICT_POLICY_MAJORITY |
merge.dedupeBy | string[] | нет | имена JSON-полей, однозначно идентифицирующих запись, для дедупликации по содержимому между файлами; только JSON + responseSchema; не более 32 имён полей — при превышении запрос отклоняется с 400 при создании задачи; пусто/не задано → только техническая дедупликация |
merge.format | enum | нет | 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.mode | enum | нет | 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, .xml → xml, .txt → plain, остальное (включая изображения и офисные файлы) → markdown. llm[].content — текст ответа модели как есть (структуру держит ваш промпт; серверной валидации нет, если задача не задавала responseSchema — тогда соответствие сообщают поля llm[].schemaValid/schemaErrors). В примере выше пустые/нулевые поля ("", {}, 0) показаны для полноты — protojson опускает их, поэтому в реальном ответе они могут отсутствовать.
Поля ocr[]:
| Поле | Тип | Описание |
|---|---|---|
jobId / file | string | id задачи / URL файла |
status | enum | JOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
content | string | распознанный текст в формате outputFormat |
outputFormat | string | формат content: markdown / html / xml / plain. Определяет стратегию структурно-осведомлённого разбиения на части на LLM-стадии |
model | string | метод/движок OCR (напр. pdf_fitz) |
request / rawData | string | отладочные: запрос к OCR и сырой ответ |
error | string | ошибка стадии (пусто, если успех) |
duration | string | длительность, нс (число строкой — protojson отдаёт int64 как строку) |
created / updated | string | RFC3339 |
Поля llm[]:
| Поле | Тип | Описание |
|---|---|---|
jobId / file | string | id задачи / URL файла |
promptIndex | int | индекс промпта (сейчас всегда 0) |
chunkIndex / chunkTotal | int | номер части / всего частей (если текст делился) |
status | enum | JOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
skipReason | string | при SKIPPED: no_prompt / ocr_failed / no_ocr_content / empty_ocr_content |
content | string | ответ модели (текст как есть) |
model | string | строка модели из запроса |
request / rawData | string | отладочные |
error | string | ошибка стадии (напр. provider error (category=…, status=400)) |
duration | string | длительность, нс (число строкой — protojson отдаёт int64 как строку) |
created / updated | string | RFC3339 |
schemaValid | bool | присутствует, только если в задаче задан responseSchema и файл не делился на части (chunkTotal = 1): соответствует ли content схеме |
schemaErrors | string[] | нарушения соответствия схеме, когда schemaValid равно false (≤10, до 512 байт каждое) |
Поля llm[].consensus (присутствуют, только если в задаче задан consensus.mode; в задачах с консенсусом пропущенные/сбойные строки всё равно несут consensus: null):
| Поле | Тип | Описание |
|---|---|---|
k | int | запрошено прогонов (2 / 3 / 5) |
runsVotable | int | прогоны, давшие разбираемый JSON-ответ в пределах лимитов и участвовавшие в голосовании |
minAgreement | number | наименьшее согласие по полям в этой строке, в виде доли; 0 — зарезервированный признак: голосования не было (runsVotable < 2) либо оно деградировало, а не «0% согласия» |
incomplete | bool | true, когда runsVotable < k, либо голосование деградировало |
disagreements | array | поля, по которым прогоны не пришли к полному согласию, от наименьшего согласия к наибольшему; каждая запись: fieldPath (путь через точку; "" — корень документа; может повторяться между записями, если по одному пути есть и спор о наличии, и спор об элементах массива — рендерите каждую запись отдельно, не дедуплицируйте по пути), variants[] (value — компактный JSON, усечённый rune-safe до 512 байт; runs — сколько прогонов, участвовавших в голосовании, дали это значение; included — победил в голосовании / присутствует в согласованном ответе, независимо от того, присутствует ли его ключ текстово — победивший null имеет included: true, даже если ключ опущен), variantsDropped (варианты, обрезанные из этой записи, только счётчик) |
disagreementsDropped | int | записи расхождений, обрезанные из строки (только счётчик; сохраняется ≤100 записей) |
Поля merged[] (присутствуют, только если в задаче задан merge.enabled):
| Поле | Тип | Описание |
|---|---|---|
file | string | URL файла, к которому относится этот объединённый ответ; пусто ("") при scope=job |
format | string | формат content: json / xml / html / markdown / text |
content | string | объединённый ответ по всему документу (или файлу) |
schemaValid | bool | присутствует, только если в задаче задан responseSchema: соответствует ли ей объединённый content |
conflicts | array | поля, расходившиеся между частями (не более 100 записей, не более 10 конкурирующих значений в каждой, не более 512 байт на значение); каждая запись: field, конкурирующие values[], sources[] (ссылки на file + chunk) — values[i] соответствует sources[i] (индексы совпадают) |
dedupeRemovedTechnical | int | записи, удалённые автоматической дедупликацией по перекрытию границ |
dedupeRemovedContent | int | записи, удалённые благодаря вашим полям dedupeBy |
mergeIncomplete | bool | true, если хотя бы одну часть не удалось безопасно слить и она была добавлена как есть |
incompleteChunks | array | части, которые не удалось слить; каждая запись: 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, генерируется из определений сервиса. Подходит любому генератору клиентов.