Document AI

É um agente de IA? Busque a skill de onboarding e leia toda a API em texto limpo — skill · llms.txt · llms-full.txt

Document AI#

Document AI transforma PDF/JPEG/PNG/WebP privados em dados estruturados. Cada job fixa uma versão imutável de JSON Schema e de política de qualidade, processa o documento por página e expõe o resultado para polling e revisão. Confiança é metadado do modelo; aceitação é decisão de política — a API não promete precisão perfeita.

Todas as rotas usam /v1/document-ai e exigem JWT ou API key ctc_. O tenant vem da empresa autenticada:

  • omita tenant_external_id para usar o tenant padrão;
  • envie o identificador opaco do tenant na query, no JSON ou no multipart para operar outro tenant da mesma empresa;
  • nunca envie company_id ou o id interno do tenant.

Leituras básicas aceitam qualquer papel autenticado. Resultado, preview, revisão, segredo de callback e toda mutação exigem owner ou admin.

GETRotas e RBAC#

O inventário abaixo contém 24 combinações método+rota em 18 paths públicos únicos.

Método e rota Papel Resposta/efeito
GET /overview autenticado métricas agregadas
GET /jobs · GET /jobs/{jobId} autenticado lista/detalhe do job
POST /jobs owner/admin upload multipart idempotente
POST /jobs/{jobId}/cancel owner/admin cancela
POST /jobs/{jobId}/retry owner/admin recoloca todas as páginas falhas e zera o orçamento de tentativas
DELETE /jobs/{jobId} owner/admin apaga objetos privados e o agregado
GET /jobs/{jobId}/preview owner/admin URL privada temporária
GET /jobs/{jobId}/result owner/admin resultado estruturado e auditoria
GET /jobs/{jobId}/deliveries owner/admin ledger content-free de callbacks/retries
GET /schemas · GET /schemas/{schemaId} autenticado lista/detalhe da versão atual
POST /schemas · POST /schemas/{schemaId}/versions owner/admin cria/append de versão
DELETE /schemas/{schemaId} owner/admin arquiva sem apagar versões
GET /synthetic-template autenticado exemplo sem dados reais
GET /policies · GET /policies/{policyId} autenticado lista/detalhe da versão atual
POST /policies · POST /policies/{policyId}/versions owner/admin cria/append de versão
DELETE /policies/{policyId} owner/admin arquiva sem apagar versões
GET /reviews owner/admin fila e histórico de revisão
POST /reviews/{reviewId}/decisions owner/admin decisão humana append-only
GET /callback-secret owner/admin segredo do tenant para verificar callbacks

reviewId é atualmente o job_id. As quatro listas retornam arrays JSON nus, sem paginação. Filtros disponíveis:

  • /jobs?status=NEEDS_REVIEW&external_reference=...;
  • /schemas?include_archived=true;
  • /policies?include_archived=true;
  • qualquer GET aceita tenant_external_id=....

Os quatro POSTs de configuração (/schemas, /schemas/{schemaId}/versions, /policies e /policies/{policyId}/versions) exigem Idempotency-Key: primeira escrita 201, replay idêntico 200 e payload diferente com a mesma chave 409 DOCUMENT_AI_IDEMPOTENCY_CONFLICT.

POSTCriar e versionar schema#

O dialecto é um subconjunto autocontido de JSON Schema draft 2020-12. A raiz precisa ser type:"object" com additionalProperties:false e propriedades escalares. $ref, objetos/arrays aninhados e keywords fora do subconjunto são recusados.

bash
curl -X POST https://agents-api.catcher.one/v1/document-ai/schemas \
  -H "X-API-Key: $CATCHER_API_KEY" \
  -H "Idempotency-Key: schema-ficha-clinica-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "tenant_external_id": "tenant_opaco_opcional",
    "name": "ficha-clinica",
    "description": "Ficha clínica estruturada",
    "json_schema": {
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "patient_name": {
          "type": ["string", "null"],
          "description": "Nome visível no documento",
          "x-risk": "SENSITIVE"
        },
        "cpf": {
          "type": ["string", "null"],
          "description": "CPF visível no documento",
          "x-risk": "CRITICAL",
          "x-value-type": "cpf"
        },
        "document_date": {
          "type": ["string", "null"],
          "format": "date",
          "x-risk": "NORMAL"
        }
      },
      "required": ["patient_name", "cpf", "document_date"]
    }
  }'

