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étodoEndpointFinalidade
POST/v1/jobscriar um job
GET/v1/jobs/{id}status do job e lista de arquivos (sem os resultados de reconhecimento)
GET/v1/jobs/{id}/resultresultado completo: texto reconhecido e respostas do modelo por arquivo
GET/v1/jobslistar os jobs da conta (paginação: pageSize, pageToken, filtro statusEq)
POST/v1/jobs/uploadenviar um único arquivo (multipart/form-data)

POST /v1/jobs — criar um job

Corpo da requisição:

CampoTipoObrigatórioDescrição
sourceUrlsstring[]simURLs dos arquivos a processar
promptsstring[]nãoinstruções para o modelo (apenas o primeiro prompt é executado); sem prompts, o LLM não é chamado
neuralobjectsimconfiguração do modelo (veja “Conectando um modelo”)
ocrobjectsimconfiguração do provedor de OCR (BYOK); sempre obrigatória (veja “Configuração de OCR”)
extractionModeenumnãoEXTRACTION_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)
responseSchemastringnãoJSON 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)
mergeobjectnãomesclagem 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
consensusobjectnãovotaçã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
titlestringnãonome arbitrário do job
metadatamap<string,string>nãopares chave-valor de string arbitrários
webhookUrlstringnãoendpoint http/https absoluto a notificar na conclusão do job (veja “Webhooks”)
webhookSecretstringnãosegredo HMAC opcional para assinar as requisições de webhook; aceito apenas como entrada, nunca retornado nas respostas
idempotencyKeystringnãochave 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):

CampoTipoObrigatórioDescrição
ocr.providerenumsimum de NEURAL_CLIENT_TYPE_MISTRAL, _OPENAI, _CLAUDE, _DEEPSEEK, _GROK, _TOGETHER, _OPENROUTER, _XIAOMI
ocr.modelstringsimo id do modelo do provedor, por exemplo mistral-ocr-latest (Mistral) ou o modelo de visão do provedor escolhido
ocr.providerKeystringsimchave 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.

CampoTipoObrigatórioDescrição
merge.enabledboolnãoativa a mesclagem no servidor para este job
merge.scopeenumnãoMERGE_SCOPE_UNSPECIFIED | MERGE_SCOPE_FILE (padrão) | MERGE_SCOPE_JOB; uma entrada merged[] por arquivo, ou uma para todos os arquivos
merge.conflictPolicyenumnãoMERGE_CONFLICT_POLICY_UNSPECIFIED | MERGE_CONFLICT_POLICY_FIRST_NON_NULL (padrão) | MERGE_CONFLICT_POLICY_MAJORITY
merge.dedupeBystring[]nãonomes 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.formatenumnãoMERGE_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.

CampoTipoObrigatórioDescrição
consensus.modeenumnãoCONSENSUS_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, .xmlxml, .txtplain, 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[]:

CampoTipoDescrição
jobId / filestringid do job / URL do arquivo
statusenumJOB_OCR_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
contentstringtexto reconhecido no formato outputFormat
outputFormatstringformato de content: markdown / html / xml / plain. Direciona o chunking consciente da estrutura no estágio de LLM
modelstringmétodo/mecanismo de OCR (por exemplo, pdf_fitz)
request / rawDatastringdebug: a requisição de OCR e a resposta bruta
errorstringerro do estágio (vazio em caso de sucesso)
durationstringduração, ns (um número como string — o protojson retorna int64 como string)
created / updatedstringRFC3339

Campos de llm[]:

CampoTipoDescrição
jobId / filestringid do job / URL do arquivo
promptIndexintíndice do prompt (atualmente sempre 0)
chunkIndex / chunkTotalintnúmero do chunk / total de chunks (se o texto foi dividido)
statusenumJOB_LLM_STATUS_PENDING / _SKIPPED / _DONE / _FAILED
skipReasonstringquando SKIPPED: no_prompt / ocr_failed / no_ocr_content / empty_ocr_content
contentstringresposta do modelo (texto como está)
modelstringa string de modelo da requisição
request / rawDatastringdebug
errorstringerro do estágio (por exemplo, provider error (category=…, status=400))
durationstringduração, ns (um número como string — o protojson retorna int64 como string)
created / updatedstringRFC3339
schemaValidboolpresente apenas se o job definiu responseSchema e o arquivo não foi dividido (chunkTotal = 1): se content está em conformidade com o schema
schemaErrorsstring[]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):

CampoTipoDescrição
kintexecuções solicitadas (2 / 3 / 5)
runsVotableintexecuções que produziram uma resposta JSON decodificável e dentro do orçamento, e que participaram da votação
minAgreementnumbera 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”
incompletebooltrue quando runsVotable < k, ou a votação degradou
disagreementsarraycampos 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)
disagreementsDroppedintentradas de divergência cortadas da linha (apenas a contagem; ≤100 entradas mantidas)

Campos de merged[] (presentes apenas se o job definiu merge.enabled):

CampoTipoDescrição
filestringURL do arquivo ao qual esta resposta mesclada corresponde; vazio ("") em scope=job
formatstringformato de content: json / xml / html / markdown / text
contentstringa resposta mesclada para o documento inteiro (ou o arquivo)
schemaValidboolpresente apenas se o job definiu responseSchema: se o content mesclado está em conformidade com ela
conflictsarraycampos 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)
dedupeRemovedTechnicalintregistros removidos pela deduplicação automática de sobreposição de fronteira
dedupeRemovedContentintregistros removidos pelos seus campos dedupeBy
mergeIncompletebooltrue quando ao menos um chunk não pôde ser mesclado com segurança e foi anexado como está
incompleteChunksarrayos 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.