BaaS
BetaContas, Pix e TED por API, com o banco parceiro trocável sem reescrever a integração
Você integra uma vez e passa a abrir contas, movimentar Pix e enviar TED. O banco parceiro que carrega a licença fica atrás de uma configuração — trocar de provedor deixa de ser um projeto e vira uma linha no banco de dados.
- Fintechs de crédito que precisam de conta escrow e liquidação por Pix sem virar instituição autorizada
- Marketplaces e plataformas B2B que recebem em nome de terceiros e precisam de conciliação por conta
- Empresas que já operam com um provedor bancário e querem reduzir o risco de depender de um só
- Integração direta e artesanal com a API de cada banco parceiro
- Camada caseira de tradução entre o formato do provedor e o modelo da sua aplicação
- Tabela própria de contas, transações e chaves Pix reimplementada dentro de cada produto
- Uma licença bancária — a Catalisa não é instituição autorizada pelo BACEN (ver §14)
- Um ledger contábil ou sistema de conciliação financeira da sua operação
- Um adquirente, gateway de cartão ou emissor de cartão
- Um sistema de antifraude, PLD/FT ou monitoramento transacional
32 endpoints em 6 recursos.
Resumo executivo
O BaaS é a camada que dá à sua aplicação uma conta bancária de verdade sem que você precise virar um banco. Você chama uma API para abrir conta, consultar saldo, registrar chave Pix, enviar Pix e TED, gerar QR Code e receber os eventos de volta. Quem executa a operação bancária é um provedor licenciado pelo Banco Central — Celcoin, QI Tech ou FitBank — e quem esconde as diferenças entre eles é este building block.
Na prática isso significa que uma financeira consegue abrir uma conta escrow por operação de crédito, liquidar o desembolso por Pix e conciliar o recebimento pelo endToEndId, tudo com o mesmo contrato de API. E significa também que trocar de banco parceiro seis meses depois é criar uma configuração nova e apontar as contas seguintes para ela — não é um projeto de reintegração.
Está deployado nas stacks de staging e produção desde janeiro de 2026, com a esteira completa exercitada de ponta a ponta contra o provedor simulado MockBank. O status é beta, e não produção, de propósito: as três integrações com provedores reais estão implementadas e testadas com HTTP mockado, mas este repositório não guarda evidência de homologação contra os ambientes de sandbox reais desses provedores. §15 detalha exatamente o que falta.
| Atributo | Valor |
|---|---|
| Identificador | baas |
| Categoria | Financeiro |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3015 |
| Path alias | @baas |
| Prefixo HTTP | /baas |
| Schema no banco | baas |
| Status | Beta — deployado desde 2026-01 |
| Depende de | PostgreSQL, Redis, IAM, Webhooks Engine |
O problema
negócioO cenário. Uma empresa quer movimentar dinheiro dentro do próprio produto: abrir conta para o cliente, receber por Pix, pagar fornecedor por TED, manter saldo separado por operação. Ela não quer — e na maioria dos casos não pode — pedir autorização ao Banco Central para isso. Então contrata um provedor bancário licenciado e integra a API dele.
O que trava hoje.
- Cada provedor inventa o próprio dialeto. A Celcoin fala REST com endpoints por recurso e status em texto. O FitBank fala RPC: um único endpoint
/main/execute, um campoMethodno corpo e status como número de 0 a 6. A QI Tech assina cada requisição com RSA. São três modelos mentais diferentes para a mesma operação de "manda um Pix". - O provedor vira parte da sua arquitetura. Quando o mapeamento entre o formato do banco e o seu domínio está espalhado pelos serviços, o provedor deixa de ser fornecedor e vira dependência estrutural. A conversa comercial de renovação acontece com você sem alternativa.
- Trocar de banco é reescrever. A migração não é só de API: é de identificador de conta, de formato de status, de semântica de webhook. Times estimam meses e adiam a decisão, que é exatamente o que o incumbente precisa.
- Não dá para testar sem contrato. Sem sandbox liberado, o time não consegue nem começar. A esteira de produto fica bloqueada por um processo comercial e de compliance que leva semanas.
- A conciliação é problema seu, mas o risco também. Quando os saldos que você mostra ao cliente e os que o banco parceiro registra divergem, quem responde ao cliente é você.
O custo de não resolver. O último item não é hipótese. Em 2024 a Synapse Financial Technologies — que operava exatamente como camada entre fintechs e bancos parceiros nos EUA — entrou em Chapter 11. Havia US$ 219 milhões nas contas dos bancos parceiros, e o caixa batia com os registros do Federal Reserve. O que não batia era o registro de quem era dono de quanto, que só existia no sistema da Synapse. Mais de cem mil pessoas ficaram sem acesso ao próprio dinheiro, e a curadoria nomeada pelo tribunal estimou entre US$ 65 e US$ 95 milhões de descasamento antes de concluir, em abril de 2025, que reconciliar custava mais do que o espólio tinha.
O FDIC foi direto ao ponto no alerta que publicou em 31/05/2024: "FDIC deposit insurance does not protect against the insolvency or bankruptcy of a nonbank company" — e acrescentou que é preciso manter registros que identifiquem quem é dono do dinheiro e quanto cada pessoa tem. A lição prática: numa arquitetura BaaS, saber quem carrega a licença e onde mora a titularidade não é detalhe jurídico, é desenho de sistema. §4 detalha o caso e §14 mostra o que o regulador brasileiro fez com essa lição.
Proposta de valor
negócio| Antes | Depois |
|---|---|
| Uma integração artesanal por provedor bancário | Uma API canônica; o provedor é configuração da organização |
| Trocar de banco parceiro é projeto de meses | Criar uma configuração nova e apontar as contas seguintes para ela |
| Só dá para começar depois do contrato com o banco | MockBank simula conta, saldo, DICT, Pix e QR Code desde o primeiro dia |
| Cada provedor exige um formato de webhook diferente | Todos os webhooks viram o mesmo conjunto de 18 eventos internos |
| Credenciais de banco em variável de ambiente | Credenciais cifradas em AES-256-GCM no banco, por organização |
O provedor é dado, não código. BaaSProviderConfig guarda o tipo do provedor e as credenciais cifradas. createBaaSProvider instancia a estratégia certa em tempo de execução. Nenhum serviço do BaaS sabe o nome de um banco.
Migração gradual, não virada de chave. Cada BaaSAccount guarda o configId a que pertence, e o provedor é resolvido a partir dele a cada operação. Isso permite que contas em provedores diferentes convivam durante a transição — as novas nascem no provedor novo, as antigas continuam funcionando onde estão. (Sobre manter dois contratos simultâneos em regime permanente, veja a ressalva do art. 6º da Resolução Conjunta nº 16/2025 em §14.)
A esteira inteira é testável sem contrato. O MockBank não é um stub de resposta: é um banco simulado com saldo real, transferência interna entre contas, geração de chave EVP, diretório de chaves, QR Code em formato EMV do BR Code e disparo de webhook. O que você prova no MockBank é o mesmo fluxo que vai rodar em produção.
Credenciais tratadas como segredo. As credenciais do provedor são cifradas com AES-256-GCM antes de tocar o banco e nunca voltam em nenhuma resposta de API — toResponseData simplesmente não as inclui.
Casos de uso reais
negócioCaso 1 — Uma financeira separa o dinheiro de cada operação de crédito Cenário ilustrativo
Financeira de crédito com garantia que desembolsa para o cliente e recebe parcelas de volta. Cada operação precisa de segregação de recursos, porque o dinheiro em trânsito não é dela.
Com uma conta única no banco parceiro, a separação virava planilha. Descobrir quanto do saldo pertencia a qual operação exigia cruzar extrato com sistema, e o processo era o mesmo tanto para fechar o mês quanto para responder a uma dúvida de cliente. O erro não aparecia no dia; aparecia na conciliação.
Cada operação abre uma conta pelo POST /baas/api/v1/accounts, com businessId carregando o identificador da operação no sistema de crédito e personId apontando para a pessoa no Customers. O desembolso sai por POST /baas/api/v1/pix/transfers. O recebimento entra como transação PIX_IN criada pelo handler de webhook, já vinculada à conta certa. GET /baas/api/v1/pix/transfers?filter[accountId]=... devolve o extrato daquela operação.
flowchart LR OP["Operação de crédito OP-2026-00841"] ACC["POST /accounts<br/>businessId = OP-2026-00841<br/>personId aponta para Customers"] OUT["POST /pix/transfers<br/>desembolso ao cliente"] IN["Webhook pix.received<br/>handler cria transação PIX_IN na conta certa"] EXT["GET /pix/transfers?filter[accountId]=...<br/>extrato daquela operação"] OP --> ACC ACC --> OUT ACC --> IN OUT --> EXT IN --> EXT
A pergunta "quanto desta operação já voltou" deixa de ser um cruzamento manual e vira uma chamada de API filtrada por conta. E como businessId está na transação, o vínculo com o sistema de crédito sobrevive à conciliação.
Caso 2 — Um marketplace cobra por QR Code e reconhece o pagamento sozinho Cenário ilustrativo
Marketplace B2B que fatura pedidos de lojistas e quer receber por Pix sem obrigar o comprador a copiar dados bancários.
O fluxo anterior era boleto com dois dias de compensação e uma rotina de baixa que rodava de madrugada. O comprador pagava, o pedido ficava travado até o dia seguinte, e o atendimento passava a manhã respondendo "já paguei".
No fechamento do pedido, o marketplace chama POST /baas/api/v1/pix/qr-codes/generate com amount, description e expiresInMinutes, e recebe o emvCode (copia e cola) mais o QR em base64. Quando o comprador paga, o provedor manda o webhook pix.received para POST /baas/api/v1/webhooks/{provedor}; o handler cria a transação PIX_IN e publica baas.pix.received no barramento de eventos. O Webhooks Engine entrega esse evento ao endpoint do marketplace.
sequenceDiagram autonumber participant M as Marketplace participant B as BaaS participant P as Provedor participant C as Comprador participant W as Webhooks Engine M->>B: POST /pix/qr-codes/generate com amount, description, expiresInMinutes B-->>M: emvCode copia e cola + QR em base64 M->>C: mostra o QR no fechamento do pedido C->>P: paga o Pix P->>B: POST /webhooks/celcoin com pix.received B->>B: cria transação PIX_IN B->>W: publica baas.pix.received W->>M: entrega no endpoint do marketplace M->>M: libera o pedido
O pedido é liberado no momento do pagamento, não no dia seguinte. E a baixa deixa de depender de uma rotina noturna — é o evento que dispara a mudança de estado.
Caso 3 — Uma operação troca de banco parceiro sem parar o produto Cenário ilustrativo
Empresa que já opera com um provedor bancário há um ano e decide diversificar, seja por custo, por SLA ou por não querer depender de um fornecedor só.
Na integração direta, isso é uma virada de chave: reescrever o cliente HTTP, remapear status, refazer o parsing de webhook, migrar todas as contas de uma vez e rezar. O risco concentrado num fim de semana é justamente o motivo pelo qual a decisão é adiada por anos.
Cria-se uma segunda BaaSProviderConfig com o novo provedor, valida com POST /baas/api/v1/provider-configs/:id/test, e marca como padrão com POST /baas/api/v1/provider-configs/:id/set-default. As contas novas nascem no provedor novo; as antigas continuam no antigo, porque cada BaaSAccount guarda o próprio configId e o serviço resolve o provedor a partir dele a cada operação. Os dois convivem pelo tempo que for preciso.
flowchart LR
subgraph ANTES["Integração direta — virada de chave"]
A1["Reescrever cliente HTTP"] --> A2["Remapear status"] --> A3["Refazer parsing de webhook"] --> A4["Migrar todas as contas num fim de semana"]
end
subgraph DEPOIS["Catalisa BaaS — curva"]
D1["POST /provider-configs — config nova"] --> D2["POST /provider-configs/:id/test"] --> D3["POST /provider-configs/:id/set-default"]
D3 --> NOVAS["Contas novas nascem no provedor novo"]
D3 --> VELHAS["Contas antigas seguem no antigo<br/>cada BaaSAccount guarda o próprio configId"]
endA migração vira uma curva, não um degrau. E o poder de barganha na renovação muda de lado, porque a alternativa deixa de ser teórica.
Caso 4 — O que acontece quando a titularidade só existe no middleware Referência de mercado
A Synapse Financial Technologies operava nos EUA como intermediária entre cerca de 100 fintechs (Yotta, Juno, Copper) e quatro bancos parceiros (Evolve, AMG National Trust, Lineage, American Bank). O desenho: no banco havia uma conta omnibus "FBO" — for benefit of — com o dinheiro de milhares de pessoas misturado. O banco enxergava um saldo agregado. Quem era dono de quanto existia apenas no ledger da Synapse, e um programa de sweep espalhava os depósitos entre vários bancos para multiplicar a cobertura de US$ 250 mil do FDIC.
Em 11/05/2024 a Synapse cortou o acesso do banco parceiro ao painel e parou de enviar o razão diário. Os bancos congelaram tudo no mesmo dia. Havia US$ 219.077.467 nas contas FBO, e o caixa reconciliava com os registros do Federal Reserve — o dinheiro estava lá. O que não fechava era o mapa de titularidade: o trial balance final gerado pela Synapse atribuía a um dos bancos um saldo diferente do saldo real, e como o dinheiro de um mesmo usuário havia circulado por várias instituições, nenhum banco tinha como reconstruir de quem era o quê — nenhum jamais teve esses dados. A curadoria estimou de US$ 65 a US$ 95 milhões de descasamento; cerca de US$ 192 milhões (87%) foram devolvidos até janeiro de 2025, e em abril de 2025 a reconciliação foi abandonada por custar mais que o espólio. O caso foi extinto em 12/11/2025, com autorização para destruir os registros remanescentes.
Quatro escolhas de desenho, todas verificáveis no código. Primeiro: a Catalisa nunca é depositária. O dinheiro fica no provedor licenciado; BaaSAccount.externalId é o identificador da conta no provedor. Segundo: não existe conta omnibus. Cada BaaSAccount corresponde a uma conta individual aberta no provedor, com agência, número e dígito próprios — a titularidade mora lá, não aqui. Terceiro: o saldo local é cache explícito, e GET /accounts/:id/balance sempre consulta a origem antes de responder. Quarto: o endToEndId do Pix é persistido e indexado, dando uma chave de conciliação que os dois lados reconhecem.
flowchart LR
subgraph SYN["Modelo Synapse — titularidade no middleware"]
S1["Milhares de usuários finais"] --> S2["Ledger da Synapse<br/>ÚNICO lugar que sabia quem é dono de quanto"]
S2 --> S3["Conta omnibus FBO no banco parceiro<br/>o banco enxerga só o saldo agregado"]
S3 --> S4["Acesso cortado em 11/05/2024 →<br/>nenhum banco conseguiu reconstruir a titularidade"]
end
subgraph CAT["Modelo Catalisa — titularidade na instituição licenciada"]
C1["BaaSAccount = uma conta individual no provedor<br/>agência, número e dígito próprios"]
C2["externalId aponta para a conta NO provedor"]
C3["Saldo local é cache explícito;<br/>GET /accounts/:id/balance consulta a origem"]
C4["endToEndId persistido e indexado<br/>chave de conciliação que os dois lados reconhecem"]
C1 --> C2 --> C3 --> C4
endA pergunta "de quem é este saldo" tem resposta única, e ela mora na instituição licenciada. Não é coincidência que o regulador brasileiro tenha chegado à mesma conclusão: a Resolução Conjunta nº 16/2025 exige, no art. 4º, §2º, que as contas sejam de titularidade do cliente na instituição prestadora — o que torna o modelo de conta bolsão inviável como arquitetura de BaaS no Brasil (§14).
Fontes. Valores e datas vêm dos autos da falência (In re Synapse Financial Technologies, Bankr. D. Del.), em especial os relatórios da trustee. Reação regulatória: alerta do FDIC ao consumidor sobre contas em fintechs, que originou a proposta de regra Recordkeeping for Custodial Accounts (RIN 3064-AG07, 02/10/2024) exigindo reconciliação diária e identificação do beneficiário final — proposta que até 16/08/2026 não foi finalizada; e a ação de enforcement do Federal Reserve contra o Evolve Bank & Trust, de 14/06/2024.
Mercado e diferenciais
negócioPanorama. O mercado brasileiro de BaaS tem duas camadas que costumam ser confundidas na conversa comercial. Na primeira estão as instituições autorizadas pelo Banco Central — Celcoin (IP e SCD), QI Tech (SCD), Dock (IP e participante direto do Pix, código 301), Fits/FitBank (IP). São elas que carregam a licença, respondem ao regulador e custodiam recurso. Na segunda estão as camadas técnicas que integram uma ou mais dessas instituições e entregam uma API ao produto final. A Catalisa está, sem ambiguidade, na segunda camada.
Nenhum dos provedores brasileiros consultados publica tabela de preço; todos direcionam para o comercial (consulta em 2026-08-16 aos sites de Celcoin, QI Tech, Dock e Fits). Isso significa que a comparação de custo entre eles só existe dentro de uma negociação — o que, por si só, é um argumento a favor de ter mais de um contrato possível.
No exterior o padrão é o mesmo, e a última rodada de crises deixou lições que valem para quem compra aqui. A Unit declara em rodapé que "is a financial technology company and not a bank", com os serviços bancários prestados por bancos parceiros membros do FDIC. Unit e Treasury Prime anunciaram o mesmo pivô para contrato direto banco↔fintech com semanas de diferença, em fevereiro e março de 2024 — a Treasury Prime foi explícita ao dizer que "os reguladores esperam que as instituições bancárias, não intermediários, supervisionem diretamente as relações com fintechs" (Treasury Prime). Do outro lado do espectro, ter licença própria não imunizou ninguém: a Solaris, banco alemão pleno, opera sob monitor especial da BaFin desde 2022 e precisou de um resgate de €140 milhões em fevereiro de 2025; a Railsr passou por pre-pack administration em março de 2023, teve a licença de EMI da subsidiária lituana revogada e hoje não existe mais como marca, absorvida pela Equals. A conclusão útil para o comprador: licença própria troca risco de contraparte por risco de capital e supervisão — não elimina risco.
A escala do problema. O Pix processou 71,3 bilhões de transações e R$ 29,61 trilhões em 2025, e em julho de 2026 sozinho foram 7,24 bilhões de transações e R$ 3,22 trilhões. Em 31/07/2026 havia 987,5 milhões de chaves cadastradas e 871 participantes distintos no DICT (série calculada a partir da API de dados abertos do BACEN, consultada em 16/08/2026). Já o tamanho do mercado de BaaS no Brasil não tem número confiável: o BCB não publica a estatística, e a base cadastral que permitirá medi-la só produz efeitos em 01/09/2026. A cifra que circula em material comercial vem de um estudo encomendado por um participante do próprio mercado — por isso não a citamos aqui.
O Brasil regulou BaaS especificamente, e o prazo está correndo. A Resolução Conjunta nº 16, de 28/11/2025 (CMN + BCB) trata da prestação de Banking as a Service, com escopo taxativo, regra de exclusividade, alocação de responsabilidade de PLD e KYC à instituição prestadora, e poder do BCB de determinar o encerramento de contratos. Os contratos vigentes têm até 31/12/2026 para se adequar. Em paralelo, a Resolução BCB nº 517/2025 lista expressamente a prestação de BaaS entre os serviços que acrescem R$ 5 milhões ao capital regulatório mínimo da prestadora. Ser o provedor licenciado ficou mais caro em 2025 — o que reforça a tese deste building block: você quer usar um, não virar um.
E o risco jurídico do lado do provedor aumentou. Em 17/06/2026 o TJSP manteve a QI Tech SCD em execução ligada a uma pirâmide financeira aplicando a teoria menor da desconsideração da personalidade jurídica, que permite alcançar provedores de infraestrutura de pagamento sem prova de fraude própria (Times Brasil). Combinado com o art. 10 da Resolução Conjunta nº 16/2025, que aloca à prestadora a responsabilidade por KYC, antifraude e PLD/FT mesmo quando as tarefas são delegadas, isso significa que o seu provedor vai apertar o onboarding e o monitoramento. Planeje a esteira de KYC contando com isso, não com o contrário.
| Critério | Catalisa BaaS | Celcoin | QI Tech | Dock | Unit (EUA) |
|---|---|---|---|---|---|
| Carrega licença do BACEN | Não — camada técnica | Sim (IP + SCD) | Sim (SCD) | Sim (IP, part. direto Pix) | Não (bancos parceiros, EUA) |
| Custodia recurso de cliente | Não | Sim | Sim | Sim | Não |
| Provedor trocável sem reescrever | Sim, por configuração | Não se aplica | Não se aplica | Não se aplica | Não |
| Mais de um provedor simultâneo | Sim, por organização | Não | Não | Não | Não |
| Ambiente de teste sem contrato | Sim (MockBank completo) | Sandbox após contrato | Sandbox após contrato | Sandbox após contrato | Sandbox |
| Pix, TED e DICT | Sim, via provedor | Sim | Sim | Sim | Não (mercado EUA) |
| Emissão de cartão | Não | Sim | Sim | Sim | Sim |
| Crédito / originação | Não (é o Decision Platform) | Parcial | Sim | Parcial | Sim |
| Boleto | Não implementado (§15) | Sim | Sim | Sim | Não |
| Preço público | Precificação em definição | Não divulgado | Não divulgado | Não divulgado | Não divulgado |
A pergunta de due diligence que separa os fornecedores é binária. Existe uma entidade autorizada, com CNPJ, no registro do BACEN — ou não? E atenção: a marca comercial e a razão social licenciada nem sempre coincidem. A licença por trás da marca Zoop, por exemplo, é da iFood Pago Instituição de Pagamento S.A., autorizada em outubro de 2023 como emissora de moeda eletrônica; a marca Bankly opera sob a Acesso Soluções de Pagamento S.A., hoje controlada pelo Banco BV. Já a Matera não possui nenhuma autorização — vende software de core banking e motor de Pix para instituições que já têm a própria licença, o que é um posicionamento legítimo e completamente diferente. Confira sempre a entidade que assina o contrato, não o logotipo (§14).
Nossos diferenciais
- O provedor é uma linha no banco de dados.
BaaSProviderConfig.providerTypemais credenciais cifradas é tudo que separa uma organização rodando sobre Celcoin de outra rodando sobre QI Tech. É difícil de copiar não pela técnica, mas porque exige manter três mapeadores completos em dia com três APIs que evoluem sozinhas — custo que só se paga quando ele é diluído por vários clientes. - Coexistência de provedores, não apenas suporte a vários. A distinção importa: suportar N provedores é escolher um por cliente. Aqui
BaaSAccount.configIdamarra cada conta à sua configuração, então duas contas da mesma organização podem viver em bancos diferentes ao mesmo tempo. É o que torna a migração gradual possível. Ressalva regulatória obrigatória: o art. 6º da Resolução Conjunta nº 16/2025 veda contratar BaaS com uma prestadora quando já existe contrato vigente com outra para o mesmo tipo de conta. A capacidade técnica é real e serve à migração; usá-la para manter dois contratos simultâneos do mesmo tipo é uma decisão que passa pelo seu jurídico, não pela engenharia (§14 e §15). - Um banco simulado de verdade, não um mock de resposta. O MockBank tem saldo, débito e crédito entre contas internas, DICT com chave EVP em formato UUID,
endToEndIdno formato de 32 caracteres da especificação, QR Code EMV parseável e disparo de webhook. O time integra e testa a esteira toda antes de o jurídico terminar o contrato. - Isolamento por organização em toda consulta de escrita. Cada serviço compara
organizationIddo token com o do registro antes de agir, e devolve404— não403— quando não bate, para não revelar existência de recurso de outro tenant.
Quando escolher o concorrente.
| Se o problema é… | A resposta certa é |
|---|---|
| Uma conta, um Pix, e nada mais | Integrar direto na Celcoin ou na QI Tech |
| Emissão de cartão como produto central | Dock |
| Crédito com originação, custódia e fundo no mesmo contrato | QI Tech |
| Boleto hoje | Nenhum de nós — não está implementado (§15) |
| Não deixar o banco parceiro virar dependência estrutural | Este building block |
Se você precisa de uma coisa só — uma conta, um Pix, e nada mais — integrar direto na Celcoin ou na QI Tech é mais simples e tem menos peças no caminho; toda abstração cobra um pedágio e o menor denominador comum vale aqui também (o ADR-0003 registra isso como consequência aceita).
Se o produto central é emissão de cartão, a Dock resolve algo que o BaaS não faz e não pretende fazer. Se a exigência é crédito com originação, custódia e fundo no mesmo contrato, a QI Tech entrega uma esteira que aqui exigiria combinar vários building blocks. E se você precisa de boleto hoje, isso não está implementado (§15).
O BaaS ganha quando o problema é não querer que o banco parceiro vire dependência estrutural — e quando a operação é multi-tenant, porque aí a configuração por organização deixa de ser conveniência e vira arquitetura.
Modelo de cobrança e ROI
negócioUnidade de cobrança. Precificação em definição. Não há preço fechado para este building block, e inventar um aqui seria pior do que não ter.
O que dispara custo. Três drivers, e vale entender a natureza de cada um:
| Driver | Por quê |
|---|---|
| Contas ativas | O provedor cobra por conta aberta e mantida; é o custo que cresce com a base |
| Transações Pix e TED processadas | Custo por transação do provedor, repassado |
| Chamadas de API ao provedor | Consulta de saldo, DICT e status batem no provedor a cada requisição (§7 explica por que não há cache) |
Comparação de custo. Não é possível montar tabela comparativa honesta: nenhum dos provedores consultados publica preço (verificado em 2026-08-16 nos sites de Celcoin, QI Tech, Dock e Fits). Toda comparação de custo entre eles acontece dentro de uma proposta comercial específica. O que dá para comparar é a estrutura de custo:
| Integração direta com um provedor | Catalisa BaaS | |
|---|---|---|
| Custo transacional | Do provedor, negociado | Do provedor, negociado — o mesmo |
| Custo de integração inicial | Uma integração completa por provedor | Uma integração; o provedor entra por configuração |
| Custo de trocar de provedor | Nova integração completa | Nova configuração; contas antigas continuam no provedor antigo |
| Custo de ter dois provedores | Duas integrações completas e mantidas | Duas configurações |
| Custo de testar antes do contrato | Bloqueado até liberação de sandbox | Zero — MockBank |
ROI. O retorno não está na taxa por Pix: essa é do banco parceiro e continua sendo, com ou sem BaaS. Está em duas coisas.
| Linha do ROI | Onde ela aparece | Quanto vale |
|---|---|---|
| Custo de integração que não se repete | Linha de engenharia | Manter dois mapeadores de provedor em produção é trabalho contínuo — aqui ele é diluído entre todos os clientes da plataforma |
| Poder de barganha na renovação | Taxa por transação, todo mês | Não aparece em nenhum orçamento de projeto, e é a maior das duas |
A primeira é o custo de integração que não se repete: manter dois mapeadores de provedor em produção é trabalho contínuo, e aqui ele é diluído entre todos os clientes da plataforma em vez de recair sobre a engenharia de cada um.
A segunda é menos visível e vale mais: um provedor que sabe que você não tem alternativa técnica negocia diferente de um que sabe que você tem. A conta dessa diferença não aparece na linha de engenharia — aparece na taxa por transação, todo mês.
Arquitetura
As cinco camadas
flowchart TD
HTTP["Requisição HTTP"] --> APP
subgraph APP["Hono app — basePath /baas + applyCommonMiddleware"]
direction TB
MW["bodyLimit 1MB · CORS · security headers · rate limit"]
GRD["authMiddleware → requirePermission → requireOrganization"]
EXC["webhookRouter é a exceção — não passa por JWT, ver §14"]
MW --> GRD
MW --> EXC
end
APP -->|"Zod parse → ResultAsync<T, AppError>"| SVC
subgraph SVC["services/"]
direction TB
S1["ProviderConfigService — CRUD de config, cifra credencial, testa"]
S2["AccountService — conta, saldo, KYC, encerramento"]
S3["PixService — chave, DICT, transferência, QR, estorno"]
S4["TedService — transferência TED e consulta"]
S5["WebhookHandlerService — recebe, deduplica, aplica e republica"]
end
SVC -->|"decryptCredentials + createBaaSProvider do tipo"| PRV
subgraph PRV["providers/ — interface BaaSProvider com 24 métodos canônicos"]
direction TB
P1["CelcoinProvider — OAuth2 client_credentials · REST · status em texto"]
P2["FitBankProvider — Basic Auth · RPC /main/execute · status numérico"]
P3["QITechProvider — requisição assinada com RSA · REST"]
P4["MockBankProvider — banco simulado, tabelas mockbank_* no Postgres"]
end
SVC --> REPO["repositories/ com Prisma"]
REPO --> PG[("PostgreSQL — schema baas")]Os cinco routers montados e o que cada um responde:
| Caminho | Router | Rotas |
|---|---|---|
/api/v1/provider-configs | providerConfigRouter | 7 |
/api/v1/accounts | accountRouter | 9 |
/api/v1/pix | pixRouter | 10 |
/api/v1/ted | tedRouter | 3 |
/api/v1/webhooks | webhookRouter | 2 |
/health | — | versão do build |
O caminho de uma transferência Pix
A ordem importa: o provedor confirma antes de a transação existir localmente.
sequenceDiagram autonumber participant C as Cliente participant R as pixRouter participant S as PixService participant CFG as providerConfigRepo participant P as Provedor bancário participant DB as transactionRepo participant EV as eventPublisher C->>R: POST /pix/transfers R->>S: input validado por Zod S->>CFG: busca a config da conta CFG-->>S: credenciais cifradas S->>S: decryptCredentials S->>P: provider.initiatePixTransfer — chamada de rede P-->>S: transação canônica com status S->>DB: transactionRepo.create — só depois S->>EV: publish baas.pix.transfer.initiated
Decisões não óbvias.
- O provedor é instanciado por requisição, não guardado no container.
getProviderForAccountbusca a config, decifra as credenciais e chamacreateBaaSProvidera cada operação. O custo é uma decifragem AES por chamada — irrelevante perto da latência de rede do provedor. O ganho é que trocar credencial ou desativar uma config surte efeito na requisição seguinte, sem reinício e sem invalidação de cache. Num building block financeiro, revogar credencial na hora vale mais que economizar microssegundos. - Saldo não tem cache.
GET /accounts/:id/balancesempre bate no provedor. O campoavailableBalanceno banco é atualizado depois, de forma assíncrona, e existe para listagem — nunca para responder "quanto tem nesta conta". Um saldo desatualizado de 30 segundos numa tela é irritação; num sistema que decide se autoriza uma transferência, é prejuízo. - A transação local só é criada depois que o provedor confirma. A ordem é sempre provedor primeiro, banco depois. Se o provedor falhar, não existe transação órfã em
PENDINGno seu banco esperando conciliação manual. O trade-off aceito é o inverso: se o provedor aceitar e a escrita local falhar, existe uma transação no banco parceiro sem contrapartida local — que é o cenário que a reconciliação porendToEndIdprecisa cobrir (§15). - Tipos canônicos, e não o formato do provedor, atravessam o serviço.
CanonicalAccount,CanonicalPixTransaction,CanonicalBalancee companhia são o contrato interno. Nenhum serviço sabe que o FitBank usaTaxNumbercomo identificador de conta ou que a Celcoin devolve status em texto. A consequência é o menor denominador comum: uma funcionalidade exclusiva de um provedor não é exposta enquanto não couber na interface. O ADR-0003 registra isso como custo aceito. externalIdé o identificador no provedor, e é ele que amarra tudo. A unicidade é(configId, externalId), nãoexternalIdsozinho — porque dois provedores diferentes podem, legitimamente, emitir o mesmo identificador. É a mesma razão pela qual não existe conta "da organização": existe conta "da configuração", e a configuração é que é da organização.- Webhook do provedor não passa por JWT. Quem chama é o banco, que não tem token da sua plataforma. A autenticação é assinatura do provedor, e a organização vem do header
X-Organization-Idou do query paramorganizationIdque o próprio BaaS gera na URL de callback. §14 detalha o modelo de confiança e o que ligar em produção. - O MockBank persiste em tabelas próprias.
mockbank_accounts,mockbank_pix_keys,mockbank_transactionsemockbank_qr_codessão o "banco de dados do banco simulado", separadas das tabelasbaas_*que são o registro da plataforma. A separação é deliberada: o simulador tem que se comportar como um sistema externo, inclusive perdendo e reconciliando estado, senão o teste não prova nada.
Monolito vs. standalone. Em monolito, registerBaas monta tudo no container TypeDI e o MockBank recebe o PrismaClient e o callback de webhook, que simula o POST do provedor chamando o WebhookHandlerService direto. Em standalone — o modo de produção — o BaaS sobe na porta 3015 com MODULE_SELF=baas, e a única dependência de rede é o IAM, via MODULE_IAM_URL, para validação de token. A verificação da assinatura JWT é local em ambos os modos.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Provider config | Configuração de um provedor bancário para uma organização: tipo, credenciais cifradas e ajustes. Uma organização pode ter várias, e uma delas é a padrão. |
| Provider | Implementação da interface BaaSProvider para um banco específico. Instanciada por requisição a partir da config. |
| Tipo canônico | Formato interno neutro (CanonicalAccount, CanonicalPixTransaction, ...) para o qual todo provedor traduz. É o contrato que isola o serviço do banco. |
externalId | Identificador do recurso no provedor. Único por (configId, externalId). É a chave de correlação com o mundo externo. |
endToEndId | Identificador único da transação Pix definido pela especificação do BACEN, com 32 caracteres. É a chave de conciliação entre o seu registro e o do banco. |
| DICT | Diretório de Identificadores de Contas Transacionais — o cadastro de chaves Pix do Banco Central. POST /pix/keys/lookup consulta quem é dono de uma chave. |
| Chave EVP | Chave Pix aleatória, em formato UUID. É a única que o BaaS gera sem você informar valor. |
| EMV / BR Code | Formato do payload de QR Code Pix. O campo emvCode é o "copia e cola". |
| KYC | Verificação de identidade do titular. O BaaS encaminha documentos ao provedor e reflete o status; a análise é do provedor. |
| MockBank | Provedor simulado, com estado próprio no Postgres. Para desenvolvimento e teste — nunca para produção. |
Modelo de dados — schema baas no PostgreSQL. Cinco modelos de domínio e quatro do simulador.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
BaaSProviderConfig | baas.baas_provider_configs | Provedor configurado para a organização | providerType, credentials (cifrado), isDefault, isActive, settings, único (organizationId, name) |
BaaSAccount | baas.baas_accounts | Conta bancária mantida no provedor | externalId, configId, status, bankCode, branchNumber, accountNumber, availableBalance, personId, businessId, único (configId, externalId) |
BaaSPixKey | baas.baas_pix_keys | Chave Pix registrada para uma conta | keyType, keyValue, status, deletedAt, único (accountId, keyType, keyValue) |
BaaSTransaction | baas.baas_transactions | Transação Pix ou TED, entrada ou saída | transactionType, status, amount Decimal(15,2), endToEndId, isCredit, originalTransactionId, único (accountId, externalId) |
BaaSWebhookEvent | baas.baas_webhook_events | Evento cru recebido do provedor | eventType, externalEventId, payload, processed, error, único (configId, externalEventId) |
MockBankAccount | baas.mockbank_accounts | Estado interno do banco simulado | externalId, saldo, dados do titular, kycStatus |
MockBankPixKey | baas.mockbank_pix_keys | DICT do banco simulado | keyValue único globalmente |
MockBankTransaction | baas.mockbank_transactions | Extrato do banco simulado | endToEndId, type, status |
MockBankQrCode | baas.mockbank_qr_codes | QR Codes emitidos pelo banco simulado | emvCode, expiresAt |
O
único (configId, externalEventId)embaas_webhook_eventsé o que dá idempotência ao recebimento de webhook: o mesmo evento reentregue pelo provedor é detectado e descartado antes de ser aplicado.
Enumerações
| Enum | Valores |
|---|---|
BaaSProviderType | CELCOIN · FITBANK · QITECH · MOCKBANK |
BaaSAccountStatus | PENDING_KYC · KYC_IN_PROGRESS · KYC_REJECTED · ACTIVE · BLOCKED · CLOSED |
BaaSTransactionType | PIX_IN · PIX_OUT · TED_IN · TED_OUT · REVERSAL |
BaaSTransactionStatus | PENDING · PROCESSING · COMPLETED · FAILED · CANCELLED · REVERSED |
BaaSPixKeyType | CPF · CNPJ · EMAIL · PHONE · EVP |
BaaSPixKeyStatus | PENDING · ACTIVE · INACTIVE |
KycStatus | NOT_STARTED · PENDING · IN_PROGRESS · APPROVED · REJECTED · REQUIRES_UPDATE |
Como as nove tabelas se ligam
erDiagram
BaaSProviderConfig ||--o{ BaaSAccount : "abre contas em"
BaaSProviderConfig ||--o{ BaaSWebhookEvent : "recebe eventos de"
BaaSAccount ||--o{ BaaSPixKey : "registra"
BaaSAccount ||--o{ BaaSTransaction : "movimenta"
BaaSTransaction ||--o| BaaSTransaction : "originalTransactionId no estorno"
BaaSProviderConfig {
string providerType "CELCOIN FITBANK QITECH MOCKBANK"
string credentials "cifrado AES-256-GCM"
bool isDefault
bool isActive
}
BaaSAccount {
string externalId "id no provedor"
string configId
string status
decimal availableBalance "cache para listagem"
string personId "Customers, sem FK"
string businessId
}
BaaSPixKey {
string keyType
string keyValue
string status
}
BaaSTransaction {
string transactionType "PIX_IN PIX_OUT TED_IN TED_OUT REVERSAL"
string status
decimal amount "Decimal 15,2"
string endToEndId "chave de conciliação"
bool isCredit
}
BaaSWebhookEvent {
string eventType
string externalEventId "dedupe por configId"
json payload
bool processed
}Ciclo de vida da conta
stateDiagram-v2 [*] --> PENDING_KYC : POST /accounts PENDING_KYC --> KYC_IN_PROGRESS : KYC enviado KYC_IN_PROGRESS --> ACTIVE : aprovado KYC_IN_PROGRESS --> KYC_REJECTED : reprovado KYC_REJECTED --> KYC_IN_PROGRESS : webhook kyc.requires_update ACTIVE --> BLOCKED : webhook account.blocked BLOCKED --> CLOSED : POST /accounts/:id/close ou webhook account.closed ACTIVE --> CLOSED : POST /accounts/:id/close ou webhook account.closed CLOSED --> [*] : terminal
Atenção. Quem move o estado é o provedor. O BaaS reflete, via resposta de submitKyc ou via webhook. Encerrar é a única transição que a sua aplicação inicia — e ela também passa pelo provedor antes de gravar. CLOSED é terminal: fechar de novo devolve 400.
Como o status de KYC do provedor vira status de conta — a regra é a mesma em AccountService.submitKyc, na consulta de KYC e no WebhookHandlerService:
| Status de KYC no provedor | Status gravado na conta |
|---|---|
APPROVED | ACTIVE |
REJECTED | KYC_REJECTED |
Qualquer outro — NOT_STARTED, PENDING, IN_PROGRESS, REQUIRES_UPDATE | KYC_IN_PROGRESS |
Atenção. PENDING_KYC é escrito apenas pelos mapeadores de provedor, quando a conta nasce ou quando o provedor devolve esse status na criação. Nenhum webhook e nenhuma chamada de KYC volta a conta para PENDING_KYC: o evento kyc.requires_update cai no ramo default do handler e grava KYC_IN_PROGRESS, publicando baas.kyc.submitted. Se a sua interface exibe "documento precisa ser reenviado", leia o campo rejectedFields da consulta de KYC, não o status da conta.
Máquina de estados da transação — vale para Pix e TED, saída e entrada.
stateDiagram-v2 [*] --> PENDING : POST /pix/transfers ou POST /ted PENDING --> PROCESSING : webhook transfer.created PROCESSING --> COMPLETED : webhook transfer.completed PROCESSING --> FAILED : webhook transfer.failed COMPLETED --> REVERSED : refund, só de PIX_IN FAILED --> [*] : terminal REVERSED --> [*] : terminal CANCELLED --> [*] : terminal, sem transição no código
| Detalhe | O que acontece |
|---|---|
| Status inicial | É o que o provedor devolveu em provider.initiate*Transfer() — não é fixado pelo BaaS |
POST /transfers/:id/refund | Só de PIX_IN e só se COMPLETED. Leva a original a REVERSED e cria uma transação nova REVERSAL, ligada pelo campo originalTransactionId |
Entrada (PIX_IN, TED_IN) | Nasce direto em COMPLETED: é criada pelo handler ao receber o webhook pix.received / ted.received — ou seja, o dinheiro já entrou quando o registro aparece |
CANCELLED | Terminal, mas nenhum caminho do código leva a ele — está reservado para status vindo do provedor (§15) |
GET /pix/transfers/:id e GET /ted/:id | Não são consultas passivas: chamam o provedor e gravam o status novo se ele mudou. É o caminho de reconciliação para quando um webhook se perde |
Eventos publicados no barramento — 18 eventos, consumíveis pelo Webhooks Engine com o filtro baas.*.
| Grupo | Eventos |
|---|---|
| Conta | baas.account.created · baas.account.updated · baas.account.closed · baas.account.blocked |
| KYC | baas.kyc.submitted · baas.kyc.approved · baas.kyc.rejected |
| Pix | baas.pix.key.created · baas.pix.key.deleted · baas.pix.transfer.initiated · baas.pix.transfer.completed · baas.pix.transfer.failed · baas.pix.received · baas.pix.refunded |
| TED | baas.ted.transfer.initiated · baas.ted.transfer.completed · baas.ted.transfer.failed · baas.ted.received |
Payload de baas.pix.received e baas.ted.received. O BaaS não expõe rota para ler uma transação, então esses dois eventos carregam a própria transação criada (PIX_IN / TED_IN) — quem consome não precisa de outra chamada para saber o valor e o pagador. Além de organizationId, accountId e transactionId (inalterados), vão: amount (número), transactionType (PIX_IN ou TED_IN), externalId, counterpartyName, counterpartyDocument, counterpartyPixKey (só Pix; null em TED), counterpartyBankCode, counterpartyBranch, counterpartyAccount, description, endToEndId (só Pix; null em TED) e processedAt (ISO 8601). Campo que o provedor não mandou vem como null.
Referência da API
Prefixo: /baas. Em standalone, a base é https://baas.bb.stg.catalisa.app.
Todas as rotas de negócio — as 29 fora de /webhooks — exigem, nesta ordem: authMiddleware (Bearer JWT válido), requirePermission(...) e requireOrganization (o token precisa carregar organizationId, senão 403). As permissões são apenas duas: BAAS_READ para leitura e BAAS_ADMIN para qualquer escrita.
Prefixo duplicado. Todo router está montado duas vezes: em
/baas/api/v1/{recurso}(canônico) e em/baas/api/v1/baas/{recurso}(compatibilidade com clientes e testes antigos). Use o canônico em código novo; o segundo existe e funciona, mas não é o caminho preferencial.
Configurações de provedor — /baas/api/v1/provider-configs
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /baas/api/v1/provider-configs | Cria config, testa a conexão e cifra as credenciais. 201 | BAAS_ADMIN |
GET | /baas/api/v1/provider-configs | Lista, paginado. Filtros filter[providerType], filter[isActive] | BAAS_READ |
GET | /baas/api/v1/provider-configs/:id | Busca por ID | BAAS_READ |
PATCH | /baas/api/v1/provider-configs/:id | Atualiza. Se enviar credentials, testa antes de gravar | BAAS_ADMIN |
DELETE | /baas/api/v1/provider-configs/:id | Exclusão lógica. 204 | BAAS_ADMIN |
POST | /baas/api/v1/provider-configs/:id/set-default | Marca como padrão e desmarca as outras | BAAS_ADMIN |
POST | /baas/api/v1/provider-configs/:id/test | Testa a conexão com as credenciais gravadas | BAAS_ADMIN |
Contas — /baas/api/v1/accounts
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /baas/api/v1/accounts | Abre conta no provedor e registra localmente. 201 | BAAS_ADMIN |
GET | /baas/api/v1/accounts | Lista, paginado. Filtros filter[status], filter[personId], filter[configId] | BAAS_READ |
GET | /baas/api/v1/accounts/:id | Busca por ID | BAAS_READ |
PATCH | /baas/api/v1/accounts/:id | Atualiza titular no provedor e/ou metadata local | BAAS_ADMIN |
POST | /baas/api/v1/accounts/:id/close | Encerra a conta. Exige reason | BAAS_ADMIN |
GET | /baas/api/v1/accounts/:id/balance | Saldo consultado no provedor a cada chamada | BAAS_READ |
POST | /baas/api/v1/accounts/:id/kyc | Envia documentos de KYC | BAAS_ADMIN |
GET | /baas/api/v1/accounts/:id/kyc | Consulta o status de KYC no provedor | BAAS_READ |
POST | /baas/api/v1/accounts/:id/sandbox/deposit | Só MockBank. Credita saldo para teste | BAAS_ADMIN |
Pix — /baas/api/v1/pix
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /baas/api/v1/pix/keys | Registra chave Pix. 201 | BAAS_ADMIN |
GET | /baas/api/v1/pix/keys | Lista chaves. Exige ?accountId=, senão 400 | BAAS_READ |
DELETE | /baas/api/v1/pix/keys/:id | Remove a chave no provedor e marca deletedAt. 204 | BAAS_ADMIN |
POST | /baas/api/v1/pix/keys/lookup | Consulta o DICT: quem é dono de uma chave | BAAS_READ |
GET | /baas/api/v1/pix/transfers | Lista transações Pix da organização, com dados da conta | BAAS_READ |
POST | /baas/api/v1/pix/transfers | Inicia transferência Pix. 201 | BAAS_ADMIN |
GET | /baas/api/v1/pix/transfers/:id | Consulta e sincroniza o status com o provedor | BAAS_READ |
POST | /baas/api/v1/pix/transfers/:id/refund | Devolve um Pix recebido. 201 | BAAS_ADMIN |
POST | /baas/api/v1/pix/qr-codes/generate | Gera QR Code estático ou dinâmico | BAAS_ADMIN |
POST | /baas/api/v1/pix/qr-codes/decode | Decodifica um código EMV | BAAS_READ |
Filtros de GET /pix/transfers: page[number], page[size], filter[accountId], filter[configId], filter[status], filter[direction] (in ou out), filter[startDate], filter[endDate].
TED — /baas/api/v1/ted
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /baas/api/v1/ted | Inicia transferência TED. 201 | BAAS_ADMIN |
GET | /baas/api/v1/ted | Lista transações TED. Aceita ?accountId= ou os mesmos filtros do Pix | BAAS_READ |
GET | /baas/api/v1/ted/:id | Consulta e sincroniza o status com o provedor | BAAS_READ |
GET /tedmuda de comportamento conforme o filtro: comaccountIde semconfigId, devolve a listagem por conta; caso contrário, devolve a listagem global com os dados da conta embutidos em cada item.
Webhooks — /baas/api/v1/webhooks
| Método | Rota | Descrição | Autenticação |
|---|---|---|---|
POST | /baas/api/v1/webhooks/:providerType | Recebe webhook do provedor. :providerType é celcoin, fitbank, qitech ou mockbank | Assinatura do provedor — não usa JWT |
GET | /baas/api/v1/webhooks/health | Sonda simples do subsistema de webhook | Pública |
Saúde
| Método | Rota | Descrição |
|---|---|---|
GET | /baas/health | Identificação e versão do build. Não valida banco nem provedor |
POST /baas/api/v1/provider-configs
Cria a configuração de um provedor. Antes de gravar, o serviço chama testConnection() no provedor — credencial errada falha aqui, não na primeira conta.
Todos os endpoints de escrita aceitam o corpo em dois formatos: direto, ou dentro de data.attributes. Os dois são equivalentes.
Request
{
"name": "Celcoin Produção",
"providerType": "CELCOIN",
"credentials": {
"clientId": "SEU_CLIENT_ID",
"clientSecret": "SEU_CLIENT_SECRET",
"sandbox": "false"
},
"isDefault": true,
"isActive": true,
"clientWebhookUrl": "https://sua-api.exemplo.com.br/webhooks/baas",
"clientWebhookEvents": ["baas.pix.received", "baas.pix.transfer.completed"]
}{
"name": "Celcoin Produção",
"providerType": "CELCOIN",
"credentials": {
"clientId": "SEU_CLIENT_ID",
"clientSecret": "SEU_CLIENT_SECRET",
"sandbox": "false"
},
"isDefault": true,
"isActive": true,
"clientWebhookUrl": "https://sua-api.exemplo.com.br/webhooks/baas",
"clientWebhookEvents": ["baas.pix.received", "baas.pix.transfer.completed"]
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
name | string (1–100) | Sim | Único dentro da organização. Colisão devolve 409 |
providerType | CELCOIN | FITBANK | QITECH | MOCKBANK | Sim | Determina qual estratégia é instanciada |
credentials | Record<string,string> | Sim | Formato depende do provedor — ver §13. Cifrado antes de gravar |
isDefault | boolean | Não | Marcar como padrão desmarca a anterior |
isActive | boolean | Não | Padrão true. Config inativa não pode virar padrão |
settings | object | Não | Ajustes específicos do provedor |
webhookUrl | string (URL) | Não | Onde o provedor entrega. Omitido, o serviço gera a partir de BAAS_WEBHOOK_BASE_URL |
webhookEvents | string[] | Não | Filtra o que é processado. Vazio significa todos |
clientWebhookUrl | string (URL) | Não | Onde você quer receber. Cria uma assinatura no Webhooks Engine |
clientWebhookEvents | string[] | Não | Filtros de evento baas.*. Vazio vira baas.* |
Resposta 201
{
"data": {
"type": "baas-provider-config",
"id": "6f1c2e40-8d9a-4b1e-9c3f-1a2b3c4d5e6f",
"links": { "self": "/api/v1/baas/provider-configs/6f1c2e40-..." },
"attributes": {
"name": "Celcoin Produção",
"providerType": "CELCOIN",
"isDefault": true,
"isActive": true,
"webhookUrl": "https://baas.bb.stg.catalisa.app/api/v1/baas/webhooks/celcoin?organizationId=...",
"createdAt": "2026-08-16T12:00:00.000Z"
}
}
}{
"data": {
"type": "baas-provider-config",
"id": "6f1c2e40-8d9a-4b1e-9c3f-1a2b3c4d5e6f",
"links": { "self": "/api/v1/baas/provider-configs/6f1c2e40-..." },
"attributes": {
"name": "Celcoin Produção",
"providerType": "CELCOIN",
"isDefault": true,
"isActive": true,
"webhookUrl": "https://baas.bb.stg.catalisa.app/api/v1/baas/webhooks/celcoin?organizationId=...",
"createdAt": "2026-08-16T12:00:00.000Z"
}
}
}As credenciais não voltam nesta resposta, nem em nenhuma outra. Não há endpoint que as devolva.
Erros
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod, credenciais incompletas, ou testConnection() falhou |
403 | Token sem organizationId, ou sem BAAS_ADMIN |
409 | Já existe config com esse name na organização |
POST /baas/api/v1/accounts
Abre a conta no provedor e registra localmente. Se configId for omitido, usa a configuração padrão da organização.
Request
{
"configId": "6f1c2e40-8d9a-4b1e-9c3f-1a2b3c4d5e6f",
"personId": "9b7e1d22-5a10-4f88-b3d1-77c9e0a41b52",
"businessId": "OP-2026-00841",
"holder": {
"name": "Maria Oliveira",
"documentType": "CPF",
"documentNumber": "00000000191",
"email": "maria@exemplo.com.br",
"phone": "+5511999990000",
"birthDate": "1990-05-15",
"motherName": "Ana Oliveira",
"address": {
"street": "Rua Exemplo",
"number": "100",
"neighborhood": "Centro",
"city": "São Paulo",
"state": "SP",
"zipCode": "01234567",
"country": "BR"
}
}
}{
"configId": "6f1c2e40-8d9a-4b1e-9c3f-1a2b3c4d5e6f",
"personId": "9b7e1d22-5a10-4f88-b3d1-77c9e0a41b52",
"businessId": "OP-2026-00841",
"holder": {
"name": "Maria Oliveira",
"documentType": "CPF",
"documentNumber": "00000000191",
"email": "maria@exemplo.com.br",
"phone": "+5511999990000",
"birthDate": "1990-05-15",
"motherName": "Ana Oliveira",
"address": {
"street": "Rua Exemplo",
"number": "100",
"neighborhood": "Centro",
"city": "São Paulo",
"state": "SP",
"zipCode": "01234567",
"country": "BR"
}
}
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
configId | uuid | Não | Config a usar. Omitido, usa a padrão |
personId | uuid | Não | Vínculo com Person do Customers. Não é chave estrangeira — ver §15 |
businessId | string | Não | Seu identificador de negócio. Fica na conta e nas transações |
holder.documentType | CPF | CNPJ | Sim | Define quais campos o provedor vai exigir |
holder.documentNumber | string (11–14) | Sim | Sem formatação |
holder.legalRepresentative | object | Não | Para CNPJ: representante legal |
metadata | object | Não | Chaves livres, guardadas em JSONB |
Resposta 201
{
"data": {
"type": "baas-account",
"id": "3c9a77e1-2b45-4c8d-9e10-5f6a7b8c9d01",
"attributes": {
"status": "PENDING_KYC",
"bankCode": "999",
"branchNumber": "0001",
"accountNumber": "0000042",
"accountDigit": "7",
"availableBalance": 0,
"blockedBalance": 0,
"personId": "9b7e1d22-5a10-4f88-b3d1-77c9e0a41b52",
"businessId": "OP-2026-00841",
"createdAt": "2026-08-16T12:00:00.000Z"
}
}
}{
"data": {
"type": "baas-account",
"id": "3c9a77e1-2b45-4c8d-9e10-5f6a7b8c9d01",
"attributes": {
"status": "PENDING_KYC",
"bankCode": "999",
"branchNumber": "0001",
"accountNumber": "0000042",
"accountDigit": "7",
"availableBalance": 0,
"blockedBalance": 0,
"personId": "9b7e1d22-5a10-4f88-b3d1-77c9e0a41b52",
"businessId": "OP-2026-00841",
"createdAt": "2026-08-16T12:00:00.000Z"
}
}
}O status inicial é o que o provedor devolveu. Com MockBank e autoApproveKyc ligado, a conta já nasce ACTIVE.
POST /baas/api/v1/pix/transfers
Inicia uma transferência Pix. Informe pixKey ou recipient — sem um dos dois, 400.
Request — por chave Pix
{
"accountId": "3c9a77e1-2b45-4c8d-9e10-5f6a7b8c9d01",
"amount": 150.75,
"description": "Pagamento pedido 8841",
"pixKey": "destinatario@exemplo.com.br"
}{
"accountId": "3c9a77e1-2b45-4c8d-9e10-5f6a7b8c9d01",
"amount": 150.75,
"description": "Pagamento pedido 8841",
"pixKey": "destinatario@exemplo.com.br"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
accountId | uuid | Sim | Conta de origem. Precisa ser da sua organização |
amount | number > 0 | Sim | Em reais, com centavos. Precisão de Decimal(15,2) |
description | string (≤140) | Não | Vai no campo livre do Pix |
pixKey | string | Condicional | Chave do destinatário. Celcoin e FitBank consultam o DICT antes de enviar |
recipient | object | Condicional | Dados bancários completos, quando não há chave |
scheduledFor | string (ISO 8601) | Não | Agendamento. Suporte depende do provedor |
businessId | string | Não | Seu identificador, guardado na transação |
idempotencyKey | uuid | Não | Sua chave de idempotência, única por organização. A mesma chave devolve a transação já criada (200, meta.idempotentReplay: true) em vez de pagar de novo; a chave também é repassada ao provedor |
Resposta 201 — data.attributes traz transactionType: "PIX_OUT", status, amount, fee, endToEndId, os campos counterparty* e processedAt. 200 quando a idempotencyKey já tinha transação: o mesmo corpo, mais meta.idempotentReplay: true.
Erros
| Status | Quando |
|---|---|
400 | Sem pixKey nem recipient; saldo insuficiente; valor acima do limite; conta bloqueada ou encerrada |
403 | Token sem organizationId ou sem BAAS_ADMIN |
404 | accountId inexistente ou de outra organização — a distinção é deliberada (§14) |
POST /baas/api/v1/pix/transfers/:id/refund
Devolve um Pix recebido. Duas regras verificadas antes de chamar o provedor: a transação precisa ser PIX_IN e precisa estar COMPLETED. Qualquer outra combinação devolve 400.
{ "reason": "Cobrança duplicada", "amount": 50.00 }{ "reason": "Cobrança duplicada", "amount": 50.00 }amount é opcional; omitido, devolve o valor integral. A resposta 201 traz a transação REVERSAL nova, e a original passa a REVERSED.
POST /baas/api/v1/webhooks/:providerType
Endpoint que o provedor chama. Não passa por JWT.
A organização vem do header X-Organization-Id ou do query param organizationId — sem um dos dois, 400. A URL que o BaaS gera automaticamente já inclui o query param.
flowchart TD
IN["POST /webhooks/:providerType"]
T1{"providerType conhecido?"}
T2{"config ativa daquele tipo<br/>existe na organização?"}
T3["verifyWebhookSignature<br/>controlado por PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT"]
T4["parseWebhookPayload → ParsedBaaSWebhook"]
T5{"evento assinado em webhookEvents?"}
T6{"já visto? dedupe por configId + externalEventId"}
T7["grava em baas_webhook_events,<br/>aplica o efeito e publica baas.*"]
DESC["descartado com received true"]
ERR["400"]
OK["200 received true"]
IN --> T1
T1 -->|não| ERR
T1 -->|sim| T2
T2 -->|não| ERR
T2 -->|sim| T3 --> T4 --> T5
T5 -->|não| DESC
T5 -->|sim| T6
T6 -->|repetido| DESC
T6 -->|novo| T7 --> OK
DESC --> OKPipeline de processamento, nesta ordem:
- Valida que
:providerTypeé conhecido. - Localiza a config ativa daquele tipo, na organização.
verifyWebhookSignature()— comportamento controlado porPROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT(§13).parseWebhookPayload()traduz o corpo do provedor paraParsedBaaSWebhook.- Filtra por
webhookEventsda config; evento não assinado é descartado com{ received: true }. - Deduplica por
(configId, externalEventId); repetido é descartado com{ received: true }. - Grava em
baas_webhook_events, aplica o efeito e publica o eventobaas.*.
Resposta 200 — { "received": true }.
Falha ao aplicar o efeito não vira erro HTTP: o evento é gravado com o erro no campo error e a resposta continua 200. É deliberado — provedor que recebe erro reentrega, e reentrega de um evento já persistido não conserta nada.
Início rápido
Do zero à primeira transferência Pix, usando MockBank. Não precisa de contrato com banco nenhum. Todos os comandos abaixo seguem o fluxo exercitado pelos testes end-to-end em tests/e2e/api/baas.e2e.test.ts.
1. Autenticar no IAM
BASE=https://baas.bb.stg.catalisa.app/baas
ORG=b0000000-0000-0000-0000-000000000001
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\":\"$ORG\"}" \
| jq -r .accessToken)
echo "${TOKEN:0:24}..."BASE=https://baas.bb.stg.catalisa.app/baas
ORG=b0000000-0000-0000-0000-000000000001
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\":\"$ORG\"}" \
| jq -r .accessToken)
echo "${TOKEN:0:24}..."2. Criar a configuração do MockBank
CONFIG=$(curl -s -X POST "$BASE/api/v1/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "MockBank Sandbox",
"providerType": "MOCKBANK",
"credentials": { "token": "mockbank-dev", "autoApproveKyc": "true", "simulateDelays": "false" },
"isDefault": true,
"isActive": true
}')
CONFIG_ID=$(echo "$CONFIG" | jq -r '.data.id')
echo "$CONFIG_ID"CONFIG=$(curl -s -X POST "$BASE/api/v1/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"name": "MockBank Sandbox",
"providerType": "MOCKBANK",
"credentials": { "token": "mockbank-dev", "autoApproveKyc": "true", "simulateDelays": "false" },
"isDefault": true,
"isActive": true
}')
CONFIG_ID=$(echo "$CONFIG" | jq -r '.data.id')
echo "$CONFIG_ID"O POST já testou a conexão. Se voltou 201, o provedor respondeu.
3. Abrir uma conta
ACC=$(curl -s -X POST "$BASE/api/v1/accounts" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{
\"configId\": \"$CONFIG_ID\",
\"holder\": {
\"name\": \"Maria Oliveira\",
\"documentType\": \"CPF\",
\"documentNumber\": \"00000000191\",
\"email\": \"maria@exemplo.com.br\",
\"birthDate\": \"1990-05-15\"
}
}")
ACC_ID=$(echo "$ACC" | jq -r '.data.id')
echo "$ACC" | jq '.data.attributes | {status, bankCode, accountNumber, accountDigit}'ACC=$(curl -s -X POST "$BASE/api/v1/accounts" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{
\"configId\": \"$CONFIG_ID\",
\"holder\": {
\"name\": \"Maria Oliveira\",
\"documentType\": \"CPF\",
\"documentNumber\": \"00000000191\",
\"email\": \"maria@exemplo.com.br\",
\"birthDate\": \"1990-05-15\"
}
}")
ACC_ID=$(echo "$ACC" | jq -r '.data.id')
echo "$ACC" | jq '.data.attributes | {status, bankCode, accountNumber, accountDigit}'{ "status": "ACTIVE", "bankCode": "999", "accountNumber": "0000001", "accountDigit": "3" }{ "status": "ACTIVE", "bankCode": "999", "accountNumber": "0000001", "accountDigit": "3" }Banco 999 é o código fictício do MockBank. Nenhum banco real usa esse número — é assim que você sabe, olhando um extrato, que está em ambiente simulado.
4. Registrar uma chave Pix aleatória
curl -s -X POST "$BASE/api/v1/pix/keys" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"accountId\":\"$ACC_ID\",\"keyType\":\"EVP\"}" \
| jq '.data.attributes | {keyType, keyValue, status}'curl -s -X POST "$BASE/api/v1/pix/keys" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"accountId\":\"$ACC_ID\",\"keyType\":\"EVP\"}" \
| jq '.data.attributes | {keyType, keyValue, status}'EVP é a única que dispensa keyValue — o provedor gera um UUID. Para CPF, CNPJ, EMAIL ou PHONE, informar keyValue é obrigatório.
5. Depositar saldo (só MockBank)
curl -s -X POST "$BASE/api/v1/accounts/$ACC_ID/sandbox/deposit" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"amount": 1000}' | jqcurl -s -X POST "$BASE/api/v1/accounts/$ACC_ID/sandbox/deposit" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"amount": 1000}' | jq{ "availableBalance": 1000, "blockedBalance": 0, "totalBalance": 1000, "currency": "BRL" }{ "availableBalance": 1000, "blockedBalance": 0, "totalBalance": 1000, "currency": "BRL" }Este endpoint existe só no MockBank. Nos provedores reais ele devolve 400 — em banco de verdade, saldo entra por Pix ou TED recebido.
6. Enviar um Pix
curl -s -X POST "$BASE/api/v1/pix/transfers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{
\"accountId\": \"$ACC_ID\",
\"amount\": 150.75,
\"description\": \"Primeiro Pix\",
\"recipient\": {
\"name\": \"Destinatário Teste\",
\"documentNumber\": \"00000000272\",
\"bankCode\": \"001\",
\"branchNumber\": \"0001\",
\"accountNumber\": \"12345\",
\"accountType\": \"CHECKING\"
}
}" | jq '.data.attributes | {status, amount, endToEndId}'curl -s -X POST "$BASE/api/v1/pix/transfers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{
\"accountId\": \"$ACC_ID\",
\"amount\": 150.75,
\"description\": \"Primeiro Pix\",
\"recipient\": {
\"name\": \"Destinatário Teste\",
\"documentNumber\": \"00000000272\",
\"bankCode\": \"001\",
\"branchNumber\": \"0001\",
\"accountNumber\": \"12345\",
\"accountType\": \"CHECKING\"
}
}" | jq '.data.attributes | {status, amount, endToEndId}'{ "status": "COMPLETED", "amount": 150.75, "endToEndId": "E9999999920260816120012AB34CD56EF78" }{ "status": "COMPLETED", "amount": 150.75, "endToEndId": "E9999999920260816120012AB34CD56EF78" }7. Conferir o saldo
curl -s "$BASE/api/v1/accounts/$ACC_ID/balance" -H "Authorization: Bearer $TOKEN" | jqcurl -s "$BASE/api/v1/accounts/$ACC_ID/balance" -H "Authorization: Bearer $TOKEN" | jqVolta 849.25. Este número veio do provedor, não da coluna do banco — foi ele quem debitou.
Credenciais de staging, documentadas em AMBIENTES.md. Nunca use credencial de provedor real, chave de produção ou dado de conta de cliente em documentação, script de exemplo ou issue.
Receitas
Migrar de provedor sem parar a operação
Objetivo. Passar as contas novas para outro banco parceiro mantendo as antigas onde estão.
flowchart LR P1["1 · cria a config nova"] --> P2["2 · valida com /test"] --> P3["3 · set-default"] --> P4["4 · acompanha a distribuição"] P3 --> N["contas novas → provedor novo"] P3 --> V["contas antigas → provedor antigo, intactas"]
Passo 1 — criar a config nova. testConnection roda aqui: se a credencial estiver errada, você descobre agora e não no primeiro cliente.
NEW=$(curl -s -X POST "$BASE/api/v1/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"QI Tech Produção","providerType":"QITECH",
"credentials":{"apiKey":"...","privateKey":"...","sandbox":"false"},
"isActive":true}')
NEW_ID=$(echo "$NEW" | jq -r '.data.id')NEW=$(curl -s -X POST "$BASE/api/v1/provider-configs" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"name":"QI Tech Produção","providerType":"QITECH",
"credentials":{"apiKey":"...","privateKey":"...","sandbox":"false"},
"isActive":true}')
NEW_ID=$(echo "$NEW" | jq -r '.data.id')Resposta esperada: 201 com o id da config. Credencial errada devolve 400 com a mensagem do testConnection.
Passo 2 — validar de novo, explicitamente.
curl -s -X POST "$BASE/api/v1/provider-configs/$NEW_ID/test" \
-H "Authorization: Bearer $TOKEN" | jqcurl -s -X POST "$BASE/api/v1/provider-configs/$NEW_ID/test" \
-H "Authorization: Bearer $TOKEN" | jqResposta esperada: 200 confirmando conectividade e credencial.
Passo 3 — virar o padrão. A partir daqui, POST /accounts sem configId abre no provedor novo.
curl -s -X POST "$BASE/api/v1/provider-configs/$NEW_ID/set-default" \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes.isDefault'curl -s -X POST "$BASE/api/v1/provider-configs/$NEW_ID/set-default" \
-H "Authorization: Bearer $TOKEN" | jq '.data.attributes.isDefault'Resposta esperada: true. A config anterior é desmarcada na mesma operação.
Passo 4 — acompanhar a distribuição por provedor.
curl -s "$BASE/api/v1/accounts?filter[configId]=$OLD_ID&page[size]=1" \
-H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems'curl -s "$BASE/api/v1/accounts?filter[configId]=$OLD_ID&page[size]=1" \
-H "Authorization: Bearer $TOKEN" | jq '.meta.totalItems'Resposta esperada: o número de contas que ainda vivem no provedor antigo. É esse número que precisa chegar a zero antes de encerrar a janela de migração.
Armadilhas.
- Não desative nem apague a config antiga enquanto houver conta nela. Cada operação resolve o provedor a partir do
configIdda conta; sem a config, a conta antiga fica inacessível — inclusive para consultar saldo. - A config nova precisa da própria URL de webhook cadastrada no painel do provedor. O BaaS gera a URL, mas quem a informa ao banco é você. Sem isso, os eventos de entrada param de chegar para as contas novas e você só descobre quando alguém reclama que um Pix recebido não apareceu.
- Contas não migram. Não existe endpoint que mova uma conta de um provedor para outro, e não existiria: a conta é do banco. Migrar de verdade significa encerrar lá e abrir cá, com o titular no meio.
set-defaultafeta só o que vier depois. Nenhuma conta existente muda de provedor.- Convivência de dois provedores tem limite jurídico, não técnico. O art. 6º da Resolução Conjunta nº 16/2025 veda contrato simultâneo com duas prestadoras para o mesmo tipo de conta. Trate a coexistência como janela de migração com data de encerramento definida, e alinhe o prazo com o seu jurídico antes de começar (§14).
Reconciliar uma transferência quando o webhook não chegou
Objetivo. Descobrir o estado real de uma transação que ficou parada em PENDING ou PROCESSING.
# GET não é passivo: ele consulta o provedor e grava o status novo
curl -s "$BASE/api/v1/pix/transfers/$TX_ID" -H "Authorization: Bearer $TOKEN" \
| jq '.data.attributes | {status, processedAt, failureReason, endToEndId}'
# Varrer tudo o que está pendente há mais tempo que o esperado
curl -s "$BASE/api/v1/pix/transfers?filter[status]=PROCESSING&filter[direction]=out&page[size]=100" \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[] | "\(.id) \(.attributes.createdAt)"'# GET não é passivo: ele consulta o provedor e grava o status novo
curl -s "$BASE/api/v1/pix/transfers/$TX_ID" -H "Authorization: Bearer $TOKEN" \
| jq '.data.attributes | {status, processedAt, failureReason, endToEndId}'
# Varrer tudo o que está pendente há mais tempo que o esperado
curl -s "$BASE/api/v1/pix/transfers?filter[status]=PROCESSING&filter[direction]=out&page[size]=100" \
-H "Authorization: Bearer $TOKEN" | jq -r '.data[] | "\(.id) \(.attributes.createdAt)"'Armadilhas.
- Não existe job de reconciliação automática. A varredura acima é sua para agendar. Um webhook perdido deixa a transação parada indefinidamente até alguém consultar (§15).
GETbate no provedor a cada chamada. Varrer centenas de transações em laço apertado consome quota da API do banco. Espace as chamadas.endToEndIdé a chave de conciliação com o extrato do banco, não oidda transação nem oexternalId. É o único identificador que os dois lados reconhecem.
Receber por QR Code e reagir ao pagamento
Objetivo. Cobrar por Pix e ser notificado quando o dinheiro entrar.
Passo 1 — assinar os eventos de entrada, na criação ou atualização da config.
curl -s -X PATCH "$BASE/api/v1/provider-configs/$CONFIG_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"clientWebhookUrl":"https://sua-api.exemplo.com.br/hooks/baas",
"clientWebhookEvents":["baas.pix.received"]}' | jq '.data.attributes.webhookSubscriptionId'curl -s -X PATCH "$BASE/api/v1/provider-configs/$CONFIG_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"clientWebhookUrl":"https://sua-api.exemplo.com.br/hooks/baas",
"clientWebhookEvents":["baas.pix.received"]}' | jq '.data.attributes.webhookSubscriptionId'Resposta esperada: o webhookSubscriptionId da assinatura criada no Webhooks Engine. Campo vazio significa que a assinatura não foi criada — veja a armadilha abaixo.
Passo 2 — gerar o QR dinâmico.
curl -s -X POST "$BASE/api/v1/pix/qr-codes/generate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"accountId\":\"$ACC_ID\",\"amount\":89.90,
\"description\":\"Pedido 8841\",\"expiresInMinutes\":30}" \
| jq '{emvCode, expiresAt}'curl -s -X POST "$BASE/api/v1/pix/qr-codes/generate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"accountId\":\"$ACC_ID\",\"amount\":89.90,
\"description\":\"Pedido 8841\",\"expiresInMinutes\":30}" \
| jq '{emvCode, expiresAt}'Resposta esperada: o emvCode (copia e cola) e o expiresAt calculado a partir dos 30 minutos.
Quando o pagamento entra, o provedor chama o webhook, o handler cria a transação PIX_IN e publica baas.pix.received. O Webhooks Engine entrega no seu endpoint.
Armadilhas.
expiresInMinutestem teto de 1440 — 24 horas. Acima disso o Zod reprova com400.- Sem
amount, o QR é estático: reutilizável e sem valor definido. Comamount, é dinâmico e de uso único. Gerar estático para cobrança de pedido faz o mesmo código ser pago duas vezes. - A criação da assinatura de webhook falha em silêncio. Se o Webhooks Engine estiver indisponível, o
PATCHregistra umwarnno log e devolve200mesmo assim, comwebhookSubscriptionIdvazio. Confira o campo na resposta — não assuma que assinou.
Configurar o KYC de uma conta pessoa física
Objetivo. Enviar documentos e acompanhar a análise.
curl -s -X POST "$BASE/api/v1/accounts/$ACC_ID/kyc" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"identityDocument": { "type": "CNH", "front": "https://storage.../frente.jpg" },
"selfie": "https://storage.../selfie.jpg",
"proofOfAddress": "https://storage.../comprovante.pdf"
}' | jq
curl -s "$BASE/api/v1/accounts/$ACC_ID/kyc" -H "Authorization: Bearer $TOKEN" \
| jq '{status, rejectionReason, rejectedFields}'curl -s -X POST "$BASE/api/v1/accounts/$ACC_ID/kyc" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"identityDocument": { "type": "CNH", "front": "https://storage.../frente.jpg" },
"selfie": "https://storage.../selfie.jpg",
"proofOfAddress": "https://storage.../comprovante.pdf"
}' | jq
curl -s "$BASE/api/v1/accounts/$ACC_ID/kyc" -H "Authorization: Bearer $TOKEN" \
| jq '{status, rejectionReason, rejectedFields}'Armadilhas.
- O corpo da requisição é limitado a 1 MB por
bodyLimit. Enviar documento em base64 estoura esse limite com facilidade. Prefira subir o arquivo pelo File Storage e mandar a URL — os campos aceitam base64 ou URL. - O status da conta muda junto:
APPROVEDleva a conta aACTIVE,REJECTEDaKYC_REJECTED, e qualquer outra coisa aKYC_IN_PROGRESS. - Quem decide é o provedor. O BaaS não avalia documento; ele encaminha e reflete.
rejectedFieldsdiz o que reenviar. - Conta sem KYC aprovado não transaciona. No MockBank isso é explícito (
KYC_NOT_APPROVEDvira403); nos provedores reais, a recusa vem deles.
Diagnosticar por que uma chamada voltou 400
O BaaS devolve 400 com dois formatos diferentes, e saber qual é encurta o diagnóstico:
# Falha de schema Zod, direto no router: { "error": { <árvore de campos> } }
curl -s -X POST "$BASE/api/v1/pix/transfers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"accountId":"nao-e-uuid","amount":-5}' | jq
# Falha de regra de negócio, vinda do serviço: { "error": "VALIDATION", "message": "..." }
curl -s -X POST "$BASE/api/v1/pix/transfers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"accountId\":\"$ACC_ID\",\"amount\":1}" | jq# Falha de schema Zod, direto no router: { "error": { <árvore de campos> } }
curl -s -X POST "$BASE/api/v1/pix/transfers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"accountId":"nao-e-uuid","amount":-5}' | jq
# Falha de regra de negócio, vinda do serviço: { "error": "VALIDATION", "message": "..." }
curl -s -X POST "$BASE/api/v1/pix/transfers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"accountId\":\"$ACC_ID\",\"amount\":1}" | jqOrdem de diagnóstico: o corpo passa no schema? A conta é da sua organização (senão vem 404, não 403)? A config está ativa? Tem saldo? A conta está ACTIVE? O valor está abaixo do limite do provedor?
Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token; organizationId, BAAS_READ e BAAS_ADMIN vêm dele | Sim |
| Webhooks Engine | ProviderConfigService cria, atualiza e apaga a assinatura de saída quando você define clientWebhookUrl | Sim — é dependência de container |
| Customers | BaaSAccount.personId aponta para Person; filter[personId] lista as contas de uma pessoa | Não |
| Audit Trail | Consome os eventos baas.* para trilha de compliance | Não |
| File Storage | Guarda os documentos de KYC; o BaaS recebe a URL, não o arquivo | Não |
| Payments | Cobrança e conciliação de recebíveis; o BaaS é a conta onde o dinheiro cai | Não |
| Open Finance | Agrega dados de contas em outras instituições; o BaaS opera a conta que é sua | Não |
| Decision Platform | Decide se concede o crédito; o BaaS desembolsa o resultado | Não |
flowchart TD IAM["IAM — emite o token com organizationId + permissões"] DEC["Decision Platform — aprovou?"] BAAS["BaaS<br/>contas · Pix · TED"] BANCO["Celcoin · QI Tech · FitBank<br/>a licença do BACEN mora aqui"] CUS["Customers — quem é"] WHE["Webhooks Engine — entrega"] CLI["Endpoint do cliente"] AUD["Audit Trail"] IAM -->|Bearer JWT| BAAS DEC --> BAAS BAAS -->|"API do provedor"| BANCO BANCO -->|webhook| BAAS BAAS -->|personId| CUS BAAS -->|"eventos baas.*"| WHE WHE --> CLI WHE --> AUD
O encaixe é o argumento: o Decision Platform decide, o BaaS movimenta, o Customers diz de quem é, o Webhooks Engine avisa e o Audit Trail registra. Nenhuma dessas conexões exige código de integração seu — elas acontecem por evento, com o mesmo organizationId atravessando toda a cadeia.
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
BAAS_CREDENTIAL_MASTER_KEY | Chave AES-256-GCM das credenciais de provedor. Exatamente 64 caracteres hexadecimais (32 bytes). Gere com openssl rand -hex 32 | Sim, para usar o módulo | — |
BAAS_WEBHOOK_BASE_URL | Base das URLs de callback geradas automaticamente | Não | http://localhost:3000 |
PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT | off ou on. Com on, webhook que não puder ser verificado é rejeitado | Não | off |
DATABASE_URL | PostgreSQL | Sim | — |
REDIS_URL | Redis, usado no rate limit | Sim | — |
JWT_SECRET | Verificação do token do IAM. Mínimo 44 caracteres | Sim | — |
MODULE_IAM_URL | URL do IAM em standalone | Em standalone | — |
DEPLOYMENT_MODE | monolith ou standalone | Não | monolith |
PORT | Porta em standalone | Não | 3000 (mapeada para 3015) |
BAAS_CREDENTIAL_MASTER_KEYnão tem rotação automatizada. Trocá-la torna todas as credenciais gravadas indecifráveis — decifre e recifre antes de trocar. Guarde com SOPS, nunca em.envversionado.
Credenciais por provedor — o que vai dentro do campo credentials.
| Provedor | Campos | Autenticação | Ambientes |
|---|---|---|---|
CELCOIN | clientId, clientSecret, sandbox | OAuth2 client_credentials, token em cache até 5 min antes de expirar | sandbox.openfinance.celcoin.dev · api.celcoin.com.br |
FITBANK | apiKey, apiSecret, partnerId, businessUnitId, sandbox | Basic Auth | sandboxapi.fitbank.com.br/main/execute · api.fitbank.com.br/main/execute |
QITECH | apiKey, privateKey, sandbox | Requisição assinada com RSA | sandbox.qitech.app · api-auth.qitech.app |
MOCKBANK | token, autoApproveKyc, simulateDelays, delayMs | Só confere que token não está vazio | Nenhum — roda local |
settings do MockBank aceita defaultBalance, maxPixAmount (padrão 100.000) e maxTedAmount (padrão 500.000), úteis para exercitar o caminho de limite excedido.
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema baas — configs, contas, chaves, transações, eventos e o estado do MockBank |
| Redis | Contadores de rate limit |
| IAM | Validação do token e resolução de permissões |
| Webhooks Engine | Assinaturas de entrega dos eventos baas.* |
| API do provedor | Toda operação bancária. Indisponibilidade do banco é indisponibilidade da operação |
Limites
| Limite | Valor | Onde |
|---|---|---|
| Corpo da requisição | 1 MB | applyCommonMiddleware — afeta KYC em base64 |
| Rate limit global | 10.000 req / 60s | RATE_LIMIT_GLOBAL_MAX |
| Descrição de Pix e TED | 140 caracteres | Zod |
| Expiração de QR dinâmico | 1 a 1440 minutos | Zod |
| Página padrão / listagens | 20 itens | Routers |
| Precisão monetária | Decimal(15,2) | Prisma |
| Limites de valor por transação | Do provedor | MockBank: 100.000 (Pix) e 500.000 (TED) |
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | (árvore Zod) | Corpo reprovado no schema | Compare os campos com §9 |
400 | VALIDATION | Regra de negócio: sem saldo, sem pixKey nem recipient, conta já encerrada, estorno de transação que não é PIX_IN COMPLETED | A message diz qual regra |
400 | VALIDATION | testConnection() falhou ao criar ou atualizar config | Confira as credenciais e o ambiente (sandbox) |
400 | — | Webhook sem X-Organization-Id nem organizationId | Corrija a URL cadastrada no painel do provedor |
401 | UNAUTHORIZED | Token ausente, inválido ou expirado | Renove no IAM |
403 | — | Token sem organizationId | Autentique informando a organização |
403 | FORBIDDEN | Sem BAAS_READ ou BAAS_ADMIN; ou conta bloqueada, encerrada, ou sem KYC aprovado | Confira permissões e o status da conta |
404 | NOT_FOUND | Recurso inexistente ou de outra organização | Confira o ID e a organização do token |
409 | CONFLICT | Nome de config já usado na organização | Escolha outro nome |
429 | — | Rate limit | Recuo exponencial |
500 | INTERNAL | Erro não mapeado do provedor | Consulte o log; o erro cru do provedor fica lá |
Observabilidade.
GET /baas/healthdevolve identificação e versão do build. Não valida banco nem provedor — para orquestrador serve como sonda de vida, não de prontidão.POST /provider-configs/:id/testé a sonda real: exercita credencial e conectividade com o banco parceiro. Vale agendar por config ativa.- Toda tentativa de webhook com assinatura não verificada gera
warnestruturado com o provedor e o motivo, tanto com enforcement ligado quanto desligado. É o log que diz se você pode ligarPROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT=onsem quebrar integração. baas_webhook_eventsé a trilha de auditoria de entrada: guarda o payload cru, se foi processado e o erro em caso de falha.processed = falseacumulando é sintoma.- Erros de comunicação com o provedor são logados com contexto antes de virarem
AppError.
Segurança e compliance
Quem carrega a licença — leia isto antes de tudo. A Catalisa não é instituição autorizada a funcionar pelo Banco Central do Brasil. Não é banco, não é Instituição de Pagamento, não é SCD nem SEP, não é participante do Pix e não custodia recurso de terceiro. O BaaS é uma camada de software que integra a API de instituições que são autorizadas — Celcoin (IP e SCD), QI Tech (SCD) e FitBank/Fits (IP).
flowchart LR CLI["Titular da conta<br/>cliente final"] TOM["Sua empresa — entidade tomadora<br/>não autorizada pelo BCB"] CAT["Catalisa BaaS<br/>camada de software — nem prestadora, nem tomadora"] PRE["Instituição prestadora — Celcoin, QI Tech, Fits<br/>licença do BACEN · custódia do recurso · PLD/FT e KYC"] CLI -->|"contrato de conta"| PRE TOM -->|"usa"| CAT CAT -->|"API"| PRE TOM -->|"contrato de BaaS"| PRE
A consequência prática, e ela é contratual antes de ser técnica:
- A conta bancária é do provedor.
BaaSAccount.externalIdé o identificador dela lá. O que existe aqui é um espelho com histórico. - O saldo é do provedor.
GET /accounts/:id/balancesempre consulta a origem. As colunas locais são cache para listagem. - O contrato de conta é entre o titular e a instituição autorizada, com a sua empresa e a Catalisa na cadeia técnica.
- Obrigações regulatórias de PLD/FT, KYC e reporte são da instituição autorizada e, conforme o arranjo, da sua empresa. O BaaS encaminha documento de KYC e reflete o status; ele não avalia, não pontua e não decide.
A norma brasileira de BaaS — leia antes de assinar contrato. Desde 28/11/2025 existe regulação dedicada: a Resolução Conjunta nº 16 (CMN + BCB), que dispõe sobre a prestação de serviços de Banking as a Service. Ela organiza o mercado em dois papéis — a instituição prestadora (autorizada pelo BCB) e a entidade tomadora (a sua empresa, se não for autorizada) — e a Catalisa não é nenhum dos dois: é o software que a tomadora usa para falar com a prestadora. O que a norma exige de você, se for a tomadora:
| Dispositivo | O que determina | O que isso significa na prática |
|---|---|---|
| Art. 4º, §2º | As contas devem ser de titularidade do cliente na instituição prestadora | Conta bolsão / omnibus deixa de ser arquitetura viável. É o desenho que este building block já adota: cada BaaSAccount é uma conta individual no provedor |
| Art. 6º | Vedado contratar BaaS com uma prestadora se já há contrato vigente com outra para o mesmo tipo de conta | A capacidade técnica de rodar dois provedores existe; o uso simultâneo para o mesmo tipo de conta é questão jurídica sua |
| Art. 8º | Vedado à tomadora receber em conta própria valores dos clientes, cobrar tarifa em nome próprio e subcontratar os serviços. Dever de informar ao cliente que a tomadora não é instituição autorizada pelo BCB | O aviso ao cliente final é obrigação sua, na sua interface — o BaaS não tem como cumpri-la por você |
| Art. 10 | PLD/FT, identificação e qualificação do cliente e prevenção a fraudes são responsabilidade da prestadora, que pode delegar tarefas acessórias sem afastar a própria responsabilidade | O BaaS encaminha documento de KYC; a análise, a decisão e o dever regulatório são da instituição |
| Art. 19 | Cooperativas de crédito e sociedades de arrendamento mercantil não podem ser prestadoras | Restringe o universo de parceiros elegíveis |
| Art. 24 | O BCB pode vetar, restringir, suspender ou determinar o encerramento do contrato | Risco de continuidade que precisa estar no seu plano, não só no contrato |
Prazo: os contratos vigentes têm até 31/12/2026 para se adequar. Some-se a IN BCB nº 754/2026, que obriga o registro das entidades tomadoras no Unicad com efeitos a partir de 1º/09/2026.
Nomenclatura. A Resolução Conjunta nº 17/2025 proíbe o uso de "banco" e "bank" em nome, marca e domínio por quem não tem licença bancária, com prazo até 28/11/2026 — foi por isso que o FitBank virou Fits. Se o seu produto se chama "algum-coisa bank", isso é um item de projeto com data.
Outras normas que enquadram o provedor — não a Catalisa: a Resolução BCB nº 80/2021, que define as quatro modalidades de instituição de pagamento (emissor de moeda eletrônica, emissor de instrumento de pagamento pós-pago, credenciador e iniciador de transação de pagamento) e o processo de autorização; a Resolução BCB nº 1/2020, regulamento do Pix, cujo art. 3º torna a participação obrigatória para instituições autorizadas com mais de 500 mil contas ativas — e só instituições autorizadas participam do Pix; e a Resolução CMN nº 5.050/2022, que rege SCD e SEP. O regime de limiares por porte da Res. 80 foi revogado pela Res. BCB nº 494/2025, que substituiu tudo por uma janela única de autorização já encerrada — se um fornecedor disser que "está no processo de autorização", peça a data e o número do protocolo.
Se a sua empresa já é uma instituição autorizada, o enquadramento muda: contratar software de terceiro passa pela Resolução BCB nº 85/2021 (terceirização por instituições de pagamento) ou pela Resolução CMN nº 4.893/2021 (instituições financeiras). Duas consequências práticas: a responsabilidade por confiabilidade, integridade, disponibilidade, segurança e sigilo não se transfere ao fornecedor, e a contratação de serviço relevante exige notificação ao BCB em até 10 dias. Trate a adoção deste building block como contratação sujeita a essas normas.
Antes de assinar com qualquer provedor, confirme na lista de instituições em funcionamento do BACEN que a entidade que assina o contrato está autorizada, e sob qual modalidade — a marca comercial e a razão social licenciada nem sempre coincidem. Esta documentação não substitui essa verificação nem parecer jurídico.
Isolamento entre tenants. O organizationId vem do claim assinado do JWT e nunca do corpo da requisição. Todo router de negócio aplica requireOrganization, que devolve 403 quando o claim falta. Em cima disso, cada serviço compara a organização do token com a do registro antes de agir — AccountService.getById, PixService.getAccount, TedService, ProviderConfigService.getById — e devolve 404, não 403, quando não bate. A escolha é deliberada: 403 confirmaria que o recurso existe em outro tenant.
Consultas de listagem recebem organizationId como filtro obrigatório no repositório, não como parâmetro opcional.
Credenciais de provedor. Cifradas com AES-256-GCM por encryptCredentials, no formato iv:authTag:ciphertext, com IV de 12 bytes aleatório por operação. A chave mestra vem de BAAS_CREDENTIAL_MASTER_KEY e é validada como exatamente 64 caracteres hexadecimais — chave malformada faz a operação falhar em vez de degradar para algo mais fraco. O GCM garante autenticidade: ciphertext adulterado falha na decifragem, não decifra errado. As credenciais nunca aparecem em resposta de API — toResponseData não as inclui — e não há endpoint de leitura delas.
Webhooks de entrada — o modelo de confiança
flowchart TD BANCO["Provedor bancário — não tem token da sua plataforma"] L1["1 · Assinatura do provedor<br/>verifyWebhookSignature de cada provedor"] L2["2 · PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT<br/>off aceita e registra warn · on rejeita"] L3["3 · Restrição de rede<br/>allowlist de IP na borda, para provedor sem esquema publicado"] D1["Deduplicação por configId + externalEventId"] D2["Persistência do payload cru em baas_webhook_events"] BANCO --> L1 --> L2 --> L3 --> D1 --> D2
Webhooks de entrada. É a única superfície sem JWT, porque quem chama é o banco. O modelo de confiança tem três camadas:
- Assinatura do provedor, verificada por
verifyWebhookSignature()de cada provedor. PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT, que decide o que fazer quando a verificação não é possível:offaceita e registrawarn;onrejeita. O padrão éoff, e para operação com dinheiro real a recomendação éon, depois de conferir nos logs que nenhuma integração legítima está caindo na trilha dowarn. §15 lista quais provedores hoje conseguem verificar de fato.- Restrição de rede. Para provedor sem esquema de assinatura publicado, a proteção viável é allowlist de IP na borda. Trate isso como requisito de implantação, não como opcional.
Somam-se duas propriedades independentes de assinatura: deduplicação por (configId, externalEventId), que impede reprocessar o mesmo evento, e persistência do payload cru em baas_webhook_events, que dá trilha completa do que chegou.
Dados pessoais e LGPD. O BaaS guarda nome, CPF ou CNPJ, e-mail, telefone, endereço, nome da mãe e data de nascimento do titular, além de nome e documento das contrapartes de cada transação. Tudo é dado pessoal; parte é dado financeiro. Três observações operacionais:
- Não há mascaramento de documento nas respostas de API. Quem tem
BAAS_READvê o CPF do titular. TrateBAAS_READcomo permissão sensível, não como leitura genérica. - Documentos de KYC não são guardados aqui. Eles vão para o provedor; o BaaS não persiste imagem nem base64. Se você usa o File Storage como origem das URLs, a retenção é lá.
- Contas e transações não têm exclusão lógica — não há
deletedAtembaas_accountsnem embaas_transactions. É coerente com retenção financeira, e significa que atender a pedido de eliminação exige processo explícito, hoje não automatizado (§15).
Permissões. Só duas, e a granularidade é grossa de propósito: BAAS_READ para tudo que lê, BAAS_ADMIN para tudo que escreve. Quem pode configurar um provedor pode também enviar Pix, porque as duas coisas exigem BAAS_ADMIN. Se a sua operação precisa separar quem configura de quem transaciona, isso hoje se faz com organizações distintas — não com papéis distintos (§15).
Superfície HTTP. Todo o app passa por applyCommonMiddleware: limite de 1 MB no corpo, CORS que bloqueia cross-origin quando não há origens configuradas, cabeçalhos de segurança e rate limit global.
Limitações conhecidas
Maturidade dos provedores. É a limitação mais importante e a razão do status beta.
| Provedor | Situação real, verificada no código | O que falta |
|---|---|---|
| MockBank | Implementação completa e exercitada de ponta a ponta pelos testes de integração e end-to-end. Persiste em tabelas próprias, faz débito e crédito, gera EVP, endToEndId, QR EMV e dispara webhook | Nada — mas é simulador. Nunca use em produção. Banco 999 e ISPB 99999999 são fictícios |
| Celcoin | Os 24 métodos da interface implementados contra a API REST real, com OAuth2 e cache de token. Testado com HTTP mockado | Verificação de assinatura de webhook não implementada (há TODO no código). Sem evidência de homologação contra o sandbox real neste repositório |
| QI Tech | Os 24 métodos implementados, com requisição assinada por RSA. Testado com HTTP mockado | Verificação de assinatura de webhook não implementada. Sem evidência de homologação contra o sandbox real neste repositório |
| FitBank / Fits | Os 24 métodos implementados sobre a API RPC. Testado com HTTP mockado | O provedor não publica esquema de assinatura de webhook — a proteção viável é allowlist de IP. Sem evidência de homologação contra o sandbox real |
Consequência direta: com PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT=on, os webhooks de Celcoin, QI Tech e FitBank passam a ser rejeitados, porque nenhum dos três consegue verificar de fato. Com off — o padrão — eles são aceitos e um warn é registrado. Não há terceira opção hoje. Antes de operar com dinheiro real, resolva isto: implemente a verificação para Celcoin e QI Tech, e restrinja por IP o FitBank.
Funcionalidades ausentes ou parciais
| Limitação | Impacto | Situação |
|---|---|---|
| Idempotência só com chave | Sem idempotencyKey, um retry de rede ainda pode gerar transferência duplicada: o bloco não deduplica por valor, destinatário ou businessId | Envie sempre uma idempotencyKey (uuid) por operação sua. Com ela, a repetição devolve a transação já criada (200), inclusive quando duas chamadas correm ao mesmo tempo (índice único por organização + chave) |
| Sem reconciliação automática | Webhook perdido deixa a transação parada em PENDING ou PROCESSING indefinidamente. Não há job de varredura | Faça o polling você — receita em §11 |
| Boleto não implementado | Não há emissão nem baixa de boleto, embora os provedores ofereçam | Fora do escopo atual |
| Bloquear e desbloquear conta não tem rota HTTP | blockAccount e unblockAccount existem na interface e nos quatro provedores, mas nenhum router os expõe. O bloqueio só chega por webhook do provedor | Implementado na camada de provedor, não exposto |
personId não é chave estrangeira | O vínculo com Person do Customers é um UUID solto: sem validação na criação, sem integridade referencial, sem cascata | Por design de desacoplamento entre building blocks — mas o personId inválido só aparece quando alguém tenta usar |
| Agendamento sem garantia uniforme | scheduledFor é aceito e repassado, mas o comportamento é do provedor. O MockBank ignora e executa na hora | Confirme com o seu provedor antes de prometer agendamento ao cliente |
| Permissões grossas | Só BAAS_READ e BAAS_ADMIN. Quem configura provedor também transfere dinheiro | Separar exige permissões novas no vocabulário do IAM |
BAAS_READ vê CPF sem máscara | Não há mascaramento de documento nas respostas | Trate BAAS_READ como permissão sensível |
| KYC limitado a 1 MB por requisição | bodyLimit global. Documento em base64 estoura facilmente | Use URL do File Storage em vez de base64 |
| Sem expurgo para LGPD | Contas e transações não têm exclusão lógica nem rotina de eliminação | Eliminação é processo manual |
| Config com credencial rotacionada não tem período de graça | PATCH com credentials substitui na hora. Requisição em voo com a credencial antiga falha | Coordene a janela |
| Sem cache de saldo | Toda consulta bate no provedor. Volume alto de consulta consome quota da API do banco | Por design (§7) — mas planeje a quota |
| Estorno só de Pix recebido e completo | POST /transfers/:id/refund exige PIX_IN e COMPLETED. Não há devolução de Pix enviado nem estorno de TED | Restrição do fluxo Pix, verificada antes de chamar o provedor |
CANCELLED não é alcançável | O status existe no enum, mas nenhum caminho do código leva a ele | Reservado para status vindo do provedor |
| MockBank compartilha estado no processo | O simulador guarda estado em estruturas estáticas por processo, não segregadas por organização. Duas organizações usando MockBank no mesmo processo enxergam o mesmo DICT | Aceitável para desenvolvimento; é mais um motivo para nunca usá-lo em produção |
| Escrita local depois da confirmação do provedor | Se o provedor aceitar e a gravação local falhar, existe transação no banco parceiro sem contrapartida aqui | Reconcilie por endToEndId — é a lição do caso Synapse (§4) |
Lacunas em relação à Resolução Conjunta nº 16/2025. A norma é de novembro de 2025 e o prazo de adequação vai até 31/12/2026. O código ainda não a reflete em três pontos:
| Exigência | Situação no código |
|---|---|
| Art. 6º — exclusividade por tipo de conta | Nada impede tecnicamente criar duas configurações ativas de provedores diferentes. O controle é contratual e humano; não há trava no software |
| Art. 8º — aviso ao cliente de que a tomadora não é instituição autorizada | Não há campo, endpoint nem conteúdo para isso. É obrigação a cumprir na sua interface |
| IN BCB 754/2026 — registro da tomadora no Unicad (efeitos em 1º/09/2026) | Não há campo para o identificador de registro nem exposição dele em API |
Nenhuma dessas é falha de implementação — são obrigações que recaem sobre a tomadora e a prestadora, não sobre a camada de software. Estão aqui porque um comprador precisa saber que o building block não as cumpre por ele.
Não deployado como dependência de outro building block. Nenhum outro módulo consome o BaaS por ModuleClient. A integração com o resto do catálogo é por evento e pelo personId. Isso é coerente com o desenho, mas significa que o BaaS ainda não tem tráfego vindo de dentro da plataforma.
Perguntas frequentes
A Catalisa é um banco? Vocês têm licença do Banco Central?
Não, e a resposta é curta de propósito. A Catalisa é a camada de software. Quem tem licença, custodia o dinheiro e responde ao Banco Central é o provedor que você contrata — Celcoin, QI Tech ou FitBank. Nós não somos instituição autorizada, não custodiamos recurso de terceiro e não somos participantes do Pix. Se alguém apresentar isto de outra forma numa proposta, a proposta está errada. §14 detalha o que isso implica em contrato.
Preciso ter contrato com um banco antes de começar a integrar?
Não. Use o MockBank: ele simula conta, saldo, DICT, transferência, QR Code e webhook, e a esteira que você constrói contra ele é a mesma que roda em produção. Na prática, o time de engenharia costuma terminar a integração antes de o jurídico terminar o contrato. Só não coloque MockBank em produção — o banco 999 não existe.
Dá para trocar de provedor depois? Quanto custa?
Dá, e é o motivo pelo qual este building block existe. Você cria a configuração nova, valida com /test, marca como padrão, e as contas novas nascem no provedor novo. As antigas continuam no antigo, funcionando, porque cada conta guarda o configId a que pertence. O que não dá é mover uma conta existente de um banco para outro — a conta é do banco. Migrar de verdade significa encerrar lá e abrir cá, com o titular no meio.
Posso usar dois provedores ao mesmo tempo?
Tecnicamente, sim — inclusive duas configurações do mesmo provedor com credenciais diferentes. Mas leia a ressalva antes de planejar em cima disso: o art. 6º da Resolução Conjunta nº 16/2025 veda contratar BaaS com uma prestadora quando já existe contrato vigente com outra para o mesmo tipo de conta. O que o software permite e o que o contrato permite são coisas diferentes, e aqui elas divergem. O uso seguro e claramente dentro da norma é a migração entre provedores; manter dois contratos simultâneos do mesmo tipo é conversa para o seu jurídico. §14 tem o quadro completo.
O saldo que a API retorna é confiável?
É o saldo do provedor, consultado na hora. GET /accounts/:id/balance sempre bate no banco antes de responder. As colunas availableBalance e blockedBalance que aparecem na listagem de contas são cache atualizado de forma assíncrona — servem para tela de listagem, não para decidir se uma transferência pode sair.
Uma transferência ficou presa em PROCESSING. O que faço?
Chame GET /baas/api/v1/pix/transfers/:id. Esse endpoint não é passivo: ele consulta o provedor e grava o status novo se mudou. É o caminho de reconciliação para webhook perdido. Não há varredura automática hoje (§15), então vale agendar a sua — a receita está em §11.
Como faço para o mesmo Pix não sair duas vezes se minha chamada der timeout?
Mande uma idempotencyKey (uuid) por operação sua em POST /pix/transfers e POST /ted. A chave é única por organização: se a primeira chamada criou a transação e a segunda chegou por retry, a segunda devolve a mesma transação com 200 e meta.idempotentReplay: true, sem tocar o provedor de novo — mesmo se as duas correrem ao mesmo tempo (a que perde o índice único devolve a que ganhou). A chave também vai ao provedor (Celcoin externalId, QiTech idempotency_key, FitBank Identifier), que dedupa do lado dele. Sem chave, controle a duplicidade do seu lado (§15).
Quais eventos eu recebo, e como?
São 18 eventos baas.*, listados em §8. Configure clientWebhookUrl e clientWebhookEvents na configuração do provedor; o BaaS cria a assinatura no Webhooks Engine, que faz a entrega com retry e assinatura. Confira o campo webhookSubscriptionId na resposta — se vier vazio, a assinatura não foi criada.
Qual a diferença entre o BaaS e o Open Finance?
Direção oposta. O Open Finance lê dados de contas que o seu cliente tem em outras instituições, com consentimento. O BaaS opera uma conta que existe por causa da sua aplicação. Um enxerga; o outro movimenta.
Por que uma conta de outra organização devolve 404 e não 403?
Porque 403 responderia uma pergunta que ninguém deveria poder fazer: "este ID existe em algum lugar?". Em building block financeiro, confirmar a existência de uma conta alheia já é vazamento. O 404 é a resposta correta para "não existe para você".
Posso rodar isso em produção hoje?
O módulo está deployado nas stacks de staging e produção, e a esteira funciona. Mas antes de mover dinheiro real: resolva a verificação de assinatura de webhook do seu provedor e ligue PROVIDER_WEBHOOK_SIGNATURE_ENFORCEMENT=on; trate a idempotência do lado da sua aplicação; e homologue contra o sandbox real do provedor, o que este repositório ainda não registra ter sido feito. §15 é a lista completa, e ela existe para ser lida antes da decisão, não depois.
Padrão: PADRAO-DOCUMENTACAO.md · Decisão de arquitetura: ADR-0003 · Detalhes de mapeamento por provedor: providers/README.md · Índice: INDEX.md