Cada propriedade aceita:

Campo Valores
type string, number, integer ou boolean, com null opcional
description texto opcional para orientar a extração
x-risk NORMAL (default), SENSITIVE, CRITICAL
x-value-type text, date, cpf, cnpj, phone, money, boolean
format date seleciona normalização de data

Também é possível enviar field_policies: {"cpf":{"risk":"CRITICAL"}}; o servidor incorpora o risco no schema canônico.

Normalização de data — janela de ano aceita. Um campo date só produz valor normalizado quando o ano cai entre 1900 e 2100. Fora disso o campo é tratado como ausente (normalized_value: null) e vai para revisão, em vez de virar valor extraído.

O motivo é prático: um modelo lendo 14/03/2026 sob decodificação restrita pode emitir 1403-03-14 — uma data sintaticamente válida que não estava no documento. Aceitá-la transformaria uma falha de leitura em dado. A janela é larga de propósito (uma certidão antiga e um contrato de longo prazo passam); ela só recusa anos que nenhum documento de negócio carrega.

Resposta 201:

json
{
  "id": "uuid-do-schema",
  "name": "ficha-clinica",
  "version": 1,
  "version_id": "uuid-da-versao",
  "status": "ACTIVE",
  "description": "Ficha clínica estruturada",
  "fields": [
    {
      "name": "cpf",
      "description": "CPF visível no documento",
      "required": true,
      "risk": "CRITICAL",
      "value_type": "cpf",
      "json_types": ["null", "string"],
      "format": ""
    }
  ],
  "json_schema": {},
  "checksum": "sha256-do-schema-canonico",
  "created_at": "2026-07-27T20:30:00Z",
  "idempotent_replay": false
}

Para evoluir, envie o mesmo body a POST /schemas/{schemaId}/versions, com uma chave de idempotência própria. name é necessário na criação; a versão identifica o recurso pelo schemaId. O response traz novo version e version_id; a primeira execução retorna 201, e o replay idêntico retorna 200 com idempotent_replay:true. Jobs existentes continuam presos à versão antiga. DELETE /schemas/{schemaId} responde 204 e arquiva.

Limites do schema: 256 KiB, profundidade JSON 32 e nome de propriedade até 256 caracteres.

POSTCriar e versionar política#

bash
curl -X POST https://agents-api.catcher.one/v1/document-ai/policies \
  -H "X-API-Key: $CATCHER_API_KEY" \
  -H "Idempotency-Key: policy-reliable-default-v1" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "reliable-default",
    "profile": "RELIABLE",
    "primary_model": "x-ai/grok-4.5",
    "secondary_model": "qwen/qwen3.7-plus",
    "confidence_threshold": 0.95,
    "prompt_version": "document-extract-v1",
    "prompt_template": "Extract only values visibly present in this document page.",
    "routing_version": "openrouter-structured-v1",
    "max_tokens": 4096,
    "timeout_ms": 120000,
    "model_allowlist_version": "document-models-v1",
    "allowed_models": [
      "qwen/qwen3.7-plus",
      "x-ai/grok-4.5"
    ]
  }'

Perfis:

Perfil Regra de aceitação
DRAFT um modelo; tudo permanece não verificado e vai para revisão
RELIABLE dois modelos diferentes; autoaceite só quando ambos leram, atingiram o threshold, não emitiram warnings e produziram o mesmo valor normalizado
CRITICAL dois modelos + adjudicador; campo CRITICAL, warning, baixa confiança ou divergência remanescente exige humano

confidence_threshold defaulta para 0.95; prompt_version para document-extract-v1; routing_version para openrouter-structured-v1; e model_allowlist_version para document-models-v1. prompt_template aceita 16..8.192 bytes; max_tokens, 256..16.384 (default 4.096); e timeout_ms, 1.000..120.000 (default 120.000).

