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»)
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.

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.

¿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, .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). 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

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.