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étodoEndpointPropósito
POST/v1/jobscrear una tarea
GET/v1/jobs/{id}estado de la tarea y lista de archivos (sin resultados de reconocimiento)
GET/v1/jobs/{id}/resultresultado completo: texto reconocido y respuestas del modelo por archivo
GET/v1/jobslistar las tareas de la cuenta (paginación: pageSize, pageToken, filtro statusEq)
POST/v1/jobs/uploadsubir un único archivo (multipart/form-data)

POST /v1/jobs — crear una tarea

Cuerpo de la solicitud:

CampoTipoObligatorioDescripción
sourceUrlsstring[]URL de archivos a procesar
promptsstring[]noinstrucciones para el modelo (solo se ejecuta el primer prompt); sin prompts, el LLM no se invoca
neuralobjectconfiguración del modelo (consulte «Conectar un modelo»)
ocrobjectconfiguración del proveedor de OCR (BYOK); siempre obligatoria (consulte «Configuración de OCR»)
extractionModeenumnoEXTRACTION_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)
responseSchemastringnoJSON 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)
mergeobjectnofusió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
consensusobjectnovotació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
titlestringnonombre arbitrario de la tarea
metadatamap<string,string>nopares clave-valor de cadenas arbitrarios
webhookUrlstringnoendpoint absoluto http/https que se notificará al completarse la tarea (consulte «Webhooks»)
webhookSecretstringnosecreto HMAC opcional para firmar las solicitudes de webhook; se acepta solo como entrada, nunca se devuelve en las respuestas
idempotencyKeystringnoclave 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):

CampoTipoObligatorioDescripción
ocr.providerenumuno de NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI
ocr.modelstringel id del modelo del proveedor, p. ej. mistral-ocr-latest (Mistral) o el modelo de visión del proveedor elegido
ocr.providerKeystringclave 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.

CampoTipoObligatorioDescripción
merge.enabledboolnoactiva la fusión en el servidor para esta tarea
merge.scopeenumnoMERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (por defecto) | MERGE_SCOPE_JOB; una entrada merged[] por archivo, o una para todos los archivos
merge.conflictPolicyenumnoMERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (por defecto) | MERGE_CONFLICT_POLICY_MAJORITY
merge.dedupeBystring[]nonombres 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.formatenumnoMERGE_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.

CampoTipoObligatorioDescripción
consensus.modeenumnoCONSENSUS_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, .xmlxml, .txtplain, 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[]:

CampoTipoDescripción
jobId / filestringid de la tarea / URL del archivo
statusenumJOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
contentstringtexto reconocido en el formato outputFormat
outputFormatstringformato de content: markdown / html / xml / plain. Determina la división consciente de la estructura en la etapa LLM
modelstringmétodo/motor de OCR (p. ej. pdf_fitz)
request / rawDatastringdepuración: la solicitud de OCR y la respuesta en bruto
errorstringerror de la etapa (vacío si todo va bien)
durationstringduración, ns (un número como cadena: protojson devuelve int64 como cadena)
created / updatedstringRFC3339

Campos de llm[]:

CampoTipoDescripción
jobId / filestringid de la tarea / URL del archivo
promptIndexintíndice del prompt (actualmente siempre 0)
chunkIndex / chunkTotalintnúmero de fragmento / total de fragmentos (si el texto se dividió)
statusenumJOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
skipReasonstringcuando es SKIPPED: no_prompt / ocr_failed / no_ocr_content / empty_ocr_content
contentstringrespuesta del modelo (texto tal cual)
modelstringla cadena de modelo de la solicitud
request / rawDatastringdepuración
errorstringerror de la etapa (p. ej. provider error (category=…, status=400))
durationstringduración, ns (un número como cadena: protojson devuelve int64 como cadena)
created / updatedstringRFC3339
schemaValidboolpresente solo si la tarea estableció responseSchema y el archivo no se dividió (chunkTotal = 1): si content cumple el esquema
schemaErrorsstring[]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):

CampoTipoDescripción
kintejecuciones solicitadas (2 / 3 / 5)
runsVotableintejecuciones que produjeron una respuesta JSON analizable y dentro de presupuesto, y que participaron en la votación
minAgreementnumberel 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»
incompletebooltrue cuando runsVotable < k, o la votación se degradó
disagreementsarraycampos 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)
disagreementsDroppedintentradas de discrepancia recortadas de la fila (solo el recuento; se conservan ≤100 entradas)

Campos de merged[] (presentes solo si la tarea definió merge.enabled):

CampoTipoDescripción
filestringURL del archivo al que corresponde esta respuesta fusionada; vacío ("") con scope=job
formatstringformato de content: json / xml / html / markdown / text
contentstringla respuesta fusionada para todo el documento (o el archivo)
schemaValidboolpresente solo si la tarea definió responseSchema: si el content fusionado cumple con ella
conflictsarraycampos 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)
dedupeRemovedTechnicalintregistros eliminados por la deduplicación automática de solapamiento en el límite
dedupeRemovedContentintregistros eliminados por sus campos dedupeBy
mergeIncompletebooltrue cuando al menos un fragmento no pudo fusionarse con seguridad y se añadió tal cual
incompleteChunksarraylos 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.