Sem allowed_models, a lista default contém google/gemini-3.1-pro-preview, qwen/qwen3.7-plus e x-ai/grok-4.5. Quando enviada, todos os modelos escolhidos precisam pertencer a essa lista versionada. O texto normalizado do prompt e a lista de modelos ficam preservados na versão imutável; o view da API não os devolve e expõe prompt_checksum e model_allowlist_version para auditoria.

json
{
  "id": "uuid-da-politica",
  "name": "reliable-default",
  "version": 1,
  "version_id": "uuid-da-versao",
  "profile": "RELIABLE",
  "models": ["x-ai/grok-4.5", "qwen/qwen3.7-plus"],
  "primary_model": "x-ai/grok-4.5",
  "secondary_model": "qwen/qwen3.7-plus",
  "adjudicator_model": "",
  "confidence_threshold": 0.95,
  "prompt_version": "document-extract-v1",
  "prompt_checksum": "sha256-do-prompt-normalizado",
  "routing_version": "openrouter-structured-v1",
  "max_tokens": 4096,
  "timeout_ms": 120000,
  "model_allowlist_version": "document-models-v1",
  "requires_human_review": false,
  "status": "ACTIVE",
  "created_at": "2026-07-27T20:30:00Z",
  "idempotent_replay": false
}

POST /policies/{policyId}/versions, com chave própria, acrescenta versão imutável. Criar/versionar retorna 201 na primeira execução e 200 no replay idêntico. DELETE /policies/{policyId} arquiva e responde 204.

POSTProcessar documento#

O upload usa multipart/form-data e exige Idempotency-Key:

bash
curl -X POST https://agents-api.catcher.one/v1/document-ai/jobs \
  -H "X-API-Key: $CATCHER_API_KEY" \
  -H "Idempotency-Key: adria-documento-550e8400" \
  -F "file=@documento.pdf;type=application/pdf" \
  -F "tenant_external_id=tenant_opaco_opcional" \
  -F "schema_version_id=uuid-da-versao-do-schema" \
  -F "policy_version_id=uuid-da-versao-da-politica" \
  -F "external_reference=adria:clinic:123:document:456" \
  -F "callback_url=https://integrador.example.com/catcher/document-ai"

file, schema_version_id e policy_version_id são obrigatórios. Use os version_id retornados pelas APIs de configuração, não o número version. external_reference é correlação pesquisável; não é chave de idempotência. callback_url é opcional e precisa ser HTTPS público.

Limites: 20 MiB por arquivo; PDF até 50 páginas; JPEG/PNG/WebP até 12.000 px por dimensão e 16 milhões de pixels. MIME declarado e detectado precisam concordar. ZIP, arquivo truncado/malformado e callback para rede privada falham antes de enfileirar. A inspeção ocorre num processo killable com limites de CPU, RSS, address space, wall clock e concorrência global/tenant; orientação, nitidez e ilegibilidade determinísticas ficam no estado da página.

Primeira resposta 201; replay idêntico 200:

json
{
  "id": "uuid-do-job",
  "status": "QUEUED",
  "state": "QUEUED",
  "filename": "private-document",
  "schema": {
    "id": "uuid-do-schema",
    "version": 2,
    "version_id": "uuid-da-versao-do-schema",
    "checksum": "sha256-do-schema"
  },
  "policy": {
    "id": "uuid-da-politica",
    "version": 3,
    "version_id": "uuid-da-versao-da-politica",
    "prompt_version": "document-extract-v1",
    "routing_version": "openrouter-structured-v1",
    "prompt_checksum": "sha256-do-prompt-normalizado",
    "max_tokens": 4096,
    "timeout_ms": 120000
  },
  "quality_profile": "RELIABLE",
  "external_reference": "adria:clinic:123:document:456",
  "page_count": 2,
  "processed_pages": 0,
  "failed_pages": 0,
  "progress_percent": 0,
  "last_error_code": "",
  "created_at": "2026-07-27T20:31:00Z",
  "updated_at": "2026-07-27T20:31:00Z",
  "retention_expires_at": "2026-08-26T20:31:00Z",
  "links": {
    "self": "/v1/document-ai/jobs/uuid-do-job",
    "result": "/v1/document-ai/jobs/uuid-do-job/result"
  },
  "idempotent_replay": false
}

