Referência da API
URL base: https://api.hotdoc.io. Os corpos de requisição e resposta são JSON; os nomes dos campos estão em camelCase. Exceções: POST /v1/jobs/upload (resposta) e o corpo da requisição do webhook — ambos usam snake_case.
Endpoints de processamento
| Método | Endpoint | Finalidade |
|---|---|---|
POST | /v1/jobs | criar um job |
GET | /v1/jobs/{id} | status do job e lista de arquivos (sem os resultados de reconhecimento) |
GET | /v1/jobs/{id}/result | resultado completo: texto reconhecido e respostas do modelo por arquivo |
GET | /v1/jobs | listar os jobs da conta (paginação: pageSize, pageToken, filtro statusEq) |
POST | /v1/jobs/upload | enviar um único arquivo (multipart/form-data) |
POST /v1/jobs — criar um job
Corpo da requisição:
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
sourceUrls | string[] | sim | URLs dos arquivos a processar |
prompts | string[] | não | instruções para o modelo (apenas o primeiro prompt é executado); sem prompts, o LLM não é chamado |
neural | object | sim | configuração do modelo (veja “Conectando um modelo”) |
ocr | object | sim | configuração do provedor de OCR (BYOK); sempre obrigatória (veja “Configuração de OCR”) |
extractionMode | enum | não | EXTRACTION_MODE_UNSPECIFIED | EXTRACTION_MODE_HYBRID | EXTRACTION_MODE_OCR_ALWAYS; se omitido / UNSPECIFIED, herda o padrão da conta (que por sua vez é HYBRID por padrão) |
responseSchema | string | não | JSON Schema opcional (string bruta) que restringe a resposta JSON do modelo; autocontida (apenas #/$defs internos), ≤128 KiB, profundidade ≤64, ≤10000 nós; um schema inválido é rejeitado com 400 na criação (veja “Saída estruturada” na documentação do Job API) |
merge | object | não | mesclagem opcional no servidor das respostas por chunk em um único resultado para o documento inteiro; desativada por padrão; veja “Opções de mesclagem” abaixo e “Mesclagem de resultados em chunks” na documentação do Job API |
consensus | object | não | votação de consenso opcional sobre k execuções por chunk; requer responseSchema (senão 400); desativada por padrão; veja “Opções de consenso” abaixo e “Execuções de consenso” na documentação do Job API |
title | string | não | nome arbitrário do job |
metadata | map<string,string> | não | pares chave-valor de string arbitrários |
webhookUrl | string | não | endpoint http/https absoluto a notificar na conclusão do job (veja “Webhooks”) |
webhookSecret | string | não | segredo HMAC opcional para assinar as requisições de webhook; aceito apenas como entrada, nunca retornado nas respostas |
idempotencyKey | string | não | chave do cliente para repetições seguras de criação; máximo de 255 caracteres (veja “Idempotência”) |
neural e ocr são sempre obrigatórios, mesmo quando prompts está vazio. Com prompts vazio, o modelo não é chamado: cada arquivo é marcado como JOB_LLM_STATUS_SKIPPED com skipReason=no_prompt e, em caso de OCR bem-sucedido, o job termina como JOB_STATUS_COMPLETE.
A resposta é o job criado com status JOB_STATUS_NEW; responseSchema é ecoado nesse objeto (vazio se não foi definido); consensus é ecoado como um objeto preenchido — mode: "CONSENSUS_MODE_UNSPECIFIED" quando não foi definido, nunca como um campo omitido ou nulo.
Configuração de OCR
ocr seleciona o backend de reconhecimento (OCR) por requisição (BYOK):
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
ocr.provider | enum | sim | um de NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI |
ocr.model | string | sim | o id do modelo do provedor, por exemplo mistral-ocr-latest (Mistral) ou o modelo de visão do provedor escolhido |
ocr.providerKey | string | sim | chave BYOK para o provedor de OCR; apenas como entrada, nunca retornada |
O Mistral é o backend de OCR típico (mistral-ocr-latest); os demais provedores executam
OCR via vision-chat. A chave é armazenada criptografada e excluída junto com o job.
Nota. O modo de extração é um campo de nível superior, extractionMode, e não faz parte de ocr: ele determina se o OCR roda sempre (EXTRACTION_MODE_OCR_ALWAYS) ou é aplicado por arquivo a critério do conversor (EXTRACTION_MODE_HYBRID, o padrão).
Qual modelo usar? Compare inteligência, preço por token e provedores no comparativo de LLM e escolha o modelo ideal.
Opções de mesclagem (merge)
merge é opcional e está desativada por padrão — defina merge.enabled para ativá-la. Os demais campos merge.* são ignorados enquanto enabled for false/omitido.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
merge.enabled | bool | não | ativa a mesclagem no servidor para este job |
merge.scope | enum | não | MERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (padrão) | MERGE_SCOPE_JOB; uma entrada merged[] por arquivo, ou uma para todos os arquivos |
merge.conflictPolicy | enum | não | MERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (padrão) | MERGE_CONFLICT_POLICY_MAJORITY |
merge.dedupeBy | string[] | não | nomes de campo JSON que identificam um registro único, para deduplicação de conteúdo entre arquivos; apenas JSON + responseSchema; no máximo 32 nomes de campo — mais são rejeitados com 400 na criação do job; vazio/omitido → apenas deduplicação técnica |
merge.format | enum | não | MERGE_FORMAT_UNSPECIFIED | MERGE_FORMAT_AUTO (padrão) | MERGE_FORMAT_JSON | MERGE_FORMAT_XML | MERGE_FORMAT_HTML | MERGE_FORMAT_MARKDOWN | MERGE_FORMAT_TEXT; sobrepõe a detecção automática de formato por arquivo |
Veja “Mesclagem de resultados em chunks” na documentação do Job API para o passo a passo completo (escopo, resolução de conflitos, os dois tipos de deduplicação).
Opções de consenso (consensus)
consensus é opcional e está desativado por padrão. Quando definido, consensus.mode precisa ser um valor reconhecido e o job precisa ter responseSchema definido — um job com consensus.mode definido mas sem responseSchema é rejeitado com 400 na criação.
| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
consensus.mode | enum | não | CONSENSUS_MODE_UNSPECIFIED (desativado, padrão) | CONSENSUS_MODE_TWO_RUNS | CONSENSUS_MODE_THREE_RUNS | CONSENSUS_MODE_FIVE_RUNS; um valor de string não reconhecido é silenciosamente ignorado pelo gateway — o job roda sem consenso, sem 400 |
Veja “Execuções de consenso” na documentação do Job API para o passo a passo completo (modos, como ler minAgreement/disagreements, ressalvas honestas).
Idempotência
Envie idempotencyKey para tornar POST /v1/jobs seguro para repetição. Uma repetição com a
mesma chave e parâmetros de requisição idênticos retorna o job original (nenhum
duplicado é criado). A mesma chave com parâmetros diferentes é rejeitada com
400 "idempotency key reused with different request parameters". A chave tem no
máximo 255 caracteres; gere um novo UUID por criação lógica.
GET /v1/jobs/{id}/result — resultado
Retorna { "result": { "job": …, "ocr": [...], "llm": [...] } }. Exemplo (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 na resposta não inclui apiKey; o objeto Job não tem campo ocr — a chave de OCR nunca é retornada. ocr[].content é o texto reconhecido no formato ocr[].outputFormat; o formato depende do tipo do arquivo: PDF e HTML → html, .xml → xml, .txt → plain, todo o resto (incluindo imagens e arquivos de office) → markdown. llm[].content é o texto da resposta do modelo exatamente como está (seu prompt determina a estrutura; não há validação no lado do servidor, a menos que o job tenha definido responseSchema — nesse caso, llm[].schemaValid/schemaErrors reportam a conformidade). No exemplo acima, campos vazios/zerados ("", {}, 0) são exibidos por completude — o protojson os omite, então eles podem estar ausentes em uma resposta real.
Campos de ocr[]:
| Campo | Tipo | Descrição |
|---|---|---|
jobId / file | string | id do job / URL do arquivo |
status | enum | JOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
content | string | texto reconhecido no formato outputFormat |
outputFormat | string | formato de content: markdown / html / xml / plain. Direciona o chunking consciente da estrutura no estágio de LLM |
model | string | método/mecanismo de OCR (por exemplo, pdf_fitz) |
request / rawData | string | debug: a requisição de OCR e a resposta bruta |
error | string | erro do estágio (vazio em caso de sucesso) |
duration | string | duração, ns (um número como string — o protojson retorna int64 como string) |
created / updated | string | RFC3339 |
Campos de llm[]:
| Campo | Tipo | Descrição |
|---|---|---|
jobId / file | string | id do job / URL do arquivo |
promptIndex | int | índice do prompt (atualmente sempre 0) |
chunkIndex / chunkTotal | int | número do chunk / total de chunks (se o texto foi dividido) |
status | enum | JOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED |
skipReason | string | quando SKIPPED: no_prompt / ocr_failed / no_ocr_content / empty_ocr_content |
content | string | resposta do modelo (texto como está) |
model | string | a string de modelo da requisição |
request / rawData | string | debug |
error | string | erro do estágio (por exemplo, provider error (category=…, status=400)) |
duration | string | duração, ns (um número como string — o protojson retorna int64 como string) |
created / updated | string | RFC3339 |
schemaValid | bool | presente apenas se o job definiu responseSchema e o arquivo não foi dividido (chunkTotal = 1): se content está em conformidade com o schema |
schemaErrors | string[] | violações de conformidade quando schemaValid é false (≤10, ≤512 bytes cada) |
Campos de llm[].consensus (presentes apenas se o job definiu consensus.mode; em jobs com consenso, linhas ignoradas/com falha ainda carregam consensus: null):
| Campo | Tipo | Descrição |
|---|---|---|
k | int | execuções solicitadas (2 / 3 / 5) |
runsVotable | int | execuções que produziram uma resposta JSON decodificável e dentro do orçamento, e que participaram da votação |
minAgreement | number | a menor concordância por campo da linha, como fração; 0 é um sentinela reservado — nenhuma votação ocorreu (runsVotable < 2) ou a votação degradou, não “0% de concordância” |
incomplete | bool | true quando runsVotable < k, ou a votação degradou |
disagreements | array | campos em que as execuções não concordaram totalmente, com a menor concordância primeiro; cada entrada: fieldPath (caminho por pontos; "" = raiz do documento; pode se repetir entre entradas quando um caminho tem tanto uma disputa de presença quanto uma disputa de elemento de array — renderize cada entrada separadamente, sem deduplicar por caminho), variants[] (value — JSON compacto, truncado de forma rune-safe em 512 bytes; runs — quantas execuções votáveis a produziram; included — venceu a votação / está presente na resposta acordada, independente de sua chave estar textualmente presente — um null vencedor tem included: true mesmo com a chave omitida), variantsDropped (variantes cortadas dessa entrada, apenas a contagem) |
disagreementsDropped | int | entradas de divergência cortadas da linha (apenas a contagem; ≤100 entradas mantidas) |
Campos de merged[] (presentes apenas se o job definiu merge.enabled):
| Campo | Tipo | Descrição |
|---|---|---|
file | string | URL do arquivo ao qual esta resposta mesclada corresponde; vazio ("") em scope=job |
format | string | formato de content: json / xml / html / markdown / text |
content | string | a resposta mesclada para o documento inteiro (ou o arquivo) |
schemaValid | bool | presente apenas se o job definiu responseSchema: se o content mesclado está em conformidade com ela |
conflicts | array | campos que divergiram entre chunks (no máx. 100 entradas, no máx. 10 valores concorrentes cada, no máx. 512 bytes por valor); cada entrada: field, values[] concorrentes, sources[] (referências de file + chunk) — values[i] corresponde a sources[i] (alinhados por índice) |
dedupeRemovedTechnical | int | registros removidos pela deduplicação automática de sobreposição de fronteira |
dedupeRemovedContent | int | registros removidos pelos seus campos dedupeBy |
mergeIncomplete | bool | true quando ao menos um chunk não pôde ser mesclado com segurança e foi anexado como está |
incompleteChunks | array | os chunks que não puderam ser mesclados; cada entrada: file, índice de chunk |
Nota. Uma descrição legível por máquina da API de Jobs no formato OpenAPI 3.0.3 está publicada em
/openapi.yaml. Ela é gerada a partir das definições protobuf do serviço, portanto não diverge da API; esta página continua sendo a referência em prosa. Você pode passar o arquivo para qualquer gerador de clientes compatível com OpenAPI.
POST /v1/jobs/upload — enviar um arquivo
Um endpoint HTTP autônomo (não é grpc-gateway). Aceita um arquivo via multipart/form-data.
Resposta (snake_case — uma exceção ao camelCase geral):
{"url": "https://…/files/…/document.pdf", "name": "document.pdf", "size_bytes": 204800}
Use a url retornada em sourceUrls ao criar um job.
O corpo de erro deste endpoint é {"code": <int>, "message": "…"}, sem o array details. O limite de tamanho do arquivo é definido pela config (grpc.maxRecvMsgBytes; 20 MiB no deployment atual), e não por um padrão de código.
Versionamento
O caminho /v1 é estável. Mudanças incompatíveis com versões anteriores são lançadas sob um novo caminho (/v2). Mudanças aditivas (novos campos opcionais e endpoints) não quebram a compatibilidade e são anunciadas no Changelog.
Recursos para desenvolvedores
- Especificação OpenAPIOpenAPI 3.0.3, gerada a partir das definições do serviço. Serve para qualquer gerador de clientes.