Data Extraction
BetaDocumento entra como imagem, sai como JSON estruturado com nota de confiança
O comprovante que o cliente fotografou com o celular vira campo preenchido na sua esteira, com uma nota de confiança por campo — para que a sua equipe só revise o que a máquina não teve certeza.
- Fintechs e financeiras que digitam à mão comprovante de renda, residência e documento de identidade no onboarding
- Operações de cobrança e conciliação que recebem boleto e nota fiscal em imagem
- Times de backoffice que mantêm uma fila de digitação manual como gargalo do processo
- Digitação manual de documento por equipe de backoffice
- Serviço de OCR com regras por tipo de documento, mantidas à mão
- Integração direta e específica com a API de um provedor de IA dentro do seu código
- Um verificador de autenticidade de documento ou serviço antifraude
- Um sistema de biometria facial ou prova de vida
- Um OCR de propósito geral para digitalizar acervo
1 endpoints em 1 recurso.
Resumo executivo
O Data Extraction lê documentos. Você envia a foto de um comprovante, de uma CNH ou de um boleto; ele devolve os campos que você pediu, em JSON, com uma nota de confiança para cada um.
Na prática, ele resolve a fila de digitação. Sem ele, o comprovante de renda que o cliente fotografou vai para uma pessoa que lê e digita — e essa pessoa é o gargalo do onboarding, a fonte dos erros de digitação e a razão pela qual a análise leva um dia em vez de dois minutos. Com ele, a esteira recebe os campos já preenchidos e a pessoa só olha o que a máquina marcou como incerto.
Está em beta desde janeiro de 2026. Roda no monolito e em standalone na porta 3017, e está declarado nas pilhas de staging e de produção. Duas ressalvas importantes antes de qualquer promessa a cliente: um único provedor de IA está implementado — o Google Gemini —, e o processador assíncrono que executa a extração não está ligado nos ambientes publicados, o que faz toda extração criada ficar parada em PENDING. Leia a §15 primeiro.
| Atributo | Valor |
|---|---|
| Identificador | data-extraction |
| Categoria | Inteligência |
| Escopo | Tenant (exige organizationId no token, em todas as rotas) |
| Porta (standalone) | 3017 |
| Path alias | @data-extraction |
| Prefixo HTTP | /data-extraction |
| Status | Beta desde 2026-01 |
| Depende de | PostgreSQL (schema extraction), Redis Streams, S3, provedor de IA |
| Permissões | DATA_EXTRACTION_ADMIN, DATA_EXTRACTION_READ, DATA_EXTRACTION_WRITE |
O problema
negócioO cenário. Uma fintech abre conta ou concede crédito. O cliente manda uma foto do RG, uma da CNH, um comprovante de residência e um holerite. Alguém precisa transformar essas quatro imagens em campos preenchidos para a análise seguir.
O que trava hoje.
- A digitação é o gargalo. Cada documento leva minutos de uma pessoa. Em pico de campanha, a fila cresce e o tempo de resposta ao cliente cresce junto — exatamente quando ele está mais propenso a desistir.
- A digitação erra. Um dígito trocado no CPF ou uma casa decimal a mais na renda passa despercebido e contamina a decisão de crédito. O erro não é raro; ele é estatístico.
- O OCR tradicional não entende contexto. Reconhecer caracteres é diferente de saber qual daqueles números é a renda líquida. Fechar essa distância exige regras por tipo de documento e por leiaute — e cada banco emite o holerite do jeito dele.
- Os modelos prontos não conhecem documento brasileiro. Os serviços internacionais têm modelo para driver's license e passport; RG, CNH no leiaute do Contran, comprovante de residência de concessionária e holerite brasileiro caem no caminho genérico ou exigem treinar modelo com base rotulada.
- Enviar documento de identidade a terceiro é uma decisão de compliance, não de engenharia. Foto de RG é dado pessoal, e frequentemente carrega dado sensível. Mandar isso para uma API de IA sem base legal definida e sem contrato de operador é um risco que costuma ser assumido por omissão.
O custo de não resolver. O custo direto é a folha do backoffice, que cresce linearmente com o volume — o oposto do que se espera de software. O custo indireto é a conversão perdida no intervalo entre o cliente enviar o documento e receber a resposta. E o custo que só aparece uma vez é o incidente: um vazamento de base com foto de documento de identidade é o tipo de evento que a ANPD trata com severidade e que o mercado não esquece.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| Uma pessoa lê o documento e digita | O documento vira JSON, e a pessoa revisa só o que ficou abaixo do limiar de confiança |
| Regra de extração por leiaute de documento | Campos declarados em linguagem natural, sem base rotulada e sem treino |
| Tudo é conferido, porque não se sabe o que confiar | Nota de confiança por campo, para separar o que precisa de olho humano |
| O dado sai no vocabulário do documento | Mapeamento configurável para o vocabulário do seu sistema |
| Chave de API do provedor de IA no ambiente da plataforma | Credencial por organização, cifrada, com o consumo faturado direto ao cliente |
Template em vez de treino. Você declara o que quer: nome do campo, tipo, descrição e obrigatoriedade. O building block monta o prompt e pede ao modelo de visão. Não há base rotulada, não há ciclo de treino e não há espera para começar a extrair um tipo de documento novo.
Confiança por campo, não por documento. A resposta traz uma nota de 0 a 1 para cada campo extraído, além da média geral. É isso que permite a regra que muda a economia do processo: aceitar automaticamente acima de um limiar e mandar para revisão só o resto.
Mapeamento é configuração. O modelo devolve valor_total; o seu sistema espera amount. Um FieldMapping faz a ponte, com transformações prontas para o que dói no Brasil: 1.234,56 vira 1234.56, 15/08/2026 vira 2026-08-15, R$ 1.500,00 vira 1500.
A conta de IA é do cliente. A credencial do provedor é configurada por organização e cifrada. O consumo é faturado pelo provedor diretamente a quem contratou — a Catalisa não intermedia, não embute margem e não compartilha cota entre clientes.
Documento brasileiro é caso de primeira classe. Há prompt dedicado para boleto bancário, com dezoito campos incluindo linha digitável, código de barras, CPF ou CNPJ de beneficiário e pagador, e contexto declarado para nota fiscal, cupom fiscal, documento de identidade, extrato bancário e contrato.
Casos de uso reais
negócioCaso 1 — Uma fintech tira a digitação do caminho crítico do onboarding Cenário ilustrativo
Fintech de crédito com cerca de 3.000 propostas por mês. Cada proposta traz documento de identidade, comprovante de residência e comprovante de renda.
Três documentos por proposta, nove mil leituras manuais por mês. A fila de digitação era o passo mais lento da esteira e o único que não escalava sem contratar. Em campanha, o tempo de resposta passava de horas para dois dias.
Um template por tipo de documento, com os campos que a análise precisa. O cliente envia o arquivo pelo file-storage, a esteira chama POST /extractions com o fileId e o templateId, e recebe 202 com um identificador. Quando a extração conclui, o evento data-extraction.extraction.completed avisa e os campos já mapeados estão em mappedData.
A revisão humana passa a ser exceção, acionada pelo limiar de confiança, em vez de ser o caminho padrão. O gargalo sai do caminho crítico.
sequenceDiagram
autonumber
participant CLI as "Cliente final"
participant FS as "File Storage"
participant EST as "Esteira da fintech"
participant DE as "Data Extraction"
CLI->>FS: "envia identidade, residência e renda"
FS-->>EST: fileId
EST->>DE: "POST /extractions { fileId, templateId }"
DE-->>EST: "202 — identificador da extração"
Note over DE: "processamento assíncrono"
DE-->>EST: "evento data-extraction.extraction.completed"
EST->>DE: "GET /extractions/:id"
DE-->>EST: "mappedData + confidenceScores"
EST->>EST: "aplica o limiar de confiança"Caso 2 — Uma operação de cobrança lê boleto de qualquer banco Cenário ilustrativo
Empresa que recebe boletos de fornecedores em PDF e imagem, de dezenas de bancos diferentes, e precisa agendar pagamento.
Cada banco tem um leiaute. A tentativa anterior, com OCR mais expressão regular, funcionava para três bancos e quebrava no quarto. Manter as regras consumia mais tempo do que digitar.
O prompt de boleto já é especializado: extrai banco, código do banco, beneficiário e o documento dele, pagador e o documento dele, vencimento, valor, linha digitável, código de barras, desconto, juros, valor cobrado, instruções e o código de agência. Basta enviar o arquivo com documentType: "boleto".
O leiaute deixa de importar, porque o modelo lê o documento em vez de casar posições. Documento de banco novo não exige regra nova.
flowchart LR B1["Boleto do banco A"] --> P["documentType: 'boleto'<br/>prompt dedicado, 18 campos"] B2["Boleto do banco B"] --> P B3["Boleto de um banco novo"] --> P P --> OUT["banco · código do banco · beneficiário e documento<br/>pagador e documento · vencimento · valor<br/>linha digitável · código de barras · desconto<br/>juros · valor cobrado · instruções · agência"] OUT --> PAG["Agendamento de pagamento"]
Caso 3 — Um backoffice revisa por exceção Cenário ilustrativo
Operação que processa comprovantes de renda de qualidade muito variável — foto tremida, contraluz, papel amassado.
Como não havia sinal de qualidade da leitura, tudo era conferido. A automação anterior tinha aumentado o trabalho: agora a pessoa lia o documento e conferia o que a máquina tinha escrito.
Cada campo volta com nota de confiança. A esteira aplica a regra: acima de 0,90 aceita, entre 0,70 e 0,90 marca para conferência rápida, abaixo disso manda para revisão completa. Extração que falha é reprocessável por POST /extractions/:id/retry, e mudança no mapeamento é reaplicável sem reler o documento, por POST /extractions/:id/remap.
O esforço de revisão se concentra onde a máquina admitiu incerteza. A conta que interessa não é "quantos documentos a IA leu", é "quantos um humano precisou abrir".
flowchart LR
EXT["Extração COMPLETED<br/>com confidenceScores por campo"] --> R{"nota do campo"}
R -->|"acima de 0,90"| A["Aceita automaticamente"]
R -->|"entre 0,70 e 0,90"| B["Conferência rápida"]
R -->|"abaixo de 0,70"| C["Revisão completa"]
FALHA["Extração FAILED"] -->|"POST /:id/retry<br/>refaz a leitura e custa token"| EXT
MAPERR["Mapeamento errado"] -->|"POST /:id/remap<br/>reaplica sem chamar a IA — de graça"| EXTCaso 4 — Os modelos prontos do mercado não foram feitos para documento brasileiro Referência de mercado
AWS Textract, Google Document AI e Azure AI Document Intelligence são as três ofertas de nuvem estabelecidas para essa categoria, todas cobrando por página e todas com modelos pré-construídos para fatura, recibo e documento de identidade.
A cobertura de documento brasileiro é pior do que o discurso comercial sugere, e isso está escrito na documentação dos próprios fornecedores. A AWS declara que o Textract extrai informação de "passaportes, carteiras de motorista e outros documentos de identificação emitidos pelo governo dos Estados Unidos". O catálogo de preços do Google Document AI nomeia os processadores de identidade literalmente como "US driver license parser" e "US passport parser". A Nanonets escreve que o modelo de OCR pré-treinado dela é "optimized for US Drivers Licenses" e recomenda o modelo genérico para carteiras de qualquer outro país. O Azure é a exceção parcial: a versão 4.0 do modelo de identidade declara cobertura mundial, mas a tabela oficial nomeia Estados Unidos, Índia e Austrália, e joga o Brasil na categoria genérica "Other". E nenhum dos oito fornecedores internacionais que consultamos oferece modelo para boleto, nota fiscal eletrônica ou CPF (consulta em 2026-08-16).
O mesmo levantamento, em forma de quadro — cada linha é o que o próprio fornecedor publica, não a nossa leitura dele:
| Fornecedor | O que a documentação dele declara | Cobre documento brasileiro? |
|---|---|---|
| AWS Textract | Extrai de "passaportes, carteiras de motorista e outros documentos de identificação emitidos pelo governo dos Estados Unidos" | Não |
| Google Document AI | Processadores nomeados como "US driver license parser" e "US passport parser" | Não |
| Nanonets | Modelo pré-treinado "optimized for US Drivers Licenses"; recomenda o genérico para os demais países | Não |
| Azure AI Document Intelligence | Versão 4.0 declara cobertura mundial, mas a tabela oficial nomeia EUA, Índia e Austrália e joga o Brasil em "Other" | Parcial, sem campos brasileiros garantidos |
| Os oito fornecedores internacionais consultados | — | Nenhum oferece modelo para boleto, nota fiscal eletrônica ou CPF |
Consulta em 2026-08-16, nas páginas e documentações públicas dos próprios fornecedores.
Em vez de modelo especializado por tipo de documento, o building block usa modelo de visão de propósito geral com prompt declarado pelo cliente. Um tipo de documento novo é um template novo, criado por API, sem base rotulada e sem espera. Já há prompt dedicado a boleto brasileiro, com dezoito campos.
Você troca precisão de pico por tempo até o primeiro resultado e por cobertura de cauda longa — que é exatamente o formato do problema de quem recebe documento brasileiro de origem variada. O preço dessa escolha é honesto: para um formulário fixo de altíssimo volume, um modelo especializado bem treinado continua sendo melhor.
Mercado e diferenciais
negócioPanorama. O mercado de processamento inteligente de documento se organiza em três faixas. A primeira é a das nuvens — Textract, Document AI, Document Intelligence —, que cobram por página e entregam modelos pré-construídos mais a opção de treinar o seu. A segunda é a dos especialistas — Mindee, Klippa, Nanonets, e mais recentemente LlamaParse e Reducto —, que cobram por documento e competem em qualidade de extração e experiência de desenvolvedor. A terceira, no Brasil, é a das plataformas de identidade — Unico, Idwall, Caf, BigDataCorp —, que somam extração a antifraude, biometria e consulta a bases oficiais, e vendem o pacote.
Uma quarta faixa surgiu recentemente e é a que este building block ocupa: usar um modelo de visão de propósito geral com prompt declarado, em vez de modelo especializado. É mais flexível e chega mais rápido a um tipo de documento novo; é menos previsível em precisão do que um modelo bem treinado para um formulário fixo.
| Critério | Catalisa Data Extraction | AWS Textract | Google Document AI | Azure AI Doc. Intelligence | Mindee | BigDataCorp | Serpro Datavalid |
|---|---|---|---|---|---|---|---|
| Abordagem | Modelo de visão com prompt declarado | Modelos especializados | Processadores especializados | Modelos pré-construídos e personalizados | Modelos por tipo de documento | Documentoscopia e bases | Fonte primária estatal |
| Custo por mil documentos (2026-08-16) | ≈ US$ 1 em token do provedor | US$ 25 no Analyze ID; US$ 50 em formulários | US$ 30 no Form Parser | US$ 10 no pré-construído | ≈ US$ 44 | ≈ R$ 170 | R$ 700 a R$ 1.140 na validação composta |
| Novo tipo de documento | Um template, por API, sem treino | Extrator genérico ou modelo próprio | Processador personalizado com treino | Modelo personalizado com rotulagem | Modelo personalizado | Catálogo fechado | Catálogo fechado |
| RG e CNH brasileiros | Via prompt, sem modelo dedicado | Não — declara documentos dos EUA | Não — processadores nomeados como dos EUA | Parcial — Brasil cai na categoria "Other" | Não | Sim, é a especialidade | Sim, é a fonte |
| Boleto, nota fiscal e CPF | Prompt dedicado a boleto | Não | Não | Não | Não | CPF sim; boleto não | CPF sim |
| Confiança por campo | Sim | Sim | Sim | Sim | Sim | Sim | Não se aplica |
| Mapeamento e transformação de campo | Sim, com formato brasileiro | Você implementa | Você implementa | Você implementa | Parcial | Depende | Não |
| Credencial de IA do próprio cliente | Sim, cifrada por organização | Não se aplica | Não se aplica | Não se aplica | Não se aplica | Não se aplica | Não se aplica |
| Antifraude e autenticidade | Não | Não | Não | Não | Não | Sim | Sim — é a base oficial |
| Região de processamento no Brasil | Sim, via Vertex AI (§14) | Não existe região da AWS na América do Sul | Sim, São Paulo | Sim, brazilsouth | Não | Sim | Sim, estatal |
| Multi-tenant nativo | Sim, pelo token do IAM | Você implementa | Você implementa | Você implementa | Você implementa | Depende | Depende |
Preços consultados nas páginas oficiais em 2026-08-16: AWS Textract, Google Document AI, Azure AI Document Intelligence pela API de preços de varejo da Microsoft, Mindee, BigDataCorp e Loja Serpro. O custo da coluna da Catalisa é a estimativa do token do provedor de IA, calculada na §6 — não é preço do building block, que está em definição. Unico, Idwall, Certta e Serasa não publicam preço em página aberta.
Nossos diferenciais
- Tipo de documento novo não tem ciclo de treino. Criar um template é uma chamada de API com os campos descritos em português. Isso é difícil de copiar para quem construiu o produto em torno de modelos treinados — não pela técnica, mas porque o modelo de negócio deles depende do ciclo de rotulagem.
- A confiança é por campo, e é acionável. A nota individual é o que permite a regra de revisão por exceção. Um sinal só no documento inteiro não permite decidir qual campo olhar, e é a diferença entre reduzir o trabalho do backoffice e apenas mudá-lo de lugar.
- O mapeamento fala português. As transformações prontas resolvem o que realmente aparece em documento brasileiro: separador de milhar com ponto e decimal com vírgula, data em
DD/MM/AAAA, valor comR$. Um extrator internacional devolve o texto cru e deixa a normalização com você. - A credencial de IA é do cliente. Cada organização configura a própria conta no provedor. O consumo é faturado direto a ela, o modelo pode ser escolhido por ela, e a relação de tratamento de dado pessoal com o provedor de IA é dela — o que, sob a LGPD, é mais simples de sustentar do que a alternativa. Ver §14.
Uma tendência que muda o problema, e vale saber antes de comprar. O caminho principal do onboarding brasileiro está migrando de "fotografar o documento e fazer OCR" para "decodificar o QR Code e consultar a base oficial". Três normas empurram nessa direção: a Lei nº 14.534/2023 tornou o CPF o número único e suficiente de identificação; o Decreto nº 10.977/2022 instituiu a Carteira de Identidade Nacional com QR Code e MRZ, permitindo consulta direta ao registro civil; e a Carteira Digital de Trânsito traz QR Code na CNH — o próprio Serpro afirma que usá-lo "pode dispensar o uso de OCR".
| Norma | O que mudou | Efeito sobre o OCR de identidade |
|---|---|---|
| Lei nº 14.534/2023 | O CPF passa a ser o número único e suficiente de identificação | Reduz a necessidade de ler outros números do documento |
| Decreto nº 10.977/2022 | Institui a Carteira de Identidade Nacional com QR Code e MRZ | Permite consultar o registro civil direto, em vez de fotografar |
| Carteira Digital de Trânsito | QR Code na CNH | O próprio Serpro afirma que usá-lo "pode dispensar o uso de OCR" |
O OCR não desaparece: o mesmo Decreto nº 10.977/2022 mantém os RGs no modelo antigo válidos até 1º de março de 2032, com validade indeterminada para quem tinha 60 anos ou mais em 2022, e documentos estrangeiros continuam existindo. É uma janela que fecha devagar, e vale dimensioná-la ao decidir quanto investir em extração de identidade.
Quando escolher o concorrente. Se o seu problema é fraude documental — saber se aquele RG é verdadeiro, se a foto é da pessoa viva, se o CPF confere com a Receita —, procure Unico, Idwall, Certta (ex-CAF), Serasa ou BigDataCorp: eles fazem isso e este building block não faz nada disso. Ele lê o que está escrito e acredita. Se você precisa de dado de fonte primária, o Serpro Datavalid é o único que consulta a base oficial, com preço público em reais a partir de R$ 0,80 por validação — vale saber que boa parte dos fornecedores privados revende Serpro por baixo, então a tabela dele é o piso da sua negociação. Se o seu volume é alto e concentrado em um formulário de leiaute fixo, um modelo personalizado do Azure ou do Google Document AI supera um modelo de visão genérico em precisão. Se você precisa de texto e coordenada de tudo que está no documento, o Textract entrega isso melhor e mais barato. E se o requisito é residência de dados no Brasil, note que o Azure oferece região brazilsouth pelo mesmo preço e a AWS não tem nenhuma região na América do Sul para o Textract — a §14 explica a configuração que resolve isso do nosso lado.
O Data Extraction ganha quando o problema é muitos tipos de documento brasileiro, de origem variada, sem tempo para treinar modelo — não quando o problema é ler um formulário padronizado em altíssimo volume, e nunca quando o problema é fraude.
Modelo de cobrança e ROI
negócioPrecificação em definição. O Data Extraction não tem preço fechado. Ele é a peça de leitura da cadeia de documentos e a intenção é que acompanhe a contratação do conjunto. Não há valor a divulgar e este documento não estima nenhum.
O custo de IA não é nosso. A credencial do provedor é do cliente, e o consumo de token é faturado pelo provedor diretamente a ele. A Catalisa não revende inferência, não embute margem e não compartilha cota entre clientes. Isso também significa que o cliente escolhe o modelo e vê o custo de origem — o que, numa categoria em que o preço por token cai várias vezes por ano, é vantagem dele.
O que dispara custo.
| Driver | Por quê |
|---|---|
| Extrações concluídas | Cada uma é uma chamada ao modelo de visão com a imagem inteira no corpo |
| Tamanho e número de páginas do documento | Imagem é cobrada em tokens pelo provedor, e documento grande consome mais |
| Tamanho do template | Cada campo declarado entra no prompt; template com quarenta campos custa mais que um com cinco |
| Reprocessamentos | retry refaz a chamada ao modelo e custa de novo. remap não — ele reaproveita o dado já extraído |
| Armazenamento | Cada extração guarda uma cópia do arquivo em S3, mais o dado bruto e o mapeado no banco |
Quanto custa uma página, de verdade. O modelo padrão, gemini-2.5-flash, é cobrado a US$ 0,30 por milhão de tokens de entrada e US$ 2,50 por milhão de saída (tabela oficial, consulta em 2026-08-16). Imagem custa o mesmo que texto — não há tarifa separada de visão.
A conta de uma página, explicitada para você poder refazê-la:
| Componente | Tokens | Custo |
|---|---|---|
| Página de PDF | 258 (valor fixo declarado pelo Google) | — |
| Prompt de instrução com o template | ≈ 500 | — |
| Entrada total | ≈ 758 | 758 × US$ 0,30 ÷ 1M = US$ 0,000227 |
| Saída em JSON | ≈ 300 | 300 × US$ 2,50 ÷ 1M = US$ 0,00075 |
| Total por página | ≈ US$ 0,00098 → cerca de US$ 0,98 por mil páginas |
Comparando com a §5, isso é aproximadamente 10 vezes mais barato que o modelo pré-construído do Azure (US$ 10 por mil), 30 vezes mais barato que o Form Parser do Google e 50 vezes mais barato que o Textract em formulários.
Uma otimização que quase ninguém conhece. Uma página de PDF custa 258 tokens fixos. Uma foto de celular do mesmo documento é dividida em blocos de 258 tokens cada — uma imagem de 1600×1200 consome cerca de 1.032, quatro vezes mais. Converter a captura para PDF antes de enviar reduz o custo de entrada em cerca de 75%. Se o seu volume é alto, esta linha paga a leitura deste documento.
ROI. A conta de guardanapo, num cenário nomeado: operação que processa 9.000 documentos por mês e hoje gasta cerca de 4 minutos de uma pessoa por documento.
São 600 horas por mês de digitação — o equivalente a três a quatro pessoas em tempo integral, só para transformar imagem em campo. Com revisão por exceção, e supondo que 20% dos documentos caiam abaixo do limiar de confiança, o esforço humano cai para cerca de 120 horas: uma pessoa, com folga. O custo de IA nesse volume, pela conta acima, fica na casa de US$ 9 por mês — irrelevante frente a qualquer folha.
| Hoje, sem o BB | Com revisão por exceção | |
|---|---|---|
| Documentos por mês | 9.000 | 9.000 |
| Documentos que um humano abre | 9.000 (100%) | ≈ 1.800 (20%, valor ilustrativo) |
| Tempo humano por mês | ≈ 600 horas | ≈ 120 horas |
| Equivalente em pessoas | 3 a 4 em tempo integral | 1, com folga |
| Custo de IA | — | ≈ US$ 9/mês |
Duas ressalvas para não vender a conta errada. Primeiro, a proporção de 20% é ilustrativa — ela depende do tipo de documento, da qualidade das fotos que os seus clientes mandam e do limiar que você escolher, e só um piloto com os seus documentos responde qual é a sua. Segundo, o ganho maior costuma não ser a folha: é o tempo de resposta ao cliente, que sai de horas para minutos e aparece direto na conversão do funil.
E um custo que não é de token. Se a sua avaliação de LGPD exigir processamento dentro do Brasil, a configuração recomendada na §14 usa o Vertex AI em São Paulo, que tem preço próprio e exige uma conta Google Cloud com projeto e conta de serviço. É trabalho de configuração, não de código — mas é trabalho, e deve entrar no cronograma do projeto.
Arquitetura
O building block tem duas metades: uma API síncrona, que aceita o trabalho e responde 202, e um processador assíncrono que consome uma fila do Redis e executa a extração em quatro passos.
As camadas
flowchart TD
HTTP["HTTP — Bearer JWT emitido pelo IAM"] --> APP["Hono app<br/>basePath '/data-extraction' · applyCommonMiddleware"]
subgraph rotas["routes/ — 22 rotas"]
R1["/api/v1/data-extraction/provider-configs<br/>7 rotas"]
R2["/api/v1/data-extraction/templates<br/>5 rotas"]
R3["/api/v1/data-extraction/templates/:id/mappings<br/>5 rotas"]
R4["/api/v1/data-extraction/extractions<br/>5 rotas"]
R5["/health — sonda pública"]
end
APP --> rotas
rotas -->|"Zod parse → ResultAsync → handleResult"| svc
subgraph svc["services/"]
S1["ProviderConfigService<br/>CRUD + testa credencial + instancia"]
S2["TemplateService<br/>tipos de documento e campos esperados"]
S3["FieldMapperService<br/>mapeamento e transformações"]
S4["ExtractionService<br/>aceita o trabalho e enfileira"]
S5["ExtractionQueueService<br/>XADD no stream do Redis"]
end
svc -->|"Redis Stream 'data-extraction:pipeline'<br/>grupo 'extraction-pipeline-processors'"| CONS
subgraph CONS["ExtractionPipelineConsumer — só roda com EXTRACTION_CONSUMER_ENABLED=true"]
P1["FILE_FETCH<br/>busca e guarda no S3"] --> P2["OCR_EXTRACT<br/>passa direto — não faz nada"]
P2 --> P3["AI_STRUCTURE<br/>chama o modelo de visão"]
P3 --> P4["FIELD_MAP<br/>aplica os mapeamentos"]
end
CONS --> S3B["S3 / MinIO<br/>extraction/{org}/{extração}/source"]
CONS --> PROV["providers/ — createExtractionProvider()"]
PROV --> G["GOOGLE_GEMINI — implementado"]
PROV --> O["OPENAI — recusa 'not yet implemented'"]
PROV --> A["ANTHROPIC — recusa 'not yet implemented'"]
G -->|"HTTPS — imagem em base64"| API["API do Google Gemini<br/>gemini-2.5-flash, resposta em JSON"]O caminho de uma extração, do upload ao registro
A API aceita o trabalho e devolve 202 numa conversa curta; o restante acontece depois, no processador. O diagrama abaixo separa as duas metades pela linha do 202:
sequenceDiagram
autonumber
participant C as "Sua aplicação"
participant API as "API do Data Extraction"
participant DB as "PostgreSQL — schema extraction"
participant Q as "Redis Stream"
participant W as "ExtractionPipelineConsumer"
participant S3 as "S3 / MinIO"
participant AI as "Provedor de IA — Gemini"
C->>API: "POST /extractions { fileId | content | url }"
Note over API: "exatamente UMA das três origens"
API->>DB: "resolve a configuração de provedor<br/>(a informada, ou a padrão da organização)"
API->>DB: "grava ExtractionRecord<br/>status PENDING, currentStep FILE_FETCH"
API->>Q: "XADD na fila"
API-->>C: "202 com o identificador"
Note over Q,W: "a partir daqui, é o processador assíncrono"
W->>Q: "lê a mensagem do grupo de consumidores"
W->>S3: "FILE_FETCH — busca a origem e grava a cópia"
Note over W,S3: "status FILE_FETCHING"
W->>W: "OCR_EXTRACT — passa direto, não há OCR"
Note over W: "status OCR_PROCESSING"
W->>S3: "lê o arquivo"
W->>AI: "AI_STRUCTURE — prompt do template + imagem em base64"
AI-->>W: "JSON com extracted_data e confidence"
Note over W,AI: "status AI_PROCESSING"
W->>DB: "grava rawData, confiança, modelo e tokens"
W->>DB: "FIELD_MAP — aplica os mapeamentos por prioridade"
Note over W,DB: "status MAPPING"
W->>DB: "grava mappedData e marca COMPLETED"
W-->>C: "evento data-extraction.extraction.completed"O que cada passo faz com cada origem
| Passo | Estado | O que acontece |
|---|---|---|
FILE_FETCH | FILE_FETCHING | fileId → busca no file-storage. content → valida base64 e tamanho. url → confere se a URL é segura e baixa com cliente protegido. Em todos os casos: grava em S3 e guarda a chave |
OCR_EXTRACT | OCR_PROCESSING | Passa direto — não há OCR nesta versão (§15) |
AI_STRUCTURE | AI_PROCESSING | Lê o arquivo do S3, monta o prompt a partir do template, chama o modelo de visão e grava rawData, confiança, modelo e tokens |
FIELD_MAP | MAPPING | Aplica os FieldMapping do template por ordem de prioridade, grava mappedData, marca COMPLETED e publica o evento |
Atenção. Qualquer falha em qualquer passo leva o registro a FAILED, com a mensagem gravada e o evento data-extraction.extraction.failed publicado. E se EXTRACTION_CONSUMER_ENABLED estiver desligado, a conversa termina no 202: a metade de baixo do diagrama simplesmente não acontece, e a extração fica em PENDING para sempre (§15).
Decisões não óbvias
- Toda extração é assíncrona, sempre. Não existe modo síncrono. A razão é o tempo: uma chamada a modelo de visão com imagem leva de segundos a dezenas de segundos, e prender uma conexão HTTP nisso transforma pico de volume em esgotamento de conexões. O custo dessa escolha é que o cliente precisa consultar ou ouvir evento — e é por isso que a §15 destaca com tanta ênfase o processador desligado: sem ele, o
202é uma promessa que ninguém cumpre. - A fila é um Redis Stream com grupo de consumidores, e não uma lista. Stream dá confirmação explícita e permite recuperar mensagem pendente de um processo que morreu. Na subida, o consumidor executa uma reivindicação automática das entradas paradas há mais de 60 segundos.
- A mensagem é confirmada mesmo quando o passo falha. É deliberado: reentrega automática de uma falha determinística — imagem ilegível, credencial errada — só multiplicaria o custo de token sem chance de sucesso. O reprocessamento é explícito, por
POST /extractions/:id/retry, e conta as tentativas. O preço é que falha transitória também não é repetida sozinha. - Cada passo é uma mensagem separada. Um passo termina enfileirando o próximo. Isso mantém cada unidade de trabalho curta, permite observar onde a extração parou pelo campo
currentStep, e permitiria escalar os passos de forma independente. O custo é mais idas ao Redis e ao banco por extração. - O arquivo é sempre copiado para o S3, mesmo quando já veio de lá. Assim o passo de IA tem uma origem única e o reprocessamento não depende de a origem continuar disponível — uma URL externa pode ter caducado. O custo é armazenamento duplicado, e é o motivo de a §14 tratar essa área do bucket como repositório de documento pessoal.
- A URL externa passa por verificação antes do download. A origem
urlé conferida por uma função de segurança compartilhada e baixada por um cliente HTTP protegido, para que ninguém use este building block como ponte para alcançar endereços internos da sua rede. - A temperatura do modelo é 0,1 e a resposta é exigida em JSON. Extração não quer criatividade. O provedor é configurado para responder com tipo JSON, e ainda assim há um caminho de recuperação que procura um bloco de código na resposta quando o modelo insiste em envolver o JSON em texto.
- A confiança geral é a média aritmética das notas por campo. É simples e é suficiente para triagem — mas note a consequência: um documento com dezenove campos ótimos e um campo crítico ilegível tem média alta. Use as notas por campo para decidir, não a média.
Monolito vs. standalone. O app.ts é montado no monolito em src/app.ts e responde em http://localhost:3000/data-extraction. O main.ts sobe o mesmo app na porta 3017 quando DEPLOYMENT_MODE=standalone, e também tenta iniciar o processador — que só sobe de fato se EXTRACTION_CONSUMER_ENABLED for verdadeiro. Essa é a diferença de comportamento mais importante entre os modos, e a fonte da limitação principal da §15.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Extração | Um trabalho: um documento, um template opcional e um resultado. Corresponde ao modelo ExtractionRecord. |
| Template de extração | O que se espera de um tipo de documento: nome, documentType e a lista de campos esperados. Pode trazer um prompt próprio que substitui o gerado. |
| Campo esperado | Declaração de um campo: name, type (string, number, date, boolean, array, object), description, required e format. A descrição vai para o prompt e é o que orienta o modelo. |
documentType | Texto livre que dá contexto ao prompt. Seis valores têm tratamento especial: receipt, invoice, id_document, bank_statement, contract e boleto. |
| Mapeamento de campo | Regra que leva um campo do resultado bruto para o vocabulário do seu sistema, com transformação opcional. Origem e destino aceitam caminho com ponto para estrutura aninhada. |
Dado bruto (rawData) | O que o modelo devolveu, sem tradução. |
Dado mapeado (mappedData) | O resultado depois de aplicados os mapeamentos. Sem mapeamentos, é igual ao bruto. |
| Nota de confiança | Valor de 0 a 1 por campo, informado pelo próprio modelo. overallConfidence é a média aritmética delas. |
Passo (step) | Uma das quatro etapas: FILE_FETCH, OCR_EXTRACT, AI_STRUCTURE, FIELD_MAP. O campo currentStep diz onde a extração está ou onde parou. |
Origem (sourceType) | De onde o documento veio: FILE_ID, BASE64 ou URL. Exatamente uma por extração. |
| Chave mestra de credenciais | DATA_EXTRACTION_CREDENTIAL_MASTER_KEY. Chave AES-256-GCM de 32 bytes, em 64 caracteres hexadecimais, que cifra as credenciais de IA de todos os tenants. |
Modelo de dados — schema extraction no PostgreSQL. 5 modelos.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
ExtractionProviderConfig | extraction_provider_configs | Provedor de IA e credenciais da organização | Único (organizationId, name), providerType, credentials (cifrado), isDefault, isActive, settings, deletedAt |
ExtractionTemplate | extraction_templates | O que extrair de um tipo de documento | Único (organizationId, name), documentType, expectedFields, customPrompt, isActive, deletedAt |
FieldMapping | field_mappings | Tradução de campo, com transformação | Único (templateId, sourceField, targetField), transformType, transformConfig, isRequired, defaultValue, priority |
ExtractionRecord | extraction_records | O trabalho e o resultado | status, currentStep, sourceType, sourceRef, s3Key, rawData, mappedData, confidenceScores, providerModel, tokensUsed, processingTimeMs, retryCount, businessId, stepHistory |
ExtractionWebhookSubscription | extraction_webhook_subscriptions | Assinatura de notificação | url, secret, events, isActive. Sem nenhum endpoint — ver §15 |
Configurações, templates e extrações são isolados por organizationId. Mapeamentos herdam o isolamento do template e são removidos em cascata com ele.
Enumerações
| Enum | Valores |
|---|---|
ExtractionProviderType | GOOGLE_GEMINI (implementado) · OPENAI · ANTHROPIC (declarados, recusam em execução — §15) |
ExtractionStatus | PENDING · PROCESSING · FILE_FETCHING · OCR_PROCESSING · AI_PROCESSING · MAPPING · COMPLETED · FAILED |
FieldTransformType | NONE · DATE_FORMAT · CURRENCY_NORMALIZE · NUMBER_PARSE · STRING_TRIM · REGEX_EXTRACT |
Máquina de estados da extração
stateDiagram-v2
[*] --> PENDING: "POST /extractions"
PENDING --> FILE_FETCHING: "processador consome a fila"
FILE_FETCHING --> OCR_PROCESSING: "arquivo gravado no S3"
OCR_PROCESSING --> AI_PROCESSING: "passa direto — não há OCR"
AI_PROCESSING --> MAPPING: "modelo devolveu o JSON"
MAPPING --> COMPLETED: "mappedData gravado"
PENDING --> FAILED: "falha"
FILE_FETCHING --> FAILED: "falha"
OCR_PROCESSING --> FAILED: "falha"
AI_PROCESSING --> FAILED: "falha"
MAPPING --> FAILED: "falha"
FAILED --> PENDING: "POST /:id/retry — soma 1 a retryCount"
COMPLETED --> COMPLETED: "POST /:id/remap — reaplica sem chamar a IA"
note right of PENDING
Gravado e enfileirado.
Se o processador não estiver ligado,
PARA AQUI para sempre (§15).
end note
note right of COMPLETED
remap só aceita COMPLETED.
retry só aceita FAILED.
end note| Estado | Quando o registro está nele | Observação |
|---|---|---|
PENDING | Gravado e enfileirado, ainda não consumido | Com o processador desligado, é onde toda extração fica para sempre (§15) |
FILE_FETCHING | Buscando a origem e gravando no S3 | — |
OCR_PROCESSING | Passo OCR_EXTRACT | Passa direto — não há OCR nesta versão (§15) |
AI_PROCESSING | Chamando o modelo de visão | É o passo que custa token |
MAPPING | Aplicando os mapeamentos do template | — |
COMPLETED | Concluída, com rawData e mappedData | remap reaplica o mapeamento e o registro permanece COMPLETED |
FAILED | Falha em qualquer passo acima | retry volta para PENDING e reprocessa do início, somando 1 a retryCount |
PROCESSING | Nunca | Existe no enum e nenhum caminho de código o atribui (§15) |
Como um campo chega ao seu sistema
flowchart TD
DOC["Documento — imagem ou PDF"] --> PROMPT["Prompt montado a partir do template"]
PROMPT --> C1["contexto do documentType<br/>se for um dos seis conhecidos"]
PROMPT --> C2["lista de campos esperados,<br/>com tipo e descrição"]
PROMPT --> C3["formato exigido:<br/>extracted_data e confidence"]
CUSTOM["customPrompt no template"] -.->|"SUBSTITUI tudo isso"| PROMPT
C1 --> MODELO["Modelo de visão"]
C2 --> MODELO
C3 --> MODELO
MODELO --> RAW["rawData = extracted_data"]
MODELO --> CONF["confidenceScores = confidence"]
RAW --> MAP["FieldMapping<br/>aplicados em ordem crescente de priority"]
MAP --> M1["lê a origem — aceita caminho com ponto<br/>ex.: endereco.cidade"]
M1 --> M2{"campo ausente?"}
M2 -->|sim| M3["usa o defaultValue"]
M2 -->|não| M4["aplica a transformação"]
M3 --> M4
M4 --> M5["escreve no destino<br/>também aceita caminho com ponto"]
M5 --> MAPPED["mappedData — o que a sua esteira consome"]
RAW -.->|"sem mapeamentos definidos,<br/>mappedData é igual a rawData"| MAPPEDTransformações disponíveis
| Tipo | O que faz | Configuração |
|---|---|---|
NONE | Nada | — |
STRING_TRIM | Remove espaços das pontas | — |
NUMBER_PARSE | Converte texto em número. Com locale: "pt-BR", entende 1.234,56 | locale |
CURRENCY_NORMALIZE | Remove R$, €, £, ¥ e espaços, e converte. Com locale: "pt-BR", entende o formato brasileiro | locale |
DATE_FORMAT | Converte entre formatos de data | from, to — por exemplo DD/MM/YYYY para YYYY-MM-DD |
REGEX_EXTRACT | Extrai parte do texto por expressão regular | pattern, group |
Referência da API
Prefixo HTTP: /data-extraction. Em monolito, a base é http://localhost:3000. Em standalone, a porta é 3017; em staging, https://data-extraction.bb.stg.catalisa.app.
Atenção ao prefixo. O caminho real repete o nome do módulo: o basePath do app é /data-extraction e os routers são montados em /api/v1/data-extraction. A rota completa é /data-extraction/api/v1/data-extraction/.... Copie do quadro abaixo em vez de deduzir.
Todas as 22 rotas exigem, sem exceção: authMiddleware (Bearer JWT do IAM), requirePermission e o middleware local requireOrganization, que devolve 403 quando o token não carrega organizationId.
Configurações de provedor — /data-extraction/api/v1/data-extraction/provider-configs
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | .../provider-configs | Cria a configuração, testa a credencial e cifra. Responde 201 | DATA_EXTRACTION_ADMIN |
GET | .../provider-configs | Lista, paginado | DATA_EXTRACTION_READ |
GET | .../provider-configs/:id | Busca (sem as credenciais) | DATA_EXTRACTION_READ |
PATCH | .../provider-configs/:id | Atualiza. Trocar credencial dispara novo teste | DATA_EXTRACTION_ADMIN |
DELETE | .../provider-configs/:id | Exclusão lógica. Responde 204 | DATA_EXTRACTION_ADMIN |
POST | .../provider-configs/:id/set-default | Marca como padrão | DATA_EXTRACTION_ADMIN |
POST | .../provider-configs/:id/test | Testa a conexão com o provedor | DATA_EXTRACTION_ADMIN |
Filtros em GET: filter[providerType], filter[isActive]. Paginação por page[number] e page[size].
Templates — /data-extraction/api/v1/data-extraction/templates
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | .../templates | Cria template. Responde 201 | DATA_EXTRACTION_ADMIN |
GET | .../templates | Lista, paginado | DATA_EXTRACTION_READ |
GET | .../templates/:id | Busca | DATA_EXTRACTION_READ |
PATCH | .../templates/:id | Atualiza | DATA_EXTRACTION_ADMIN |
DELETE | .../templates/:id | Exclusão lógica. Responde 204 | DATA_EXTRACTION_ADMIN |
Filtros em GET: filter[documentType], filter[isActive].
Mapeamentos — /data-extraction/api/v1/data-extraction/templates/:templateId/mappings
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | .../templates/:templateId/mappings | Cria mapeamento. Responde 201 | DATA_EXTRACTION_ADMIN |
GET | .../templates/:templateId/mappings | Lista os mapeamentos do template | DATA_EXTRACTION_READ |
GET | .../templates/:templateId/mappings/:id | Busca | DATA_EXTRACTION_READ |
PATCH | .../templates/:templateId/mappings/:id | Atualiza | DATA_EXTRACTION_ADMIN |
DELETE | .../templates/:templateId/mappings/:id | Remove. Responde 204 | DATA_EXTRACTION_ADMIN |
Extrações — /data-extraction/api/v1/data-extraction/extractions
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | .../extractions | Cria a extração e enfileira. Responde 202 | DATA_EXTRACTION_WRITE |
GET | .../extractions | Lista, paginado | DATA_EXTRACTION_READ |
GET | .../extractions/:id | Busca — é a rota de acompanhamento | DATA_EXTRACTION_READ |
POST | .../extractions/:id/retry | Reprocessa. Só aceita FAILED | DATA_EXTRACTION_WRITE |
POST | .../extractions/:id/remap | Reaplica os mapeamentos. Só aceita COMPLETED | DATA_EXTRACTION_WRITE |
Filtros em GET: filter[status], filter[businessId], filter[templateId].
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /data-extraction/health | Sonda de disponibilidade. Pública, não contabilizada nas 22 rotas |
POST /data-extraction/api/v1/data-extraction/provider-configs
Cria a configuração. Ao contrário de outros building blocks com provedor, esta rota testa a credencial antes de gravar: uma chave inválida é recusada na criação.
Request
{
"name": "Gemini Produção",
"providerType": "GOOGLE_GEMINI",
"credentials": { "apiKey": "<sua chave da API Gemini>" },
"settings": { "model": "gemini-2.5-flash", "temperature": 0.1, "maxTokens": 8192 },
"isDefault": true
}{
"name": "Gemini Produção",
"providerType": "GOOGLE_GEMINI",
"credentials": { "apiKey": "<sua chave da API Gemini>" },
"settings": { "model": "gemini-2.5-flash", "temperature": 0.1, "maxTokens": 8192 },
"isDefault": true
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–100) | Sim | Único por organização |
providerType | GOOGLE_GEMINI | OPENAI | ANTHROPIC | Sim | Só GOOGLE_GEMINI funciona hoje (§15) |
credentials | objeto de textos | Sim | Cifrado antes de gravar |
isDefault | booleano | Não | — |
isActive | booleano | Não | Padrão true |
settings | objeto | Não | model, maxTokens, temperature |
Credenciais aceitas para o Google Gemini
| Combinação | Uso |
|---|---|
{ apiKey } | Chave da API Gemini. É o caminho mais simples. Confirme que o projeto tem faturamento ativo — sem isso você cai na camada gratuita, que os termos do fornecedor proíbem para dado pessoal (§14) |
{ serviceAccountKey, vertexProjectId?, vertexRegion? } | Conta de serviço para Vertex AI. Região padrão us-central1 — informe southamerica-east1 se quiser processamento no Brasil (§14) |
{ vertexProjectId, refreshToken, clientId, clientSecret } | Vertex AI com OAuth de usuário |
{ refreshToken, clientId, clientSecret } | OAuth de usuário na API pública |
Ajustes (settings) — padrões: modelo gemini-2.5-flash, maxTokens 8192, temperature 0,1.
Para documento de identidade, a configuração recomendada é conta de serviço com
vertexRegion: "southamerica-east1"emodel: "gemini-2.5-flash". É a única combinação com compromisso público de processamento dentro do Brasil — ver §14.
Resposta 201 — devolve a configuração sem o campo credentials, que não volta em nenhuma rota.
Erros
| Status | Código | Quando |
|---|---|---|
400 | VALIDATION | Corpo reprovado; credenciais em combinação inválida; falha no teste de conexão |
403 | — | Sem DATA_EXTRACTION_ADMIN, ou token sem organizationId |
409 | CONFLICT | Nome já usado na organização |
500 | — | DATA_EXTRACTION_CREDENTIAL_MASTER_KEY ausente ou malformada |
POST /data-extraction/api/v1/data-extraction/templates
Request
{
"name": "CNH",
"description": "Carteira Nacional de Habilitação",
"documentType": "id_document",
"expectedFields": [
{ "name": "nome", "type": "string", "required": true, "description": "Nome completo do condutor" },
{ "name": "cpf", "type": "string", "required": true, "description": "CPF, com pontos e traço" },
{ "name": "data_nascimento", "type": "date", "required": true, "format": "YYYY-MM-DD" },
{ "name": "numero_registro", "type": "string", "description": "Número do registro da CNH" },
{ "name": "validade", "type": "date", "format": "YYYY-MM-DD" },
{ "name": "categoria", "type": "string", "description": "Categoria de habilitação, por exemplo AB" }
]
}{
"name": "CNH",
"description": "Carteira Nacional de Habilitação",
"documentType": "id_document",
"expectedFields": [
{ "name": "nome", "type": "string", "required": true, "description": "Nome completo do condutor" },
{ "name": "cpf", "type": "string", "required": true, "description": "CPF, com pontos e traço" },
{ "name": "data_nascimento", "type": "date", "required": true, "format": "YYYY-MM-DD" },
{ "name": "numero_registro", "type": "string", "description": "Número do registro da CNH" },
{ "name": "validade", "type": "date", "format": "YYYY-MM-DD" },
{ "name": "categoria", "type": "string", "description": "Categoria de habilitação, por exemplo AB" }
]
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–100) | Sim | Único por organização |
description | string (máx. 1000) | Não | — |
documentType | string (1–50) | Sim | Contexto do prompt. Seis valores têm tratamento especial |
expectedFields | lista | Sim | Pode ser vazia; nesse caso o prompt fica genérico |
customPrompt | string (máx. 5000) | Não | Substitui integralmente o prompt gerado, inclusive as instruções de formato |
isActive | booleano | Não | Padrão true |
A
descriptionde cada campo é o que mais influencia a qualidade da extração — ela vai literalmente para o prompt. "CPF, com pontos e traço" produz resultado melhor e mais estável do que apenas "cpf".Se usar
customPrompt, você assume a responsabilidade de instruir o modelo a devolver o objeto com as chavesextracted_dataeconfidence. Sem isso, a resposta não é interpretada e a extração falha.
POST /data-extraction/api/v1/data-extraction/extractions
Aceita o trabalho e responde 202. A extração ainda não aconteceu quando você recebe a resposta.
Request
{
"fileId": "8f2c1a90-0000-0000-0000-000000000000",
"templateId": "3a2b1c00-0000-0000-0000-000000000000",
"documentType": "id_document",
"businessId": "proposta-2026-000481",
"metadata": { "origem": "app-cliente" }
}{
"fileId": "8f2c1a90-0000-0000-0000-000000000000",
"templateId": "3a2b1c00-0000-0000-0000-000000000000",
"documentType": "id_document",
"businessId": "proposta-2026-000481",
"metadata": { "origem": "app-cliente" }
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
fileId | UUID | Uma das três | Arquivo no file-storage |
content | string | Uma das três | Arquivo em base64 |
contentMimeType | string (máx. 100) | Não | Tipo do conteúdo em base64 |
url | URL (máx. 2048) | Uma das três | Endereço público do arquivo. Passa por verificação de segurança |
configId | UUID | Não | Configuração a usar. Omitido, usa a padrão |
templateId | UUID | Não | Sem ele, a extração é genérica e não há mapeamento |
documentType | string (máx. 50) | Não | Sobrepõe o do template |
businessId | string | Não | Identificador da operação de origem |
metadata | objeto | Não | Campo livre |
Exatamente uma das três origens. Zero ou duas resultam em
400.
Resposta 202
{
"data": {
"type": "extraction",
"id": "d41f7b02-0000-0000-0000-000000000000",
"links": { "self": "/api/v1/data-extraction/extractions/d41f7b02-0000-0000-0000-000000000000" },
"attributes": {
"status": "PENDING",
"currentStep": "FILE_FETCH",
"sourceType": "FILE_ID",
"retryCount": 0,
"businessId": "proposta-2026-000481",
"createdAt": "2026-08-16T14:03:11.000Z"
}
}
}{
"data": {
"type": "extraction",
"id": "d41f7b02-0000-0000-0000-000000000000",
"links": { "self": "/api/v1/data-extraction/extractions/d41f7b02-0000-0000-0000-000000000000" },
"attributes": {
"status": "PENDING",
"currentStep": "FILE_FETCH",
"sourceType": "FILE_ID",
"retryCount": 0,
"businessId": "proposta-2026-000481",
"createdAt": "2026-08-16T14:03:11.000Z"
}
}
}Erros
| Status | Código | Quando |
|---|---|---|
400 | — | Nenhuma ou mais de uma origem; corpo reprovado no Zod |
403 | — | Sem DATA_EXTRACTION_WRITE, ou token sem organizationId |
413 | — | Corpo acima de 1 MB — teto prático de content (§15) |
A configuração de provedor não é exigida na criação: se a organização não tiver uma padrão, a extração é criada mesmo assim e falha depois, no passo de IA.
GET /data-extraction/api/v1/data-extraction/extractions/:id
A rota de acompanhamento.
Resposta 200, extração concluída
{
"data": {
"type": "extraction",
"id": "d41f7b02-0000-0000-0000-000000000000",
"attributes": {
"status": "COMPLETED",
"currentStep": "FIELD_MAP",
"rawData": {
"nome": "MARIA SOUZA",
"cpf": "123.456.789-00",
"data_nascimento": "1988-03-12",
"validade": "2030-05-15",
"categoria": "AB"
},
"mappedData": {
"fullName": "MARIA SOUZA",
"taxId": "12345678900",
"birthDate": "1988-03-12"
},
"confidenceScores": {
"nome": 0.98, "cpf": 0.99, "data_nascimento": 0.95,
"validade": 0.91, "categoria": 0.72
},
"providerModel": "gemini-2.5-flash",
"tokensUsed": 1842,
"processingTimeMs": 4231,
"retryCount": 0,
"completedAt": "2026-08-16T14:03:26.000Z"
}
}
}{
"data": {
"type": "extraction",
"id": "d41f7b02-0000-0000-0000-000000000000",
"attributes": {
"status": "COMPLETED",
"currentStep": "FIELD_MAP",
"rawData": {
"nome": "MARIA SOUZA",
"cpf": "123.456.789-00",
"data_nascimento": "1988-03-12",
"validade": "2030-05-15",
"categoria": "AB"
},
"mappedData": {
"fullName": "MARIA SOUZA",
"taxId": "12345678900",
"birthDate": "1988-03-12"
},
"confidenceScores": {
"nome": 0.98, "cpf": 0.99, "data_nascimento": 0.95,
"validade": 0.91, "categoria": 0.72
},
"providerModel": "gemini-2.5-flash",
"tokensUsed": 1842,
"processingTimeMs": 4231,
"retryCount": 0,
"completedAt": "2026-08-16T14:03:26.000Z"
}
}
}Resposta 200, extração falhada — status: "FAILED", currentStep com o passo em que parou e errorMessage com o motivo.
Use
confidenceScorescampo a campo para decidir o que revisar. A média geral esconde exatamente o caso que importa: um único campo crítico com nota baixa no meio de vários campos ótimos.
POST /data-extraction/api/v1/data-extraction/extractions/:id/retry
Volta a extração para PENDING, soma 1 a retryCount, limpa a mensagem de erro e reenfileira desde o início. Refaz a chamada ao modelo e custa tokens de novo.
| Status | Quando |
|---|---|
200 | Reenfileirada |
400 | Can only retry failed extractions — o estado não é FAILED |
404 | Extração inexistente ou de outra organização |
Não há limite de tentativas nem espera progressiva. Se a causa for determinística — imagem ilegível, credencial errada —, repetir só gasta.
POST /data-extraction/api/v1/data-extraction/extractions/:id/remap
Reaplica os mapeamentos sobre o dado bruto já extraído. Não chama o modelo e não custa tokens.
Request (corpo opcional)
{ "templateId": "outro-template-uuid" }{ "templateId": "outro-template-uuid" }Sem templateId, usa o da extração.
| Status | Quando |
|---|---|
200 | mappedData atualizado |
400 | Can only remap completed extractions; sem dado bruto; sem template; ou o template não tem mapeamento definido |
404 | Extração inexistente ou de outra organização |
É a rota certa quando você errou o mapeamento, não a extração. Corrigir o mapeamento e reaplicar é gratuito; reprocessar não é.
Início rápido
Do zero a um documento lido. Comandos escritos para staging; não executados nesta redação.
Antes de começar: confirme com a operação que
EXTRACTION_CONSUMER_ENABLEDestá ligado no ambiente. Sem isso, o passo 5 fica emPENDINGpara sempre — ver §15.
1. Autenticar no IAM
TOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@catalisa.app",
"password": "root123456",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)
BASE=https://data-extraction.bb.stg.catalisa.app/data-extraction/api/v1/data-extractionTOKEN=$(curl -s -X POST https://iam.bb.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@catalisa.app",
"password": "root123456",
"organizationId": "b0000000-0000-0000-0000-000000000001"
}' | jq -r .accessToken)
BASE=https://data-extraction.bb.stg.catalisa.app/data-extraction/api/v1/data-extraction2. Configurar o provedor de IA
CFG=$(curl -s -X POST "$BASE/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Gemini Homologação",
"providerType": "GOOGLE_GEMINI",
"credentials": { "apiKey": "SUBSTITUA_PELA_SUA_CHAVE" },
"isDefault": true
}')
echo "$CFG" | jq '.data.attributes'CFG=$(curl -s -X POST "$BASE/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Gemini Homologação",
"providerType": "GOOGLE_GEMINI",
"credentials": { "apiKey": "SUBSTITUA_PELA_SUA_CHAVE" },
"isDefault": true
}')
echo "$CFG" | jq '.data.attributes'A criação já testa a credencial. Se a chave estiver errada, você recebe 400 aqui — e é bom que seja assim.
3. Criar o template
TPL=$(curl -s -X POST "$BASE/templates" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Boleto Bancário",
"documentType": "boleto",
"expectedFields": [
{ "name": "beneficiary_name", "type": "string", "required": true, "description": "Nome do beneficiário" },
{ "name": "payer_name", "type": "string", "required": true, "description": "Nome do pagador" },
{ "name": "due_date", "type": "date", "required": true, "format": "YYYY-MM-DD" },
{ "name": "amount", "type": "number", "required": true, "description": "Valor do documento em reais" },
{ "name": "digitable_line", "type": "string", "required": true, "description": "Linha digitável de 47 dígitos" }
]
}')
TPL_ID=$(echo "$TPL" | jq -r '.data.id')TPL=$(curl -s -X POST "$BASE/templates" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "Boleto Bancário",
"documentType": "boleto",
"expectedFields": [
{ "name": "beneficiary_name", "type": "string", "required": true, "description": "Nome do beneficiário" },
{ "name": "payer_name", "type": "string", "required": true, "description": "Nome do pagador" },
{ "name": "due_date", "type": "date", "required": true, "format": "YYYY-MM-DD" },
{ "name": "amount", "type": "number", "required": true, "description": "Valor do documento em reais" },
{ "name": "digitable_line", "type": "string", "required": true, "description": "Linha digitável de 47 dígitos" }
]
}')
TPL_ID=$(echo "$TPL" | jq -r '.data.id')4. Enviar o documento
EXT=$(curl -s -X POST "$BASE/extractions" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"content": "'"$(base64 -w0 boleto.png)"'",
"contentMimeType": "image/png",
"templateId": "'"$TPL_ID"'",
"documentType": "boleto",
"businessId": "teste-001"
}')
EXT_ID=$(echo "$EXT" | jq -r '.data.id')
echo "$EXT" | jq '.data.attributes | {status, currentStep}'EXT=$(curl -s -X POST "$BASE/extractions" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"content": "'"$(base64 -w0 boleto.png)"'",
"contentMimeType": "image/png",
"templateId": "'"$TPL_ID"'",
"documentType": "boleto",
"businessId": "teste-001"
}')
EXT_ID=$(echo "$EXT" | jq -r '.data.id')
echo "$EXT" | jq '.data.attributes | {status, currentStep}'Resposta esperada: 202 com {"status": "PENDING", "currentStep": "FILE_FETCH"}.
5. Acompanhar até concluir
for i in $(seq 1 20); do
R=$(curl -s "$BASE/extractions/$EXT_ID" -H "Authorization: Bearer $TOKEN")
S=$(echo "$R" | jq -r '.data.attributes.status')
echo "$S — passo: $(echo "$R" | jq -r '.data.attributes.currentStep')"
case "$S" in
COMPLETED) echo "$R" | jq '.data.attributes | {rawData, mappedData, confidenceScores, tokensUsed}'; break ;;
FAILED) echo "$R" | jq '.data.attributes | {currentStep, errorMessage}'; break ;;
esac
sleep 3
donefor i in $(seq 1 20); do
R=$(curl -s "$BASE/extractions/$EXT_ID" -H "Authorization: Bearer $TOKEN")
S=$(echo "$R" | jq -r '.data.attributes.status')
echo "$S — passo: $(echo "$R" | jq -r '.data.attributes.currentStep')"
case "$S" in
COMPLETED) echo "$R" | jq '.data.attributes | {rawData, mappedData, confidenceScores, tokensUsed}'; break ;;
FAILED) echo "$R" | jq '.data.attributes | {currentStep, errorMessage}'; break ;;
esac
sleep 3
doneSe o estado nunca sair de PENDING, o processador não está rodando. É a limitação principal da §15, e não é problema da sua chamada.
6. Traduzir para o vocabulário do seu sistema
curl -s -X POST "$BASE/templates/$TPL_ID/mappings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"sourceField": "amount",
"targetField": "valorDocumento",
"transformType": "CURRENCY_NORMALIZE",
"transformConfig": { "locale": "pt-BR" },
"priority": 1
}' | jq
curl -s -X POST "$BASE/extractions/$EXT_ID/remap" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' \
| jq '.data.attributes.mappedData'curl -s -X POST "$BASE/templates/$TPL_ID/mappings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"sourceField": "amount",
"targetField": "valorDocumento",
"transformType": "CURRENCY_NORMALIZE",
"transformConfig": { "locale": "pt-BR" },
"priority": 1
}' | jq
curl -s -X POST "$BASE/extractions/$EXT_ID/remap" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{}' \
| jq '.data.attributes.mappedData'O remap reaproveita o dado já extraído — não gasta token nenhum.
Credenciais de staging conforme AMBIENTES.md. Nunca use documento real de cliente em ambiente de homologação.
Receitas
Ler um documento que já está no file-storage
O caminho recomendado, e o único que não esbarra no limite de 1 MB do corpo.
1. Suba o arquivo no file-storage
FILE_ID=$(curl -s -X POST https://storage.bb.stg.catalisa.app/file-storage/api/v1/files \
-H "Authorization: Bearer $TOKEN" -F "file=@comprovante.jpg" | jq -r '.data.id')FILE_ID=$(curl -s -X POST https://storage.bb.stg.catalisa.app/file-storage/api/v1/files \
-H "Authorization: Bearer $TOKEN" -F "file=@comprovante.jpg" | jq -r '.data.id')Resposta esperada — um UUID em FILE_ID.
2. Crie a extração apontando para o arquivo
curl -s -X POST "$BASE/extractions" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"fileId":"'"$FILE_ID"'","templateId":"'"$TPL_ID"'","businessId":"proposta-2026-000481"}' | jqcurl -s -X POST "$BASE/extractions" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"fileId":"'"$FILE_ID"'","templateId":"'"$TPL_ID"'","businessId":"proposta-2026-000481"}' | jqResposta esperada — 202, com sourceType igual a FILE_ID:
{
"data": {
"type": "extraction",
"id": "d41f7b02-0000-0000-0000-000000000000",
"attributes": { "status": "PENDING", "currentStep": "FILE_FETCH", "sourceType": "FILE_ID" }
}
}{
"data": {
"type": "extraction",
"id": "d41f7b02-0000-0000-0000-000000000000",
"attributes": { "status": "PENDING", "currentStep": "FILE_FETCH", "sourceType": "FILE_ID" }
}
}Armadilhas.
- O
fileIdprecisa pertencer à mesma organização do token, ou o passo de busca falha. - Use o mesmo
businessIddo document-template e do e-signature. É o fio que costura a operação nos três serviços. - O arquivo é copiado para uma área própria no S3. Isso é intencional — o reprocessamento não depende de a origem continuar existindo —, e implica armazenamento duplicado.
Montar a regra de revisão por exceção
O padrão que muda a economia do processo. Aplique na sua esteira depois de receber o evento de conclusão.
curl -s "$BASE/extractions/$EXT_ID" -H "Authorization: Bearer $TOKEN" | jq '
.data.attributes as $a
| {
abaixoDoLimiar: [ $a.confidenceScores | to_entries[] | select(.value < 0.9) | .key ],
valores: $a.mappedData
}'curl -s "$BASE/extractions/$EXT_ID" -H "Authorization: Bearer $TOKEN" | jq '
.data.attributes as $a
| {
abaixoDoLimiar: [ $a.confidenceScores | to_entries[] | select(.value < 0.9) | .key ],
valores: $a.mappedData
}'Armadilhas.
- Não use a média. Um campo crítico com nota 0,4 no meio de dezenove campos ótimos produz média alta. Decida campo a campo.
- A nota vem do próprio modelo e é uma autoavaliação, não uma medida calibrada. Trate-a como ordenação relativa — "este campo é mais duvidoso que aquele" —, não como probabilidade.
- Calibre o limiar com os seus documentos. O valor que funciona para boleto impresso não funciona para foto de holerite amassado.
- Campo que o modelo não achou volta com valor nulo e confiança 0. Verifique nulo antes de comparar o limiar.
Traduzir campos aninhados e formatos brasileiros
Objetivo: entregar à sua esteira o vocabulário e os tipos que ela espera, sem tratamento no meio do caminho.
| Passo | Campo de origem | Vira | Transformação |
|---|---|---|---|
| 1 | valor_total = "R$ 1.234,56" | pagamento.valor = 1234.56 | CURRENCY_NORMALIZE, locale: pt-BR |
| 2 | vencimento = "15/08/2026" | pagamento.vencimento = "2026-08-15" | DATE_FORMAT, DD/MM/YYYY → YYYY-MM-DD |
| 3 | cpf_pagador = "123.456.789-00" | pagador.documento | REGEX_EXTRACT — leia a armadilha abaixo |
1. Valor em reais vira número
curl -s -X POST "$BASE/templates/$TPL_ID/mappings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sourceField":"valor_total","targetField":"pagamento.valor",
"transformType":"CURRENCY_NORMALIZE","transformConfig":{"locale":"pt-BR"},"priority":1}'curl -s -X POST "$BASE/templates/$TPL_ID/mappings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sourceField":"valor_total","targetField":"pagamento.valor",
"transformType":"CURRENCY_NORMALIZE","transformConfig":{"locale":"pt-BR"},"priority":1}'Resposta esperada: 201 com o mapeamento criado.
2. Data brasileira vira ISO
curl -s -X POST "$BASE/templates/$TPL_ID/mappings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sourceField":"vencimento","targetField":"pagamento.vencimento",
"transformType":"DATE_FORMAT","transformConfig":{"from":"DD/MM/YYYY","to":"YYYY-MM-DD"},"priority":2}'curl -s -X POST "$BASE/templates/$TPL_ID/mappings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sourceField":"vencimento","targetField":"pagamento.vencimento",
"transformType":"DATE_FORMAT","transformConfig":{"from":"DD/MM/YYYY","to":"YYYY-MM-DD"},"priority":2}'3. Só os dígitos do CPF
curl -s -X POST "$BASE/templates/$TPL_ID/mappings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sourceField":"cpf_pagador","targetField":"pagador.documento",
"transformType":"REGEX_EXTRACT","transformConfig":{"pattern":"[0-9]+","group":0},"priority":3}'curl -s -X POST "$BASE/templates/$TPL_ID/mappings" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sourceField":"cpf_pagador","targetField":"pagador.documento",
"transformType":"REGEX_EXTRACT","transformConfig":{"pattern":"[0-9]+","group":0},"priority":3}'Armadilhas.
REGEX_EXTRACTcom[0-9]+devolve apenas o primeiro grupo de dígitos: de123.456.789-00sai123. Para juntar tudo, o padrão precisa capturar o número inteiro — teste antes de confiar.sourceFieldetargetFieldaceitam caminho com ponto para estrutura aninhada. Se o modelo devolveu uma lista, o índice não é suportado.- A ordem de aplicação é a de
prioritycrescente. Mapeamentos que escrevem no mesmo destino se sobrescrevem em silêncio. - Só os campos com mapeamento entram em
mappedData. Tudo que você não mapear some do resultado traduzido — o dado continua emrawData. - A combinação de
templateId,sourceFieldetargetFieldé única. Repetir devolve409.
Melhorar a extração sem trocar de modelo
O maior ganho de qualidade vem do template, não do provedor.
curl -s -X PATCH "$BASE/templates/$TPL_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"expectedFields": [
{ "name": "renda_liquida", "type": "number", "required": true,
"description": "Valor líquido a receber, em reais, na linha final do holerite. NÃO é o salário bruto nem o total de proventos." },
{ "name": "competencia", "type": "date", "format": "YYYY-MM",
"description": "Mês de competência do holerite, no formato ano-mês" }
]
}' | jqcurl -s -X PATCH "$BASE/templates/$TPL_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"expectedFields": [
{ "name": "renda_liquida", "type": "number", "required": true,
"description": "Valor líquido a receber, em reais, na linha final do holerite. NÃO é o salário bruto nem o total de proventos." },
{ "name": "competencia", "type": "date", "format": "YYYY-MM",
"description": "Mês de competência do holerite, no formato ano-mês" }
]
}' | jqArmadilhas.
- A
descriptionvai literalmente para o prompt. Descrição que diz o que o campo não é costuma render mais que sinônimo. formattambém entra no prompt e é o que faz o modelo devolver a data no padrão certo — sem ele, cada documento volta num formato.customPromptsubstitui tudo, inclusive as instruções de formato de resposta. Se usá-lo, replique a exigência das chavesextracted_dataeconfidence, ou a extração falha.- Mudar o template não afeta extrações já concluídas. Para reaplicar, é
retry— e aí custa token.
Investigar uma extração que falhou
curl -s "$BASE/extractions/$EXT_ID" -H "Authorization: Bearer $TOKEN" \
| jq '.data.attributes | {status, currentStep, errorMessage, retryCount, sourceType}'curl -s "$BASE/extractions/$EXT_ID" -H "Authorization: Bearer $TOKEN" \
| jq '.data.attributes | {status, currentStep, errorMessage, retryCount, sourceType}'currentStep | Causa provável | O que fazer |
|---|---|---|
FILE_FETCH | Arquivo inexistente, base64 inválido, URL recusada pela verificação de segurança, ou tamanho acima do limite | Confira a origem. URL interna ou de rede privada é recusada de propósito |
AI_STRUCTURE | Nenhuma configuração padrão; credencial expirada; provedor não implementado; o modelo não devolveu JSON interpretável | Rode POST /provider-configs/:id/test |
FIELD_MAP | Falha ao aplicar transformação | Revise transformConfig e use remap depois de corrigir |
Continua PENDING | O processador não está rodando | Verifique EXTRACTION_CONSUMER_ENABLED no ambiente (§15) |
Armadilha. retry não tem espera progressiva nem teto de tentativas. Antes de repetir, descubra se a causa é transitória — repetir erro determinístico só gasta token.
Integração com outros building blocks
O Data Extraction é a última peça da cadeia de documentos da Catalisa, e a única que trabalha no sentido contrário. O document-template gera o documento que sai; o e-signature coleta a assinatura; o Data Extraction lê o que chega.
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token e define o organizationId que isola configurações, templates e extrações | Sim |
| File Storage | Origem do documento por fileId, e destino recomendado dos arquivos recebidos | Não |
| E-Signature | Fornece o documento assinado que volta para conferência | Não |
| Document Template | Fecha a cadeia: gera o documento que, preenchido, volta para ser lido | Não |
| Customers | Destino natural dos dados extraídos de documento de identidade | Não |
| Decision Platform | Consome os campos extraídos como entrada da esteira de decisão | Não |
| Webhooks Engine | Entrega os eventos data-extraction.* a sistemas externos | Não |
| Audit Trail | Registra quem criou extração e quem leu resultado com dado pessoal | Não |
A cadeia de documentos, ponta a ponta
flowchart TD
subgraph ida["O documento que SAI da plataforma"]
DT["Document Template<br/>gera a CCB"] --> FS1["File Storage<br/>guarda o PDF"]
FS1 --> ES["E-Signature<br/>coleta a assinatura"]
ES --> ASS["Documento assinado"]
end
ASS -->|"o cliente devolve documento e comprovantes"| FS2
subgraph volta["O documento que CHEGA à plataforma"]
FS2["File Storage<br/>recebe a foto do cliente"] -->|fileId| DE["Data Extraction<br/>lê o documento e devolve JSON<br/>com confiança por campo"]
DE -->|mappedData| DP["Decision Platform<br/>decide com o dado já estruturado"]
DE -->|"dados cadastrais"| CU["Customers<br/>cadastro do cliente"]
end
BID["businessId = 'proposta-2026-000481'<br/>o mesmo em todos"] -.-> DT
BID -.-> ES
BID -.-> DE
BID -.-> DPEste diagrama é o argumento comercial da Catalisa nesta família: gerar, assinar e ler são o mesmo problema visto de três ângulos, e a cadeia fecha um ciclo. O documento que a plataforma emitiu volta preenchido e é lido pela mesma plataforma; o comprovante que o cliente manda entra pela mesma porta.
Quem monta isso com três fornecedores diferentes gasta a maior parte do esforço em amarrar identidade, isolamento por cliente e correlação — e depois descobre que responder "onde está a proposta 481" exige juntar três bases que não se conhecem. Aqui as três peças compartilham o token do IAM, o organizationId do tenant e o businessId da operação. GET /extractions?filter[businessId]=proposta-2026-000481 responde o que foi lido; a mesma chamada, com o mesmo filtro, responde nos outros dois serviços o que foi gerado e o que foi assinado.
Vale registrar a fronteira com honestidade: a cadeia está desenhada e as peças se encaixam pelo businessId, mas o encadeamento não é automático. Não há orquestração pronta que dispare a extração quando um arquivo chega. Quem monta o fluxo é a sua aplicação, ou o decision-platform, ouvindo os eventos.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
DATA_EXTRACTION_CREDENTIAL_MASTER_KEY | Chave AES-256-GCM para cifrar credenciais de IA. Exatamente 64 caracteres hexadecimais. Gere com openssl rand -hex 32 | Sim, para usar o módulo | — |
EXTRACTION_CONSUMER_ENABLED | Liga o processador assíncrono. Sem ele, nenhuma extração sai de PENDING | Sim, na prática | false |
EXTRACTION_PIPELINE_BATCH_SIZE | Mensagens lidas por ciclo | Não | 10 |
EXTRACTION_PIPELINE_BLOCK_MS | Espera na leitura da fila | Não | 5000 |
EXTRACTION_MAX_FILE_SIZE_MB | Tamanho máximo do arquivo | Não | 10 |
DATABASE_URL | PostgreSQL. O building block usa o schema extraction | Sim | — |
REDIS_URL | Redis. Não é opcional — a fila de processamento vive nele | Sim | — |
S3_* | Endpoint, credenciais e bucket, para guardar os documentos | Sim | — |
JWT_SECRET | Segredo HS256 compartilhado com o IAM. Mínimo 44 caracteres | Sim | — |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
PORT | Porta em standalone | Não | 3000 (a topologia expõe 3017) |
EXTRACTION_CONSUMER_ENABLEDé a variável que mais importa neste building block. Ela éfalsepor padrão e o serviço sobe normalmente sem ela, registrando apenas uma linha informativa no log. A API aceita trabalho e responde202; ninguém processa. Confirme-a no deploy — e ver §15.
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema extraction — configurações, templates, mapeamentos e extrações |
| Redis Streams | Fila data-extraction:pipeline, com o grupo extraction-pipeline-processors |
| S3 ou MinIO | Cópia de trabalho de cada documento, em extraction/{organização}/{extração}/source |
| IAM | Verificação do token; o organizationId vem do claim assinado |
| Provedor de IA | Hoje, a API do Google Gemini |
Limites e quotas
| Limite | Valor | Onde |
|---|---|---|
| Corpo da requisição | 1 MB | applyCommonMiddleware — é o teto prático de content em base64 |
| Tamanho do arquivo | 10 MB por padrão | EXTRACTION_MAX_FILE_SIZE_MB — inalcançável por content, alcançável por fileId e url |
| Espera ao baixar de URL | 30 segundos | Cliente HTTP protegido |
| Tokens de saída do modelo | 8192 por padrão | settings.maxTokens |
customPrompt | 5.000 caracteres | Zod |
| Recuperação de mensagem parada | Após 60 segundos de inatividade, na subida | Consumidor |
| Limite de taxa global | 10.000 requisições por minuto por IP, quando ligado | RATE_LIMIT_GLOBAL_MAX |
Eventos publicados
| Evento | Quando |
|---|---|
data-extraction.extraction.started | Extração criada e enfileirada |
data-extraction.extraction.completed | Mapeamento concluído e estado COMPLETED |
data-extraction.extraction.failed | Falha em qualquer passo, com o passo e o motivo |
data-extraction.extraction.retry | Reprocessamento solicitado |
Os tipos extraction.step.started e extraction.step.completed existem no catálogo e não são publicados por nenhum caminho de código.
Catálogo de erros
O envelope de erro é { "error": "<código>", "message": "<texto>", "details": <opcional> }. As rotas que usam validação com safeParse — extrações, templates, mapeamentos e configurações — devolvem, em caso de corpo inválido, { "error": { ...formato do Zod... } }, que é um formato diferente. Trate os dois.
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo reprovado; origem ambígua; estado incompatível com retry ou remap; teste de credencial falhou | Leia a message |
401 | — | Token ausente, inválido ou expirado | Renove no IAM |
403 | — | Falta a permissão exigida, ou o token não carrega organizationId | Confira o papel e a organização |
404 | NOT_FOUND | Recurso inexistente, excluído, ou de outra organização | Confira o identificador. "De outra organização" e "inexistente" são a mesma resposta, de propósito |
409 | CONFLICT | Nome de configuração ou de template repetido; mapeamento duplicado | Escolha outro |
413 | — | Corpo acima de 1 MB | Use fileId ou url |
429 | — | Limite de taxa estourado | Aplique recuo exponencial |
500 | INTERNAL | Falha de infraestrutura, ou chave mestra ausente | Verifique a message e os logs |
Erros do provedor de IA aparecem em errorMessage da extração, não como resposta HTTP — a chamada ao modelo acontece depois do 202.
Observabilidade.
GET /data-extraction/healthresponde a sonda de disponibilidade. Ela não verifica se o processador está rodando — um serviço com o processador desligado responde saudável.- O melhor indicador operacional é a contagem de extrações em
PENDINGcom mais de alguns minutos: se ela cresce, o processador não está consumindo. - Cada extração concluída grava
providerModel,tokensUsedeprocessingTimeMs. É a base para acompanhar custo de IA e latência sem depender do painel do provedor. retryCountacumula por extração e é o sinal mais direto de qualidade de origem: muitas tentativas indicam documento ruim ou template mal descrito.- O processador registra cada passo com o identificador da extração e o passo, e recupera na subida as mensagens paradas há mais de 60 segundos.
Segurança e compliance
Esta seção é mais longa que a dos outros building blocks de propósito. Documento de identidade é dado pessoal, e a decisão de enviá-lo a um provedor de IA de terceiro é uma decisão de LGPD que precisa ser tomada conscientemente, não por omissão.
Isolamento entre tenants e credenciais cifradas
Isolamento entre tenants. O organizationId vem do claim assinado do JWT e nunca do corpo. Todas as 22 rotas passam pelo middleware local requireOrganization, que devolve 403 quando o claim está ausente. Os serviços conferem a organização do recurso antes de devolvê-lo, e um recurso de outra organização responde 404, não 403 — a distinção permitiria descobrir a existência dele. No S3, cada arquivo fica em extraction/{organizationId}/{extractionId}/source, o que mantém a separação também no armazenamento.
Credenciais do provedor de IA. O que você envia em credentials — chave de API, chave de conta de serviço, tokens de OAuth — é serializado e cifrado com AES-256-GCM antes de tocar o banco, com vetor de inicialização de 12 bytes sorteado a cada operação e etiqueta de autenticação de 16 bytes. A chave mestra DATA_EXTRACTION_CREDENTIAL_MASTER_KEY é validada como 64 caracteres hexadecimais e é do serviço, cifrando as credenciais de todos os tenants — guarde-a em SOPS e trate um vazamento dela como comprometimento de todas as credenciais. O campo cifrado nunca volta em nenhuma resposta: a função que monta o corpo de resposta simplesmente não o inclui. Não há rotação de chave implementada (§15).
O documento é enviado a um provedor de IA de terceiro
O documento é enviado a um provedor de IA de terceiro. Sim, e isto precisa estar claro.
O passo de estruturação lê o arquivo do S3, converte em base64 e envia a imagem inteira, no corpo da requisição, para a API do provedor configurado. Com a configuração padrão, esse provedor é o Google, e o destino é a API do Gemini ou o Vertex AI, conforme o tipo de credencial. Não há edição, mascaramento nem recorte antes do envio: o que vai é o documento completo, com foto, número e todos os demais campos.
A natureza do dado, com a distinção que importa. O art. 5º, II da LGPD traz um rol taxativo de dado sensível, que inclui "dado genético ou biométrico, quando vinculado a uma pessoa natural". O número do RG ou da CNH não está nesse rol — é dado pessoal comum, tratado pelo art. 7º. A imagem facial, quando usada para identificação ou autenticação, é dado biométrico e portanto sensível, sujeita ao art. 11.
A distinção é operacional, não acadêmica: extrair texto de uma CNH é art. 7º; comparar o rosto da CNH com uma selfie é art. 11. Este building block faz apenas a primeira coisa — ele não faz comparação facial nem biometria. Mas a imagem enviada ao provedor contém o rosto, e um fluxo de onboarding típico faz as duas coisas com fornecedores diferentes, caindo nos dois regimes. Registre isso no seu mapeamento de tratamento.
Um alerta que evita erro caro: legítimo interesse (art. 7º, IX) não serve para dado sensível — ele não consta do rol do art. 11. Para o componente biométrico, as bases realistas são o art. 11, II, "a" (cumprimento de obrigação legal ou regulatória) e o art. 11, II, "g" (prevenção à fraude e segurança do titular em processos de identificação e autenticação de cadastro em sistemas eletrônicos).
flowchart TD
DOC["Imagem de uma CNH"] --> Q{"o que se faz com ela?"}
Q -->|"extrair o texto — nome, CPF, validade"| A7["Dado pessoal comum<br/>art. 7º da LGPD"]
Q -->|"comparar o rosto com uma selfie"| A11["Dado biométrico — SENSÍVEL<br/>art. 11 da LGPD"]
A7 --> BB["É isto que este building block faz"]
A11 --> OUTRO["Outro fornecedor —<br/>este BB não faz biometria"]
A11 --> AVISO["Legítimo interesse (art. 7º, IX)<br/>NÃO serve aqui"]
AVISO --> BASES["Bases realistas:<br/>art. 11, II, 'a' — obrigação legal ou regulatória<br/>art. 11, II, 'g' — prevenção à fraude"]
BB -.->|"mas a imagem enviada contém o rosto"| A11Atenção. Um fluxo de onboarding típico faz as duas coisas, com fornecedores diferentes, e cai nos dois regimes ao mesmo tempo. Registre isso no seu mapeamento de tratamento.
| Ponto | Situação |
|---|---|
| Papel das partes | O cliente da Catalisa é o controlador. A Catalisa é operadora. O provedor de IA é suboperador, e a relação contratual com ele é do cliente, porque a credencial é dele |
| Transferência internacional | Com a configuração padrão, o processamento ocorre fora do Brasil. Isso é transferência internacional, regida pelos arts. 33 a 36 |
| Base legal | Precisa ser definida pelo cliente para a finalidade específica de leitura automatizada do documento. O building block não presume nenhuma |
| Retenção | O documento fica no S3 e o dado extraído fica no banco indefinidamente. Não há política de retenção nem expurgo automático (§15) |
Transferência internacional: o que a ANPD já decidiu
Transferência internacional: o que a ANPD já decidiu, e o que muda a escolha.
O art. 33 permite a transferência em hipóteses fechadas. As duas que importam aqui são o inciso I — países ou organismos com grau de proteção adequado reconhecido — e o inciso II, "b" — cláusulas-padrão contratuais. Dois atos da autoridade mudam o cálculo:
- A Resolução CD/ANPD nº 19, de 23 de agosto de 2024 aprovou o Regulamento de Transferência Internacional e o texto das cláusulas-padrão. O art. 16 é taxativo: a validade da transferência amparada nelas "pressupõe a adoção integral e sem alteração do texto disponibilizado no Anexo II". O prazo para adequar contratos existentes era de doze meses da publicação — vencido em 23 de agosto de 2025. Não localizamos nenhuma resolução que o tenha prorrogado.
- A Resolução nº 32, de 26 de janeiro de 2026 reconheceu a União Europeia como organismo que proporciona grau de proteção adequado, autorizando transferências com base no art. 33, I para todos os Estados membros, para os países do EEE e para as instituições da própria União.
Os Estados Unidos não foram reconhecidos. Não localizamos decisão de adequação para os EUA, nem reconhecimento de equivalência das cláusulas europeias. A consequência prática é direta e reordena a escolha de fornecedor: enviar documento a uma API americana exige cláusulas-padrão da ANPD adotadas integralmente, com prazo de adequação já vencido; enviar a um processador na União Europeia passou, desde janeiro de 2026, a ser amparado pelo art. 33, I — instrumento muito mais simples.
flowchart TD
ONDE{"onde o documento é processado?"}
ONDE -->|"São Paulo — southamerica-east1"| BR["NÃO há transferência internacional<br/>dispensa cláusulas-padrão e toda a cadeia"]
ONDE -->|"União Europeia ou EEE"| UE["Art. 33, I<br/>Resolução nº 32, de 26/01/2026<br/>grau de proteção adequado reconhecido"]
ONDE -->|"Estados Unidos"| US["Art. 33, II, 'b'<br/>cláusulas-padrão da Resolução nº 19/2024,<br/>adotadas integralmente e sem alteração"]
ONDE -->|"padrão do código — us-central1"| US
US --> PRAZO["Prazo de adequação<br/>venceu em 23/08/2025"]
BR:::melhor
classDef melhor stroke-width:3pxAtenção — o padrão do código cai no caminho mais caro. Sem vertexRegion explícito, o provedor usa us-central1, ou seja, Estados Unidos. Quem não souber disso caracteriza transferência internacional sem perceber, sob o instrumento cujo prazo de adequação já venceu.
A política de dados do provedor implementado
A política de dados do provedor implementado, e por que a camada importa mais que o preço.
Os termos da API Gemini, consultados em 2026-08-16, separam duas camadas com consequências opostas:
| Camada gratuita | Camada paga | Vertex AI | |
|---|---|---|---|
| Usa o seu conteúdo para desenvolver produtos e treinar modelos | Sim | Não | Não, por restrição contratual |
| Revisores humanos podem ler entrada e saída | Sim | Não | Só em monitoramento de abuso |
| Retenção declarada | Não quantificada | "limited period of time", sem número publicado | Cache de 24 horas, desativável; retenção zero é alcançável |
| Onde processa | Qualquer país | Qualquer país | Região escolhida, se usar endpoint regional |
| Instrumento contratual | Termos de uso | Aditivo de tratamento de dados | Aditivo mais termos específicos do serviço |
Os próprios termos da camada gratuita contêm a frase que encerra a discussão: "Do not submit sensitive, confidential, or personal information to the Unpaid Services." Enviar documento de titular para a camada gratuita contraria o contrato do fornecedor, antes mesmo de qualquer análise de LGPD.
Há ainda duas armadilhas nesses termos que mudam a análise. A primeira: o que separa as camadas é existir cobrança ativa no projeto, e não o fato de você estar efetivamente pagando — o próprio texto diz que o acesso é considerado pago "mesmo quando oferecido sem custo", desde que a conta tenha um projeto de nuvem com faturamento ativo. A segunda, e é desconfortável: os termos estendem as regras da camada paga a todos os serviços para quem está no Espaço Econômico Europeu, na Suíça e no Reino Unido. O Brasil não está nessa lista. Um usuário brasileiro na camada gratuita tem os dados usados para treino; um europeu, não.
A configuração recomendada
A configuração recomendada, e ela resolve o problema.
Existe uma combinação que mantém o processamento dentro do Brasil e, com isso, elimina a transferência internacional — dispensando cláusulas-padrão, o prazo vencido e toda a cadeia de suboperadores:
| Item | Valor |
|---|---|
| Credencial | serviceAccountKey — conta de serviço do Vertex AI |
vertexRegion | southamerica-east1 (São Paulo) — o padrão é us-central1, então você precisa informar |
settings.model | gemini-2.5-flash, na variante de 128 mil de contexto |
| Endpoint | Regional, nunca global — endpoints globais não dão garantia de residência |
| Cache implícito | Desativado no projeto |
| Monitoramento de abuso | Solicitar exceção de registro de prompt ao fornecedor |
O detalhe que quase ninguém conhece: de todos os modelos Gemini, apenas o gemini-2.5-flash na variante de 128 mil de contexto tem compromisso público de processamento de aprendizado de máquina dentro do Brasil. A variante de 1 milhão do mesmo modelo não tem; o Flash-Lite não tem; o Pro não tem; nenhum modelo da geração seguinte tem (tabela oficial de residência de dados do Google Cloud, consulta em 2026-08-16). É por isso que gemini-2.5-flash é o padrão deste building block.
O que isso obriga você a fazer, e é responsabilidade de quem opera:
- Definir a base legal para a leitura automatizada de documento, na política de privacidade e no registro de operações de tratamento (art. 37). Para onboarding bancário, o enquadramento mais defensável costuma combinar o art. 7º, II com o art. 11, II, "a", ancorados em obrigação regulatória — o seu jurídico decide.
- Nunca usar a camada gratuita do provedor para documento de titular. Os termos do próprio fornecedor proíbem.
- Configurar região explicitamente se residência importa. O padrão do código é
us-central1. - Enquadrar a transferência internacional caso opte por processar fora do Brasil — hoje, isso significa cláusulas-padrão da ANPD adotadas integralmente e sem alteração.
- Verificar ativamente o operador. O art. 39 diz que o controlador "verificará a observância das próprias instruções" — não basta contratar, é preciso auditar. E lembre que o art. 42, § 1º, I equipara o operador ao controlador quando ele descumpre a lei, com responsabilidade solidária.
- Documentar a cadeia de suboperadores. A cláusula sobre transferências posteriores do Anexo II da Resolução nº 19/2024 exige autorização expressa para subcontratação e responsabiliza o importador pelas irregularidades do terceiro.
A alternativa honesta. Se a sua avaliação concluir que nem a configuração brasileira resolve, este building block não é o produto certo para documento de identidade. Use-o para documentos de menor sensibilidade — boleto, nota fiscal, extrato — e trate identidade com fornecedor nacional especializado. Vale saber que, para comprovante de renda, o mercado brasileiro largamente abandonou o OCR: as duas soluções que venceram evitam o documento, consultando dado oficial ou fazendo análise forense do arquivo. Essas recomendações estão aqui porque são a resposta certa em alguns casos, e omiti-las seria vender errado.
Verificação de destino ao baixar de URL. A origem url passa por uma verificação de segurança antes do download, e o download usa um cliente HTTP protegido. É o que impede que alguém use este building block como ponte para alcançar endereços internos da sua rede. Não desabilite essa verificação.
O que fica gravado, e por quanto tempo
O que fica gravado, e por quanto tempo.
| Onde | O que | Retenção |
|---|---|---|
S3, em extraction/{organização}/{extração}/source | O documento inteiro, como enviado | Indefinida. Sem expurgo automático |
extraction_records.rawData | Todos os campos extraídos — nome, CPF, endereço, renda | Indefinida |
extraction_records.mappedData | Os mesmos dados, traduzidos | Indefinida |
extraction_records.sourceRef | Para origem em base64, o conteúdo do arquivo inteiro fica gravado neste campo de texto | Indefinida |
extraction_provider_configs.credentials | Credenciais do provedor, cifradas | Até a exclusão |
Duas consequências práticas. A primeira: o banco de extrações é um repositório de dado pessoal, e às vezes sensível — trate-o com o mesmo controle de acesso, cifragem em repouso e política de retenção que você aplica ao cadastro de clientes. A segunda, e menos óbvia: quando a origem é content em base64, o arquivo é gravado também em texto no registro da extração, além da cópia no S3. Prefira fileId, que não tem esse efeito.
Permissões. Três permissões: DATA_EXTRACTION_READ para leitura, DATA_EXTRACTION_WRITE para criar e reprocessar extração, e DATA_EXTRACTION_ADMIN para configurar provedor, template e mapeamento. Note que DATA_EXTRACTION_READ dá acesso ao resultado da extração, que contém os dados pessoais lidos — é uma permissão sensível, e não deve ser tratada como "somente leitura, então é inofensiva". Hoje, apenas o papel ADMIN recebe essas três permissões no catálogo padrão do IAM (§15).
Exclusão lógica. Configurações e templates usam deletedAt. Registros de extração não têm exclusão lógica nem rota de exclusão — uma vez criados, permanecem. Atender a pedido de eliminação da LGPD exige expurgo manual, no banco e no S3 (§15).
Limitações conhecidas
| Limitação | Impacto | Situação |
|---|---|---|
| O processador assíncrono não está ligado nos ambientes publicados | EXTRACTION_CONSUMER_ENABLED é false por padrão e não aparece nas pilhas de staging nem de produção. Sem ele, toda extração criada fica em PENDING para sempre, e a API responde 202 normalmente | Bloqueante — é a primeira coisa a verificar antes de qualquer demonstração |
| Só o Google Gemini está implementado | OPENAI e ANTHROPIC existem no enum, mas as classes são placeholders que recusam com "not yet implemented". A criação da configuração falha no teste de conexão, então o erro aparece cedo. O ADR-0004 declara os três como implementados e está desatualizado | Roadmap — não anuncie multi-provedor |
| Não há OCR | O passo OCR_EXTRACT existe, muda o estado e não faz nada. Toda a leitura é feita pelo modelo de visão. PDF puramente textual é enviado como imagem, sem extração prévia de texto | Por design nesta versão; o passo está reservado |
| Corpo limitado a 1 MB | content em base64 só comporta arquivo de cerca de 750 KB, embora EXTRACTION_MAX_FILE_SIZE_MB permita 10 MB. Use fileId ou url para arquivos maiores | Por design do middleware comum; ajustável na infraestrutura |
| Base64 fica gravado em texto no registro | Com origem content, o arquivo inteiro é gravado em sourceRef, além da cópia no S3 — duplicando o armazenamento de dado pessoal. Prefira fileId | Roadmap |
| Sem exclusão de extração | Não há rota de exclusão nem exclusão lógica no registro. Documento e dado extraído permanecem indefinidamente; expurgo para LGPD é manual, no banco e no S3 | Roadmap — é a lacuna mais relevante para compliance |
ExtractionWebhookSubscription não tem nenhum endpoint | O modelo existe no banco, com URL, segredo e eventos, e nenhuma rota o expõe. Notificação é pelo webhooks-engine, ouvindo os eventos | Especificado, não implementado |
stepHistory nunca é preenchido | A coluna existe para guardar o histórico de passos com duração e erro, e nenhum código escreve nela | Especificado, não implementado |
PROCESSING nunca é atribuído | O estado existe no enum e nenhum caminho o usa | Resquício |
| Sem reentrega automática de falha | A mensagem é confirmada mesmo quando o passo falha. Falha transitória exige retry manual | Por design, para não repetir custo de token em falha determinística |
retry sem teto nem espera progressiva | Nada impede reprocessar indefinidamente uma extração que sempre falha, gastando token a cada vez | Roadmap |
| A confiança geral é média simples | Um campo crítico ruim some na média de vários campos bons. Decida campo a campo | Por design; a média é apenas indicativa |
| A nota de confiança é autoavaliação do modelo | Não é medida calibrada. Serve para ordenar, não como probabilidade | Limitação da abordagem |
| Sem verificação de autenticidade e sem antifraude | O building block lê o que está escrito e acredita. Documento adulterado é lido normalmente. Não use como controle antifraude | Fora de escopo — é o território de fornecedores de identidade |
| Sem modelo especializado por documento brasileiro | Há prompt dedicado a boleto e contexto para cinco outros tipos, mas não há modelo treinado para RG ou CNH. A precisão depende do modelo de visão genérico | Por design da abordagem |
| A região padrão do Vertex AI é fora do Brasil | Sem vertexRegion explícito, o código usa us-central1. Quem não souber disso caracteriza transferência internacional sem perceber (§14) | Por design do provedor; documente na sua configuração |
| Sem rotação da chave mestra | Trocar DATA_EXTRACTION_CREDENTIAL_MASTER_KEY inutiliza todas as credenciais já cifradas | Roadmap |
Permissões só no papel ADMIN | DATA_EXTRACTION_* não está atribuída a nenhum papel intermediário no catálogo padrão do IAM | Configuração pendente |
| Modo de autenticação não convencional no provedor Google | Além de chave de API, conta de serviço e OAuth, o provedor tem um quarto caminho que usa endpoints internos do Google e um identificador de cliente montado. Não é caminho documentado publicamente pelo fornecedor e não deve ser usado em produção — prefira chave de API, conta de serviço ou Vertex AI | Deve ser revisto |
O extrator de templateId das rotas de mapeamento usa o texto da URL | O identificador do template é obtido por correspondência sobre a URL da requisição, e não pelo roteador. É frágil a mudança de caminho | Roadmap |
Perguntas frequentes
Criei uma extração e ela não sai de PENDING. O que houve?
O processador assíncrono não está rodando. Ele depende de EXTRACTION_CONSUMER_ENABLED=true, que é false por padrão e não está definido nas pilhas de staging nem de produção (§15). O serviço sobe normalmente sem ele e a sonda de saúde responde que está tudo bem — o único sinal é a fila parada. É a primeira coisa a verificar, antes de investigar qualquer outra hipótese.
Quais provedores de IA vocês suportam?
Um: Google Gemini, no modelo gemini-2.5-flash por padrão, com quatro formas de autenticação. OPENAI e ANTHROPIC aparecem no schema mas são placeholders — a criação da configuração falha no teste de conexão, então você descobre na hora. O ADR-0004 do projeto diz que os três estão implementados e está desatualizado; o código é a fonte de verdade.
O documento do meu cliente é enviado para fora?
Sim, a imagem completa é enviada em base64 para a API do provedor configurado — e sai do Brasil, salvo se você configurar explicitamente o contrário. O padrão do código é a região us-central1, o que caracteriza transferência internacional sob os arts. 33 a 36 da LGPD.
Existe uma configuração que resolve: conta de serviço do Vertex AI, vertexRegion igual a southamerica-east1, modelo gemini-2.5-flash na variante de 128 mil de contexto e endpoint regional. É a única combinação de modelo Gemini com compromisso público de processamento dentro do Brasil, e ela elimina a transferência internacional. A §14 traz o quadro completo e o que ainda cabe a você fazer.
O provedor de IA treina modelo com os meus documentos?
Depende da camada, e a diferença é radical. Na camada gratuita da API Gemini, os próprios termos declaram que o Google usa o conteúdo enviado para desenvolver produtos e tecnologias de aprendizado de máquina, e que revisores humanos podem ler entrada e saída. O mesmo documento instrui, literalmente: "Do not submit sensitive, confidential, or personal information to the Unpaid Services." Nunca use a camada gratuita com documento de titular — antes de qualquer análise de LGPD, isso contraria o contrato do fornecedor.
Na camada paga, os termos afirmam que o Google não usa prompts nem respostas para melhorar produtos, e processa sob aditivo de tratamento de dados. No Vertex AI, a restrição de treino é contratual e explícita. Duas ressalvas: o prazo de retenção da camada paga é descrito apenas como "limited period of time", sem número publicado; e o que separa gratuito de pago é existir faturamento ativo no projeto, não o valor da fatura. Como a credencial é sua, essa relação contratual é sua — vantagem, porque você controla o instrumento; responsabilidade, porque ninguém a assume por você.
Preciso mesmo das cláusulas-padrão da ANPD?
Só se você processar fora do Brasil e fora da União Europeia. Desde a Resolução nº 32, de 26 de janeiro de 2026, a ANPD reconhece a União Europeia como organismo com grau de proteção adequado, o que permite usar o art. 33, I — mecanismo simples, sem contrato específico. Os Estados Unidos não foram reconhecidos, então processar lá exige as cláusulas-padrão da Resolução nº 19/2024, adotadas integralmente e sem alteração, cujo prazo de adequação venceu em agosto de 2025. Processar em São Paulo, pela configuração da §14, dispensa tudo isso.
Isso serve para detectar documento falso?
Não, e é importante ser categórico. O building block lê o que está escrito e acredita. Um RG adulterado é lido normalmente, com confiança alta. Para verificação de autenticidade, biometria ou prova de vida, o mercado tem fornecedores especializados — Unico, Idwall, Caf, BigDataCorp. Este building block é complementar a eles, não substituto.
Preciso de template?
Não, mas ajuda muito. Sem template, a extração é genérica: o modelo decide o que é relevante e você recebe o que ele achou. Com template, você declara os campos, os tipos e as descrições — e as descrições são o que mais melhora a qualidade. Sem template também não há mapeamento, então mappedData fica igual a rawData.
Qual a diferença entre retry e remap?
retry reprocessa desde o início e chama o modelo de novo, gastando token. Serve quando a extração falhou. remap reaplica os mapeamentos sobre o dado já extraído, sem chamar o modelo e sem custo. Serve quando a extração deu certo e você errou a tradução. Se a dúvida é qual usar, a pergunta é: o problema está no que foi lido, ou no que foi traduzido?
Quanto custa uma extração?
O custo de IA é do provedor e é faturado direto a você, porque a credencial é sua. O que o building block faz é registrar tokensUsed e providerModel em cada extração, para que você acompanhe. O consumo depende do tamanho da imagem, do número de campos declarados no template e do modelo escolhido. O preço do building block em si está em definição.
Posso usar isto para digitalizar um acervo de documentos antigos?
Tecnicamente sim, mas é o uso errado. Não há processamento em lote, não há paralelismo configurável e cada documento é uma chamada a um modelo de visão — o que é caro para volume de digitalização em massa. Para acervo, um OCR tradicional custa uma fração. Este building block foi feito para leitura estruturada dentro de um fluxo de negócio, não para digitalização.
Como faço para apagar os dados de um titular que pediu eliminação?
Manualmente, hoje. Não há rota de exclusão de extração, não há exclusão lógica no registro e não há expurgo automático do arquivo no S3 (§15). O procedimento envolve remover as linhas de extraction_records e os objetos correspondentes em extraction/{organização}/{extração}/source. É a limitação mais relevante para quem opera sob obrigação de eliminação, e está no roadmap.
Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md