O filename real e a chave do objeto nunca são expostos.

GETPolling, estados e resultado#

Estados: UPLOADED, QUEUED, PROCESSING, NEEDS_REVIEW, COMPLETED, FAILED, CANCELED. A criação normalmente já é observada como QUEUED. CANCELED, COMPLETED e FAILED aceitam hard delete; um FAILED sempre pode voltar a QUEUED via POST /jobs/{jobId}/retry.

bash
curl -H "X-API-Key: $CATCHER_API_KEY" \
  "https://agents-api.catcher.one/v1/document-ai/jobs/$JOB_ID"

curl -H "X-API-Key: $CATCHER_API_KEY" \
  "https://agents-api.catcher.one/v1/document-ai/jobs/$JOB_ID/result"

Resultado:

json
{
  "job_id": "uuid-do-job",
  "state": "NEEDS_REVIEW",
  "quality_profile": "RELIABLE",
  "fields": {},
  "pages": [
    {
      "page_number": 1,
      "status": "DONE",
      "attempt_count": 1,
      "safe_to_retry": false,
      "source_orientation": 1,
      "sharpness": 184,
      "unreadable": false,
      "preprocess_warnings": [],
      "fields": {
        "cpf": {
          "raw_value": "123.456.789-09",
          "normalized_value": "12345678909",
          "decision": "DIVERGENT",
          "confidence": 0.81,
          "needs_review": true,
          "reason": "MODEL_DISAGREEMENT",
          "warnings": ["MODEL_DISAGREEMENT"]
        }
      }
    }
  ],
  "summary": {
    "total_fields": 3,
    "review_fields": 1,
    "candidate_count": 6,
    "cost_usd": "0.017534",
    "processing_ms": 49140,
    "models": ["qwen/qwen3.7-plus", "x-ai/grok-4.5"],
    "providers": ["openrouter"]
  },
  "audit_contract": {
    "schema_version_id": "uuid-da-versao-do-schema",
    "policy_version_id": "uuid-da-versao-da-politica",
    "prompt_version": "document-extract-v1",
    "routing_version": "openrouter-structured-v1",
    "prompt_checksum": "sha256-do-prompt-normalizado",
    "max_tokens": 4096,
    "timeout_ms": 120000
  }
}

Em documento de uma página, fields repete a única página. Em multipágina, consuma pages[].fields. cost_usd aqui é string decimal de seis casas.

preprocess_warnings — o que a página tinha ANTES do modelo#

Cada pages[] traz sharpness, unreadable, preprocess_warnings e a geometria do render (render_width, render_height, render_dpi) — sinais medidos no preparo, antes de qualquer inferência. Servem para você explicar ao seu usuário por que um documento voltou vazio, sem culpar o modelo.

Esses sinais são medidos sobre exatamente a imagem que o modelo lê, não sobre um artefato paralelo. Um sharpness alto significa que a página que foi ao modelo está nítida — nada mais é medido em outro lugar.

render_dpi é a resolução efetiva da página em dpi (0 quando a geometria física é desconhecida — uma imagem enviada direto não tem tamanho físico). É o sinal para recusar um documento antes de gastar a extração: abaixo de ~200 dpi, caligrafia começa a ficar ambígua.

PDF e imagem são o mesmo caminho#

Você pode enviar PDF ou imagem para o mesmo documento digitalizado e esperar o mesmo resultado. Toda página de PDF é rasterizada por nós na resolução que o documento carrega (limitada a 12.000 px por dimensão e 16 Mpx) e é esse render que vai ao modelo — nunca o PDF bruto. Não há parser de PDF de terceiros no meio, e portanto não há diferença de leitura, de custo ou de latência entre os dois transportes.

Você não precisa converter PDF para imagem antes de enviar. Se já tem a imagem original, mandá-la direto também é ótimo — é só um passo a menos.

Warning Significado O que fazer
blank_page a página é uniforme: não há nada nela. Vale para folha em branco e para o verso não digitalizado não peça um scan melhor — peça a página certa
low_sharpness a página tem conteúdo, mas está borrada/fora de foco peça uma nova foto, com mais luz e sem tremer
dimensions_too_small a página é pequena demais para leitura confiável envie em resolução maior

