Referencia de la API
URL base: https://api.hotdoc.io. Los cuerpos de las solicitudes y respuestas son JSON; los nombres de campo están en camelCase. Excepciones: POST /v1/jobs/upload (respuesta) y el cuerpo de la solicitud del webhook, que usan snake_case.
Endpoints de procesamiento
| Método | Endpoint | Propósito |
|---|---|---|
POST | /v1/jobs | crear una tarea |
GET | /v1/jobs/{id} | estado de la tarea y lista de archivos (sin resultados de reconocimiento) |
GET | /v1/jobs/{id}/result | resultado completo: texto reconocido y respuestas del modelo por archivo |
GET | /v1/jobs | listar las tareas de la cuenta (paginación: pageSize, pageToken, filtro statusEq) |
POST | /v1/jobs/upload | subir un único archivo (multipart/form-data) |
POST /v1/jobs — crear una tarea
Cuerpo de la solicitud:
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
sourceUrls | string[] | sí | URL de archivos a procesar |
prompts | string[] | no | instrucciones para el modelo (solo se ejecuta el primer prompt); sin prompts, el LLM no se invoca |
neural | object | sí | configuración del modelo (consulte «Conectar un modelo») |
ocr | object | sí | configuración del proveedor de OCR (BYOK); siempre obligatoria (consulte «Configuración de OCR») |
extractionMode | enum | no | EXTRACTION_MODE_UNSPECIFIED | EXTRACTION_MODE_HYBRID | EXTRACTION_MODE_OCR_ALWAYS; si se omite / UNSPECIFIED, hereda el valor predeterminado de la cuenta (que a su vez es HYBRID por defecto) |
responseSchema | string | no | JSON Schema opcional (cadena en bruto) que restringe la respuesta JSON del modelo; autocontenida (solo #/$defs internos), ≤128 KiB, profundidad ≤64, ≤10000 nodos; un esquema inválido se rechaza con 400 al crear (consulte «Salida estructurada» en la documentación del Job API) |
merge | object | no | fusión opcional en el servidor de las respuestas por fragmento en un único resultado para todo el documento; desactivada por defecto; consulte «Opciones de fusión» más abajo y «Fusión de resultados fragmentados» en la documentación del Job API |
consensus | object | no | votación de consenso opcional sobre k ejecuciones por fragmento; requiere responseSchema (si no, 400); desactivada por defecto; consulte «Opciones de consenso» más abajo y «Ejecuciones de consenso» en la documentación del Job API |
title | string | no | nombre arbitrario de la tarea |
metadata | map<string,string> | no | pares clave-valor de cadenas arbitrarios |
webhookUrl | string | no | endpoint absoluto http/https que se notificará al completarse la tarea (consulte «Webhooks») |
webhookSecret | string | no | secreto HMAC opcional para firmar las solicitudes de webhook; se acepta solo como entrada, nunca se devuelve en las respuestas |
idempotencyKey | string | no | clave de cliente para reintentos de creación seguros; máximo 255 caracteres (consulte «Idempotencia») |
neural y ocr son siempre obligatorios, incluso cuando prompts está vacío. Con prompts vacío, el modelo no se invoca: cada archivo se marca como JOB_LLM_STATUS_SKIPPED con skipReason=no_prompt y, si el OCR es correcto, la tarea termina como JOB_STATUS_COMPLETE.
La respuesta es la tarea creada en estado JOB_STATUS_NEW; responseSchema se devuelve en ese objeto (vacío si no se estableció); consensus se devuelve como un objeto poblado — mode: "CONSENSUS_MODE_UNSPECIFIED" si no se estableció, nunca como un campo omitido o null.
Configuración de OCR
ocr selecciona el backend de reconocimiento (OCR) por solicitud (BYOK):
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
ocr.provider | enum | sí | uno de NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI |
ocr.model | string | sí | el id del modelo del proveedor, p. ej. mistral-ocr-latest (Mistral) o el modelo de visión del proveedor elegido |
ocr.providerKey | string | sí | clave BYOK para el proveedor de OCR; solo entrada, nunca se devuelve |
Mistral es el backend de OCR habitual (mistral-ocr-latest); otros proveedores ejecutan
OCR mediante vision-chat. La clave se almacena cifrada y se elimina con la tarea.
Nota. El modo de extracción es un campo de nivel superior, extractionMode, no forma parte de ocr: determina si el OCR se ejecuta siempre (EXTRACTION_MODE_OCR_ALWAYS) o se aplica por archivo a criterio del convertidor (EXTRACTION_MODE_HYBRID, el valor predeterminado).
¿Qué modelo usar? Compare inteligencia, precio por token y proveedores en la comparativa de LLM y elija el modelo adecuado.
Opciones de fusión (merge)
merge es opcional y está desactivada por defecto — active merge.enabled para optar por ella. El resto de campos merge.* se ignoran mientras enabled sea false/se omita.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
merge.enabled | bool | no | activa la fusión en el servidor para esta tarea |
merge.scope | enum | no | MERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (por defecto) | MERGE_SCOPE_JOB; una entrada merged[] por archivo, o una para todos los archivos |
merge.conflictPolicy | enum | no | MERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (por defecto) | MERGE_CONFLICT_POLICY_MAJORITY |
merge.dedupeBy | string[] | no | nombres de campo JSON que identifican un registro único, para deduplicación de contenido entre archivos; solo JSON + responseSchema; máximo 32 nombres de campo — más se rechazan con 400 al crear la tarea; vacío/omitido → solo deduplicación técnica |
merge.format | enum | no | MERGE_FORMAT_UNSPECIFIED | MERGE_FORMAT_AUTO (por defecto) | MERGE_FORMAT_JSON | MERGE_FORMAT_XML | MERGE_FORMAT_HTML | MERGE_FORMAT_MARKDOWN | MERGE_FORMAT_TEXT; anula la detección automática de formato por archivo |
Consulte «Fusión de resultados fragmentados» en la documentación del Job API para el recorrido completo (alcance, resolución de conflictos, los dos tipos de deduplicación).
Opciones de consenso (consensus)
consensus es opcional y está desactivado por defecto. Cuando se define, consensus.mode debe ser un valor reconocido y la tarea debe tener responseSchema definido — una tarea con consensus.mode definido pero sin responseSchema se rechaza con 400 al crearla.
| Campo | Tipo | Obligatorio | Descripción |
|---|---|---|---|
consensus.mode | enum | no | CONSENSUS_MODE_UNSPECIFIED (desactivado, por defecto) | CONSENSUS_MODE_TWO_RUNS | CONSENSUS_MODE_THREE_RUNS | CONSENSUS_MODE_FIVE_RUNS; un valor de cadena no reconocido lo ignora silenciosamente el gateway — la tarea se ejecuta sin consenso, no recibe 400 |
Consulte «Ejecuciones de consenso» en la documentación del Job API para el recorrido completo (modos, cómo leer minAgreement/disagreements, advertencias honestas).
Idempotencia
Envíe idempotencyKey para hacer que POST /v1/jobs sea seguro de reintentar. Un reintento con la
misma clave y parámetros de solicitud idénticos devuelve la tarea original (no se crea
ningún duplicado). La misma clave con parámetros diferentes se rechaza con
400 "idempotency key reused with different request parameters". La clave tiene como
máximo 255 caracteres; genere un UUID nuevo por cada creación lógica.
GET /v1/jobs/{id}/result — resultado
Devuelve { "result": { "job": …, "ocr": [...], "llm": [...] } }. Ejemplo (abreviado):
{
"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 en la respuesta no incluye apiKey; el objeto Job no tiene campo ocr: la clave de OCR nunca se devuelve. ocr[].content es el texto reconocido en el formato ocr[].outputFormat; el formato depende del tipo de archivo: PDF y HTML → html, .xml → xml, .txt → plain, todo lo demás (incluidas imágenes y archivos de oficina) → markdown. llm[].content es el texto de la respuesta del modelo tal cual (su prompt determina la estructura; no hay validación del lado del servidor, salvo que la tarea haya establecido responseSchema — en ese caso, llm[].schemaValid/schemaErrors informan la conformidad). En el ejemplo anterior se muestran campos vacíos/cero ("", {}, 0) por exhaustividad: protojson los omite, así que pueden estar ausentes en una respuesta real.
Campos de ocr[]:
| Campo | Tipo | Descripción |
|---|---|---|
jobId / file | string | id de la tarea / URL del archivo |
status | enum | JOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
content | string | texto reconocido en el formato outputFormat |
outputFormat | string | formato de content: markdown / html / xml / plain. Determina la división consciente de la estructura en la etapa LLM |
model | string | método/motor de OCR (p. ej. pdf_fitz) |
request / rawData | string | depuración: la solicitud de OCR y la respuesta en bruto |
error | string | error de la etapa (vacío si todo va bien) |
duration | string | duración, ns (un número como cadena: protojson devuelve int64 como cadena) |
created / updated | string | RFC3339 |
Campos de llm[]:
| Campo | Tipo | Descripción |
|---|---|---|
jobId / file | string | id de la tarea / URL del archivo |
promptIndex | int | índice del prompt (actualmente siempre 0) |
chunkIndex / chunkTotal | int | número de fragmento / total de fragmentos (si el texto se dividió) |
status | enum | JOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
skipReason | string | cuando es SKIPPED: no_prompt / ocr_failed / no_ocr_content / empty_ocr_content |
content | string | respuesta del modelo (texto tal cual) |
model | string | la cadena de modelo de la solicitud |
request / rawData | string | depuración |
error | string | error de la etapa (p. ej. provider error (category=…, status=400)) |
duration | string | duración, ns (un número como cadena: protojson devuelve int64 como cadena) |
created / updated | string | RFC3339 |
schemaValid | bool | presente solo si la tarea estableció responseSchema y el archivo no se dividió (chunkTotal = 1): si content cumple el esquema |
schemaErrors | string[] | infracciones de conformidad cuando schemaValid es false (≤10, ≤512 bytes cada una) |
Campos de llm[].consensus (presentes solo si la tarea definió consensus.mode; en tareas con consenso, las filas omitidas/fallidas siguen llevando consensus: null):
| Campo | Tipo | Descripción |
|---|---|---|
k | int | ejecuciones solicitadas (2 / 3 / 5) |
runsVotable | int | ejecuciones que produjeron una respuesta JSON analizable y dentro de presupuesto, y que participaron en la votación |
minAgreement | number | el acuerdo por campo más bajo de la fila, como fracción; 0 es un centinela reservado — no hubo votación (runsVotable < 2) o la votación se degradó, no «0 % de acuerdo» |
incomplete | bool | true cuando runsVotable < k, o la votación se degradó |
disagreements | array | campos en los que las ejecuciones no coincidieron por completo, con el menor acuerdo primero; cada entrada: fieldPath (ruta con puntos; "" = raíz del documento; puede repetirse entre entradas cuando una ruta tiene tanto una disputa de presencia como una disputa de elementos de array — renderice cada entrada por separado, no deduplique por ruta), variants[] (value — JSON compacto, truncado de forma rune-safe a 512 bytes; runs — cuántas ejecuciones votables lo produjeron; included — ganó la votación / está presente en la respuesta acordada, independientemente de si su clave está presente textualmente — un null ganador tiene included: true aunque la clave se omita), variantsDropped (variantes recortadas de esta entrada, solo el recuento) |
disagreementsDropped | int | entradas de discrepancia recortadas de la fila (solo el recuento; se conservan ≤100 entradas) |
Campos de merged[] (presentes solo si la tarea definió merge.enabled):
| Campo | Tipo | Descripción |
|---|---|---|
file | string | URL del archivo al que corresponde esta respuesta fusionada; vacío ("") con scope=job |
format | string | formato de content: json / xml / html / markdown / text |
content | string | la respuesta fusionada para todo el documento (o el archivo) |
schemaValid | bool | presente solo si la tarea definió responseSchema: si el content fusionado cumple con ella |
conflicts | array | campos que discreparon entre fragmentos (máx. 100 entradas, máx. 10 valores en competencia cada una, máx. 512 bytes por valor); cada entrada: field, values[] en competencia, sources[] (referencias de file + chunk) — values[i] corresponde a sources[i] (alineados por índice) |
dedupeRemovedTechnical | int | registros eliminados por la deduplicación automática de solapamiento en el límite |
dedupeRemovedContent | int | registros eliminados por sus campos dedupeBy |
mergeIncomplete | bool | true cuando al menos un fragmento no pudo fusionarse con seguridad y se añadió tal cual |
incompleteChunks | array | los fragmentos que no pudieron fusionarse; cada entrada: file, índice de chunk |
Nota. Una descripción legible por máquina de la API de Jobs en formato OpenAPI 3.0.3 está publicada en
/openapi.yaml. Se genera a partir de las definiciones protobuf del servicio, así que no se desvía de la API; esta página sigue siendo la referencia en prosa. Puedes pasarle el archivo a cualquier generador de clientes compatible con OpenAPI.
POST /v1/jobs/upload — subir un archivo
Un endpoint HTTP independiente (no grpc-gateway). Acepta un archivo vía multipart/form-data.
Respuesta (snake_case — una excepción al camelCase general):
{"url": "https://…/files/…/document.pdf", "name": "document.pdf", "size_bytes": 204800}
Use la url devuelta en sourceUrls al crear una tarea.
El cuerpo de error de este endpoint es {"code": <int>, "message": "…"}, sin array details. El límite de tamaño de archivo lo fija la configuración (grpc.maxRecvMsgBytes; 20 MiB en el despliegue actual), no un valor por defecto del código.
Versionado
La ruta /v1 es estable. Los cambios incompatibles hacia atrás se publican bajo una nueva ruta (/v2). Los cambios aditivos (nuevos campos y endpoints opcionales) no rompen la compatibilidad y se anuncian en el Changelog.
Recursos para desarrolladores
- Especificación OpenAPIOpenAPI 3.0.3, generada a partir de las definiciones del servicio. Sirve para cualquier generador de clientes.