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») |
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.
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.
¿Qué modelo usar? Compare inteligencia, precio por token y proveedores en la comparativa de LLM y elija el modelo adecuado.
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). 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 |
Nota. Todavía no se genera una especificación legible por máquina (OpenAPI/Swagger); esta referencia se mantiene a mano. OpenAPI está planificado como una tarea aparte.
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.