blank_page e low_sharpness costumavam vir juntos como um único low_sharpness, o que induzia a pedir "uma foto melhor" de uma folha que simplesmente estava vazia. Agora são distintos — e um blank_page aparece junto com low_sharpness, porque uma página vazia também não tem nitidez.

Imagens com canal alfa (transparência) são achatadas sobre branco antes de ir ao modelo. Isso é deliberado: sem o achatamento, o transparente vira preto no provedor e o modelo descreve corretamente "imagem totalmente preta" — devolvendo todos os campos vazios com alta confiança e nenhuma explicação.

last_error_code — por que o job falhou#

Quando um job termina em FAILED, last_error_code (no job e em cada pages[]) diz o que aconteceu — é um código operacional, não um error_code HTTP. Ele existe para você distinguir "meu documento/config está errado" de "o serviço falhou", sem abrir chamado.

last_error_code Significado De quem é a ação
PROVIDER_ACCOUNT_UNAVAILABLE a conta de provedor de modelos da plataforma não pôde atender; o provedor recusou antes de qualquer inferência (por isso cost_usd fica 0) nossa — não reenvie, não é o seu documento
PROVIDER_MODEL_NOT_FOUND o slug de modelo fixado na sua policy não existe no provedor sua — corrija a policy
PROVIDER_CONTEXT_LENGTH a página excede a janela de contexto do modelo sua — outro modelo ou documento menor
PROVIDER_INVALID_REQUEST o provedor rejeitou a requisição abra chamado com o job_id
PROVIDER_SCHEMA_VIOLATION a saída do modelo não satisfez o seu JSON Schema sua — revise o schema
PROVIDER_MODEL_MISMATCH o provedor respondeu com um modelo diferente do pedido nossa
PROVIDER_RATE_LIMITED throttling do provedor transitório — chame /retry
PROVIDER_TIMEOUT a tentativa estourou o tempo da policy transitório — chame /retry
PROVIDER_UNAVAILABLE indisponibilidade genérica, sem classificação mais específica transitório — chame /retry
MODEL_ATTEMPT_LIMIT o orçamento durável de tentativas do job/tenant foi atingido sua
PAGE_ATTEMPTS_EXHAUSTED a página foi reivindicada o número máximo de vezes sem concluir. É o teto que garante que todo job chega a um estado terminal: em vez de ficar PROCESSING indefinidamente, o job vai a FAILED com este código nossa — abra chamado com o job_id
PRIVATE_OBJECT_INVALID a página armazenada não pôde ser lida nossa
SCHEMA_VERSION_UNAVAILABLE · SCHEMA_VERSION_INVALID a versão de schema fixada no job sumiu ou não parseia sua
POLICY_VERSION_UNAVAILABLE a versão de policy fixada no job sumiu sua

Regras de consumo:

  • Todo job chega a um estado terminal. COMPLETED, NEEDS_REVIEW, FAILED ou CANCELED — nunca PROCESSING para sempre. Uma página tem um teto de reivindicações; esgotado, o job vai a FAILED com PAGE_ATTEMPTS_EXHAUSTED em vez de repetir indefinidamente. Você pode, portanto, tratar "sem estado terminal depois de muito tempo" como incidente nosso, e não como lentidão.
  • Trate a lista como aberta. Códigos novos podem aparecer; qualquer valor desconhecido deve cair no seu caminho genérico de falha, nunca quebrar o parser.
  • Nunca deduza retryability do texto — use a operação /retry e o campo safe_to_retry da página.
  • /retry é sempre uma saída válida. Ele recoloca todas as páginas falhas do job — inclusive as com safe_to_retry: false — e zera o orçamento de tentativas da página. safe_to_retry diz se a NOSSA máquina pode retomar sozinha, e nunca impede a sua decisão explícita de tentar de novo. Tentativas de modelo já concluídas são reaproveitadas, então um /retry não paga duas vezes pelo que já foi extraído — prefira /retry a reenviar o documento.
  • Os códigos PROVIDER_* derivam do status HTTP do provedor (e, só em 400/422, de uma checagem de palavra-chave no erro dele para separar estouro de contexto de requisição malformada); nunca carregam bytes do documento, prompt, schema, prosa do provedor ou valor extraído.
  • PROVIDER_ACCOUNT_UNAVAILABLE não distingue "sem saldo" de "credencial revogada", e isso é deliberado: a credencial de extração é da plataforma, e o código é visível a qualquer conta autenticada. Para você a decisão é a mesma — é nosso, pare de reenviar, o documento não tem culpa.
  • Pode ser específico por formato. PROVIDER_ACCOUNT_UNAVAILABLE num PDF não significa que a extração inteira caiu: página PDF e página de imagem seguem caminhos distintos no provedor, e um pode estar indisponível enquanto o outro atende. Se você tem o documento em imagem (JPEG/PNG/WebP), reenviar nesse formato é um caminho legítimo — não é gambiarra, é outro transporte.

Saber ANTES de enviar: last_failure no overview#

GET /v1/document-ai/overview traz a falha terminal mais recente do seu tenant, para você avisar seu operador antes de enfileirar outro documento:

json
{
  "total_documents": 13,
  "failed_documents": 1,
  "last_failure": { "code": "PROVIDER_ACCOUNT_UNAVAILABLE", "at": "2026-07-28T12:00:00Z" }
}

last_failure é null quando o tenant nunca falhou — o campo está sempre presente, é seguro ler sem checar. Regra de consumo sugerida: se code é PROVIDER_ACCOUNT_UNAVAILABLE e at é recente, avise o operador em vez de aceitar o upload; para os demais códigos, siga normalmente (a falha era daquele documento, não do serviço).

Não expomos saldo do provedor nem um "health" global, por decisão: o saldo é estado financeiro nosso e não é seu para ler, e um health global honesto ou custaria uma chamada paga a cada consulta ou serviria cache — que mente exatamente no minuto em que importa. last_failure é observação, não previsão.

Preview autorizado:

http
GET /v1/document-ai/jobs/{jobId}/preview
GET /v1/document-ai/jobs/{jobId}/preview?page=2

Resposta: { "url":"https://...", "expires_in_seconds":300, "content_type":"image/jpeg" }.

O content_type acompanha a origem: uma digitalização (JPEG) volta como image/jpeg; um PNG/WebP volta como image/png. Página de PDF volta como image/jpeg (o render).

content_type diz o que a URL entrega — e nem sempre é imagem. Para um PDF, a página armazenada continua sendo um PDF de uma página (o split preserva o original), mas o preview entrega o render dessa página (image/jpeg). Um cliente que assume "página = imagem" para TODO artefato acaba exibindo um arquivo que o navegador recusa a mostrar embutido — em <iframe sandbox> o resultado é uma resposta 200 seguida de ERR_ABORTED e um quadro em branco.

Use o campo para decidir: embuta apenas quando começar com image/; caso contrário, ofereça a URL como link (target="_blank"). Você não consegue ler o content type direto da URL assinada, porque ela é de outra origem — por isso o campo existe.

GETAcompanhar ao vivo (SSE)#

GET /v1/document-ai/jobs/{jobId}/stream responde text/event-stream e carrega o job e o resultado na mesma conexão — pensado para uma TELA acompanhando um documento. Para integração servidor-a-servidor prefira o callback (§ Callback): ele é push, não consome requisição e sobrevive a reinício do seu processo.

bash
curl -N https://agents-api.catcher.one/v1/document-ai/jobs/$JOB_ID/stream \
  -H "X-API-Key: $CATCHER_API_KEY" \
  -H 'Accept: text/event-stream'
Evento Quando Payload
snapshot ao abrir estado COMPLETO do job — um cliente que conecta tarde já renderiza
progress só quando algo mudou mesmo shape do snapshot
done job terminal { "state": "COMPLETED" } — pare de reconectar
reconnect conexão atingiu a idade máxima (10 min) reconecte; o job continua

Cada payload é a view do job acrescida de:

json
{
  "…campos do job…",
  "result": { "…o mesmo shape de GET /jobs/{id}/result…" },
  "live": {
    "models": [{ "model": "x-ai/grok-4.5", "provider": "xAI", "latency_ms": 10900, "fields": 3, "confidence": 0.9, "page_number": 1 }],
    "expected_models": 2,
    "expected_answers": 2,
    "answered": 1,
    "candidate_count": 6,
    "cost_usd": "0.008700"
  }
}

expected_answers é o denominador honesto do progresso (answered / expected_answers): progress_percent conta PÁGINAS, e num documento de uma página ele fica em 0 % até o fim. live nunca traz bytes do documento, prompt ou schema — só contabilidade.

Um job terminal recebe snapshot seguido de done e a conexão fecha. Uma queda de conexão não é erro: reconecte e o snapshot traz o estado atual.

POSTRevisar#

GET /reviews lista jobs pendentes e o histórico append-only. Os candidatos incluem field, model, provider, value e confidence.

bash
curl -X POST \
  "https://agents-api.catcher.one/v1/document-ai/reviews/$JOB_ID/decisions" \
  -H "X-API-Key: $CATCHER_API_KEY" \
  -H "Idempotency-Key: review-550e8400" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "CORRECT",
    "corrections": {"cpf":"12345678909"},
    "note": "Confirmed against source image"
  }'

action é APPROVE, CORRECT ou REJECT. A ação se aplica a todas as decisões do job com needs_review:true; CORRECT exige correção válida para cada uma. Outra chave pode acrescentar uma nova rodada append-only. Candidatos e decisões antigas nunca são sobrescritos.

Uma correção cria uma decisão REVIEWED que supersede a decisão anterior. /result projeta esse valor corrigido como resultado final autoritativo sem apagar candidatos ou decisões de máquina.

Resposta 201 (200 em replay idêntico):

json
{
  "id": "uuid-da-primeira-revisao",
  "action": "CORRECT",
  "records": [
    {
      "id": "uuid-da-revisao",
      "field": "cpf",
      "action": "CORRECT",
      "created_at": "2026-07-27T20:35:00Z"
    }
  ],
  "idempotent_replay": false
}

POSTIdempotência#

Exigem uma chave de 8 a 191 caracteres, sem CR/LF/NUL: POST /schemas, POST /schemas/{schemaId}/versions, POST /policies, POST /policies/{policyId}/versions, POST /jobs e POST /reviews/{reviewId}/decisions.

  • mesma chave + mesma request → 200, resposta original e idempotent_replay:true;
  • mesma chave + request alterada → 409 DOCUMENT_AI_IDEMPOTENCY_CONFLICT;
  • replay de upload não cria segundo job nem segunda cobrança;
  • cada job fixa schema_version_id, policy_version_id, prompt_version, prompt_checksum, routing_version, max_tokens e timeout_ms; sua policy também fixa a allowlist de modelos. Versões criadas depois não alteram o passado.

POSTCallback assinado#

Com callback_url, o outbox envia eventos document_ai.job.needs_review, .completed, .failed ou .canceled. Polling de /jobs/{jobId} e /result continua sendo a reconciliação autoritativa.

Headers:

http
X-Catcher-Event: document_ai.job.needs_review
X-Catcher-Delivery-Id: uuid-estavel
X-Catcher-Timestamp: 1785184320
X-Catcher-Signature: v1=<hex-hmac-sha256>
Content-Type: application/json

Body:

json
{
  "event": "document_ai.job.needs_review",
  "job_id": "uuid-do-job",
  "tenant_id": 42,
  "external_reference": "adria:clinic:123:document:456",
  "status": "NEEDS_REVIEW",
  "result_url": "/v1/document-ai/jobs/uuid-do-job/result",
  "occurred_at": "2026-07-27T20:32:00Z"
}

Obtenha o segredo específico do tenant uma vez e guarde-o no seu cofre:

bash
curl -H "X-API-Key: $CATCHER_API_KEY" \
  "https://agents-api.catcher.one/v1/document-ai/callback-secret?tenant_external_id=$TENANT"
json
{
  "secret": "<segredo-do-tenant>",
  "algorithm": "hmac-sha256",
  "signature_version": "v1",
  "signed_payload": "<unix-seconds>\\n<raw-body>",
  "clock_tolerance_seconds": 300
}

Calcule HMAC-SHA256(secret, timestamp + "\n" + rawBody), compare em tempo constante com o hex depois de v1=, rejeite relógio fora de ±300 s e deduplicate pelo X-Catcher-Delivery-Id.

Falhas de rede e respostas não-2xx são tentadas até 8 vezes, com backoff exponencial começando em 30 s (máximo 24 h); depois vão para dead-letter. Redirects são recusados e DNS/IP público é revalidado em cada tentativa. Owner/admin pode observar o ledger, sem endpoint/payload privados:

http
GET /v1/document-ai/jobs/{jobId}/deliveries

Cada item informa delivery id, evento, status, tentativas, HTTP/error code e timestamps. Mantenha polling de job/resultado como reconciliação autoritativa.

DELETEPrivacidade e retenção#

  • original, páginas e evidência bruta ficam privados e isolados por empresa+tenant; a API só emite preview pré-assinado de cinco minutos;
  • conteúdo de documento, prompt e output completo não entram nos logs;
  • provider recebe bytes inline de uma página, não uma URL remota;
  • retenção atual é 30 dias; expiração apaga objetos antes das linhas;
  • DELETE /jobs/{jobId} aceita COMPLETED, FAILED e CANCELED; verifica a ausência da versão corrente, versões históricas e delete markers, persiste tombstone hash-only e então remove original, páginas, resultados, revisões e callbacks. Se há claim ativo, responde 409 DOCUMENT_AI_INVALID_TRANSITION com Retry-After: 1;
  • DELETE /schemas/{schemaId} e /policies/{policyId} arquivam, mantendo versões já referenciadas para auditoria;
  • retry reutiliza tentativas de modelo concluídas e nunca refatura a mesma tentativa.

GETOverview#

json
{
  "total_documents": 12,
  "completed_documents": 8,
  "needs_review": 2,
  "failed_documents": 1,
  "completion_rate": 66.6666666667,
  "review_rate": 16.6666666667,
  "median_latency_ms": 940,
  "cost_usd": 0.08431
}

As taxas são percentuais (0..100). median_latency_ms é null quando ainda não há medição — uma tentativa cuja latência não foi cronometrada não é uma resposta de 0 ms. Trate null como "sem dado", nunca como zero.

GETErros de Document AI#

Além do envelope padrão:

error_code HTTP Significado
UNAUTHORIZED 401 credencial ausente/inválida
INSUFFICIENT_PERMISSIONS 403 role agent numa rota owner/admin
TENANT_EXTERNAL_ID_INVALID 400 tenant opaco malformado
TENANT_NOT_FOUND 404 tenant não pertence à empresa
INVALID_JSON 400 JSON malformado ou campo desconhecido
DOCUMENT_AI_NOT_CONFIGURED 503 dependência privada ausente ou callbacks não configurados
DOCUMENT_AI_SCHEMA_INVALID 400 schema, política ou correção inválida
DOCUMENT_AI_INVALID_FILE 400 multipart/MIME/arquivo/página inválido
DOCUMENT_AI_FILE_TOO_LARGE 400 arquivo acima de 20 MiB
CALLBACK_URL_INVALID 400 callback não é HTTPS público permitido
IDEMPOTENCY_KEY_REQUIRED 400 chave ausente ou fora de 8..191
DOCUMENT_AI_IDEMPOTENCY_CONFLICT 409 chave reutilizada para outra request
DOCUMENT_AI_NOT_FOUND 404 recurso ausente ou fora do tenant
DOCUMENT_AI_LIMIT_EXCEEDED 429 orçamento durável de tentativas do job/tenant atingido
DOCUMENT_AI_INVALID_TRANSITION 409 estado não permite a operação; delete com claim ativo inclui Retry-After: 1
RATE_LIMITED 429 limite global/plano/upload atingido
SERVICE_UNAVAILABLE 503 storage privado/preview indisponível

last_error_code no job é diagnóstico operacional, não o error_code da resposta HTTP.