Guia de system design para construir uma plataforma Open Finance no Brasil, cobrindo BACEN, consentimento granular, FAPI 2.0, Pix, arquitetura event-driven e compliance.
Recursos selecionados para complementar sua leitura

Software Architect
Pós-graduado em arquitetura de software e soluções. Conecto profundidade técnica com resultados de negócio para entregar produtos que as pessoas realmente usam. Também mentoro desenvolvedores e criadores em programas ao vivo, podcasts e iniciativas de comunidade focadas em tecnologia inclusiva.
Checklist de 47 pontos para encontrar bugs, riscos de segurança e problemas de performance antes do lançamento.
Continue explorando tópicos similares

Um guia completo de system design para construir uma plataforma de compartilhamento de corridas lidando com milhões de viagens simultâneas, matching geoespacial em tempo real, precificação dinâmica e predição de ETA em escala global.

Um guia de arquitetura em nível de produção para construir um sistema estilo Instagram com feed personalizado, stories, reels, mensagens, notificações, ranking em tempo real e operação global.
Templates testados em produção, usados por desenvolvedores. Economize semanas de setup no seu próximo projeto.
Consultorias modulares para founders e CTOs fracionados. Você recebe diagnóstico acionável e acompanhamento direto comigo.
2 vagas para consultorias no Q2

Resumo: Guia definitivo de system design para construir uma plataforma Open Finance no Brasil do zero à produção, cobrindo integração com o ecossistema BACEN, fintechs, insurtechs, consentimento granular, segurança FAPI 2.0, arquitetura event-driven e conformidade regulatória completa.
O Brasil construiu um dos sistemas financeiros abertos mais sofisticados do mundo. Enquanto a Europa implementava o PSD2 e o Reino Unido estabelecia seu Open Banking, o Banco Central do Brasil foi além: criou o Open Finance, que engloba não apenas dados bancários e pagamentos, mas também seguros, previdência, câmbio e investimentos.
Em 2026, o ecossistema Open Finance brasileiro conecta mais de 800 instituições participantes, processa bilhões de consentimentos e movimenta trilhões de reais em transações iniciadas por terceiros. Uma fintech que não entende como se integrar com essa infraestrutura está operando com uma venda nos olhos.
Este guia vai além da teoria. Vamos projetar um sistema completo: desde a arquitetura do servidor de autorização OAuth 2.0/FAPI até o pipeline de dados em tempo real, passando por gestão de consentimento granular, integração com Pix, Open Insurance e mecanismos de resiliência para um ambiente regulatório que nunca para de evoluir.
Você vai sair daqui sabendo como construir, operar e escalar uma plataforma Open Finance que passa em auditoria, sobrevive a falhas de parceiros e entrega experiências que os usuários realmente querem usar.
Open Finance é a evolução do Open Banking. Enquanto Open Banking abre dados de contas correntes, cartões e crédito, Open Finance estende o escopo para:
| Domínio | O que abre | Regulação |
|---|---|---|
| Open Banking | Contas, cartões, crédito, transações | Res. BCB 32/2020 |
| Open Insurance | Apólices, sinistros, prêmios | Res. CNSP 415/2021 |
| Open Investments | Carteira, posições, extratos | Res. CVM 35/2021 |
| Open Pension | Planos PGBL/VGBL, contribuições | PREVIC |
| Open Exchange | Remessas, câmbio, limite operacional | Res. BCB 278/2022 |
O princípio central é o portabilidade de dados com consentimento: os dados pertencem ao cliente, não à instituição. A instituição é apenas custodiante.
Fase 1 (Fev 2021) → Dados públicos das instituições (produtos, tarifas, agências)
Fase 2 (Ago 2021) → Dados de clientes (contas, transações, crédito)
Fase 3 (Out 2021) → Iniciação de pagamento via Pix e TED
Fase 4 (Dez 2021+) → Dados de investimentos, seguros, câmbio, previdência
Conhecer a regulação não é opcional. É o contrato que define o que seu sistema pode e deve fazer.
| Norma | Emissora | Tema |
|---|---|---|
| Resolução Conjunta 1/2020 | BCB + CMN | Marco geral do Open Banking |
| Resolução BCB 32/2020 | BCB | Requisitos técnicos e de segurança |
| Resolução BCB 95/2021 | BCB | Iniciação de pagamento |
| Resolução CNSP 415/2021 | SUSEP | Open Insurance |
| Circular SUSEP 635/2021 | SUSEP | Detalhes técnicos Open Insurance |
| LGPD (Lei 13.709/2018) | Congresso | Proteção de dados pessoais |
Dados cadastrais/transacionais: até 12 meses renováveis
Iniciação de pagamento único: uso único (one-time)
Pagamento recorrente: até a data especificada pelo cliente
Dados de seguro/investimento: até 12 meses renováveis
Como Instituição Transmissora (você tem os dados do cliente):
Como Instituição Receptora / TPP (você consome os dados):
Funcionalidades de Produto (Fintech/InsurTech):
| Requisito | Meta | Motivo |
|---|---|---|
| Disponibilidade APIs | 99,9% (8,7h downtime/ano) | Regulatório: 99,5% mínimo + SLA comercial |
| Latência p95 | < 1.000ms | Limite regulatório BCB |
| Latência p50 | < 300ms | UX aceitável em agregação |
| Throughput pico | 10.000 req/s | Black Friday financeiro |
| Revogação de consentimento | < 5 segundos | Exigência regulatória |
| Auditoria | 100% das chamadas logadas | Compliance BCB |
| Retenção de logs | 5 anos | Resolução BCB 32/2020 |
| Criptografia dados em trânsito | TLS 1.2+ | Obrigatório |
| Criptografia dados em repouso | AES-256 | Obrigatório |
Premissas deste guia:
Usuários ativos: 500.000
Consentimentos ativos por usuário: 3 (média)
Total de consentimentos: 1.500.000
Refresh tokens gerados por dia: 500.000 × 0,3 = 150.000/dia
Token validations por dia: 150.000 × 20 chamadas = 3.000.000/dia
TPS médio de token validation: 3.000.000 / 86.400 = ~35 TPS
TPS pico (10x): ~350 TPS
Usuários que acessam o app por dia: 100.000 (20%)
Chamadas de API por sessão: 8 (dados conta + transações + limites)
Total chamadas/dia: 800.000
TPS médio: 800.000 / 86.400 = ~9,3 TPS
TPS pico (horário comercial, 4h de pico): ~55 TPS
TPS pico absoluto (evento especial): ~500 TPS
Tamanho médio de uma transação: 500 bytes
Transações por usuário por mês: 100
Usuários: 500.000
Dados novos por mês: 500B × 100 × 500.000 = 25 GB/mês
Com retenção de 5 anos: 25 GB × 60 = 1,5 TB (dados brutos)
Com índices e overhead: ~5 TB total
Logs de auditoria (5 anos):
1 log por API call = 2 KB
800.000 calls/dia × 2 KB = 1,6 GB/dia
5 anos: 1,6 GB × 1.825 = 2,9 TB de logs
Par de chaves RSA-2048 por participante
JWKS endpoint: renovação a cada 30 dias
Volume de verificação de assinatura JWT:
Cada chamada verifica 1-2 JWTs
500 TPS pico × 2 = 1.000 verificações/s
RSA verify: ~50ms → precisa de cache de chaves públicas
O coração de qualquer participante Open Finance é o Authorization Server. Ele precisa implementar o perfil FAPI 2.0 (Financial-grade API), que adiciona restrições ao OAuth 2.0 padrão para contextos financeiros.
| Aspecto | OAuth 2.0 Padrão | FAPI 2.0 |
|---|---|---|
| Fluxo permitido | Authorization Code, Implicit, Client Credentials | Apenas Authorization Code + PKCE |
| Implicit Flow | Permitido | Proibido |
| Client Authentication | Client Secret | mTLS ou Private Key JWT |
| Request Objects | Opcional | Obrigatório (JAR - RFC 9101) |
| PAR | Opcional | Obrigatório (RFC 9126) |
| Token Binding | Opcional | mTLS Sender-Constrained (RFC 8705) |
| PKCE | Opcional | Obrigatório |
| Response Mode | query, fragment | jwt (JARM - JWT Secured Auth Response) |
// src/auth/authorization-server.ts
import { SignJWT, importPKCS8, jwtVerify } from 'jose';
import crypto from 'crypto';
interface PARRequest {
clientId: string;
scopes: string[];
redirectUri: string;
codeChallenge: string;
codeChallengeMethod: 'S256';
requestObject: string; // JWT assinado pelo TPP
}
interface ConsentScope {
permissions: OpenFinancePermission[];
expirationDateTime: Date;
transactionFromDateTime?: Date;
transactionToDateTime?: Date;
}
type OpenFinancePermission =
| 'ACCOUNTS_READ'
| 'ACCOUNTS_BALANCES_READ'
| 'ACCOUNTS_TRANSACTIONS_READ'
| 'CREDIT_CARDS_ACCOUNTS_READ'
| 'INVESTMENTS_READ'
| 'INSURANCE_READ'
| 'PAYMENTS_INITIATE';
export class FAPIAuthorizationServer {
constructor(
private readonly consentRepository: ConsentRepository,
private readonly directoryClient: DirectoryParticipantsClient,
private readonly tokenSigner: TokenSigner,
) {}
async handlePushedAuthorizationRequest(
request: PARRequest,
clientCertThumbprint: string,
): Promise<{ requestUri: string; expiresIn: number }> {
await this.validateClientRegistration(request.clientId, clientCertThumbprint);
const requestObjectPayload = await this.validateAndDecodeRequestObject(
request.requestObject,
request.clientId,
);
this.validatePKCE(request.codeChallenge, request.codeChallengeMethod);
const consentId = await this.consentRepository.createPending({
clientId: request.clientId,
scopes: request.scopes as OpenFinancePermission[],
expirationDateTime: requestObjectPayload.expirationDateTime,
});
const requestUri = `urn:ietf:params:oauth:request_uri:${crypto.randomUUID()}`;
await this.storeParRequest(requestUri, {
...request,
consentId,
clientCertThumbprint,
expiresAt: new Date(Date.now() + 90_000), // 90 segundos, conforme FAPI
});
return { requestUri, expiresIn: 90 };
}
async exchangeCodeForToken(
code: string,
codeVerifier: string,
clientCertThumbprint: string,
): Promise<TokenResponse> {
const authCode = await this.retrieveAuthorizationCode(code);
if (!authCode) {
throw new InvalidGrantError('Código de autorização inválido ou expirado');
}
this.validateCodeVerifier(codeVerifier, authCode.codeChallenge);
const consent = await this.consentRepository.findById(authCode.consentId);
if (consent.status !== 'AUTHORISED') {
throw new InvalidGrantError('Consentimento não está autorizado');
}
// Token binding: vincula o access token ao certificado mTLS do cliente
// Requisito FAPI 2.0 / RFC 8705
const certThumbprint = this.computeSHA256Thumbprint(clientCertThumbprint);
const accessToken = await this.tokenSigner.sign({
sub: consent.userId,
client_id: authCode.clientId,
consent_id: consent.id,
scope: consent.permissions.join(' '),
cnf: { x5t: certThumbprint }, // certificate-bound token
exp: Math.floor(Date.now() / 1000) + 900, // 15 minutos
});
const refreshToken = await this.issueRefreshToken(consent.id, clientCertThumbprint);
return { accessToken, refreshToken, expiresIn: 900, tokenType: 'Bearer' };
}
private async validateClientRegistration(
clientId: string,
certThumbprint: string,
): Promise<void> {
const registration = await this.directoryClient.getClientRegistration(clientId);
if (!registration) {
throw new UnauthorizedClientError(`Cliente ${clientId} não encontrado no Diretório`);
}
if (registration.certThumbprint !== certThumbprint) {
throw new UnauthorizedClientError('Certificado mTLS não corresponde ao registro');
}
if (registration.status !== 'ACTIVE') {
throw new UnauthorizedClientError('Registro do cliente inativo no Diretório');
}
}
private validateCodeVerifier(verifier: string, challenge: string): void {
const computed = crypto
.createHash('sha256')
.update(verifier)
.digest('base64url');
if (computed !== challenge) {
throw new InvalidGrantError('PKCE code_verifier inválido');
}
}
private computeSHA256Thumbprint(cert: string): string {
return crypto.createHash('sha256').update(cert, 'hex').digest('base64url');
}
}
O consentimento é o ativo mais crítico do Open Finance. Cada chamada de API deve verificar: existe consentimento ativo? O escopo cobre o dado solicitado? O consentimento ainda não expirou?
O Open Finance Brasil define grupos de permissão que o usuário seleciona individualmente:
// src/consent/permissions.ts
export const PERMISSION_GROUPS = {
ACCOUNTS: {
label: 'Dados de Conta Corrente e Poupança',
permissions: [
'ACCOUNTS_READ',
'ACCOUNTS_BALANCES_READ',
'ACCOUNTS_TRANSACTIONS_READ',
'ACCOUNTS_OVERDRAFT_LIMITS_READ',
] as const,
},
CREDIT_CARDS: {
label: 'Cartão de Crédito',
permissions: [
'CREDIT_CARDS_ACCOUNTS_READ',
'CREDIT_CARDS_ACCOUNTS_BILLS_READ',
'CREDIT_CARDS_ACCOUNTS_BILLS_TRANSACTIONS_READ',
'CREDIT_CARDS_ACCOUNTS_LIMITS_READ',
'CREDIT_CARDS_ACCOUNTS_TRANSACTIONS_READ',
] as const,
},
CREDIT_OPERATIONS: {
label: 'Operações de Crédito',
permissions: [
'LOANS_READ',
'LOANS_SCHEDULED_INSTALMENTS_READ',
'LOANS_PAYMENTS_READ',
'FINANCINGS_READ',
'UNARRANGED_ACCOUNTS_OVERDRAFT_READ',
] as const,
},
INVESTMENTS: {
label: 'Investimentos',
permissions: [
'BANK_FIXED_INCOMES_READ',
'CREDIT_FIXED_INCOMES_READ',
'FUNDS_READ',
'VARIABLE_INCOMES_READ',
'TREASURE_TITLES_READ',
] as const,
},
INSURANCE: {
label: 'Seguros',
permissions: [
'RESOURCES_READ',
'LIFE_PENSION_READ',
'PENSION_PLAN_READ',
] as const,
},
PAYMENTS: {
label: 'Iniciação de Pagamento',
permissions: ['PAYMENTS_INITIATE'] as const,
},
} as const;
// src/consent/consent.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
export interface ConsentValidationResult {
isValid: boolean;
reason?: string;
consent?: Consent;
}
@Injectable()
export class ConsentService {
constructor(
@InjectRepository(ConsentRepository)
private readonly consentRepo: ConsentRepository,
private readonly eventBus: EventBus,
private readonly cacheService: CacheService,
) {}
async validateForApiCall(
consentId: string,
requiredPermission: OpenFinancePermission,
clientId: string,
): Promise<ConsentValidationResult> {
// Verificação em cache primeiro para reduzir latência
// Cache TTL: 30s (balanço entre performance e consistência na revogação)
const cacheKey = `consent:${consentId}`;
const cached = await this.cacheService.get<ConsentValidationResult>(cacheKey);
if (cached) {
return this.checkPermissionInCachedResult(cached, requiredPermission);
}
const consent = await this.consentRepo.findById(consentId);
if (!consent) {
return { isValid: false, reason: 'Consentimento não encontrado' };
}
if (consent.clientId !== clientId) {
return { isValid: false, reason: 'Cliente não corresponde ao consentimento' };
}
const validationResult = this.validateConsentState(consent, requiredPermission);
if (validationResult.isValid) {
// Só cacheamos consentimentos válidos; inválidos não cacheamos
// para forçar re-consulta e capturar mudanças de estado
await this.cacheService.set(cacheKey, validationResult, 30);
}
return validationResult;
}
async revokeConsent(consentId: string, revokedBy: 'USER' | 'TPP' | 'INSTITUTION'): Promise<void> {
const consent = await this.consentRepo.findById(consentId);
if (!consent || consent.status !== 'AUTHORISED') {
return; // Idempotente: revogar já revogado é no-op
}
await this.consentRepo.updateStatus(consentId, 'REVOKED', {
revokedAt: new Date(),
revokedBy,
});
// Invalida cache IMEDIATAMENTE - revogação deve propagar em < 5s
await this.cacheService.delete(`consent:${consentId}`);
await this.eventBus.publish(new ConsentRevokedEvent({ consentId, revokedBy }));
}
private validateConsentState(
consent: Consent,
requiredPermission: OpenFinancePermission,
): ConsentValidationResult {
if (consent.status !== 'AUTHORISED') {
return { isValid: false, reason: `Consentimento em estado inválido: ${consent.status}` };
}
if (consent.expirationDateTime < new Date()) {
// Transição assíncrona para EXPIRED, não bloqueia a resposta
this.expireConsentAsync(consent.id);
return { isValid: false, reason: 'Consentimento expirado' };
}
if (!consent.permissions.includes(requiredPermission)) {
return {
isValid: false,
reason: `Permissão ${requiredPermission} não consentida`,
};
}
return { isValid: true, consent };
}
private async expireConsentAsync(consentId: string): Promise<void> {
setImmediate(async () => {
await this.consentRepo.updateStatus(consentId, 'EXPIRED');
await this.cacheService.delete(`consent:${consentId}`);
await this.eventBus.publish(new ConsentExpiredEvent({ consentId }));
});
}
}
O Diretório de Participantes é o registro central mantido pelo BACEN. Cada participante (transmissora ou receptora) deve se registrar ali. O Diretório contém:
// src/directory/directory-participants.client.ts
import { Injectable, Logger } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { Cron } from '@nestjs/schedule';
interface ParticipantRegistration {
organizationId: string;
clientId: string;
status: 'ACTIVE' | 'SUSPENDED' | 'CANCELLED';
roles: ParticipantRole[];
jwksUri: string;
certThumbprint: string;
softwareStatementUri: string;
apiEndpoints: Record<string, string>;
}
type ParticipantRole = 'DADOS' | 'PAGTO' | 'CONTA';
@Injectable()
export class DirectoryParticipantsClient {
private readonly logger = new Logger(DirectoryParticipantsClient.name);
private readonly DIRECTORY_BASE_URL = 'https://data.directory.openbankingbrasil.org.br';
constructor(
private readonly httpService: HttpService,
private readonly cacheService: CacheService,
) {}
@Cron('*/15 * * * *') // A cada 15 minutos
async syncParticipants(): Promise<void> {
this.logger.log('Iniciando sincronização com Diretório de Participantes');
try {
const participants = await this.fetchAllParticipants();
await Promise.all(
participants.map(p =>
this.cacheService.set(
`directory:participant:${p.clientId}`,
p,
3600, // 1 hora, substituído pela próxima sync
),
),
);
this.logger.log(`Sincronizados ${participants.length} participantes`);
} catch (error) {
this.logger.error('Falha na sincronização com Diretório', error);
// Não propaga: cache antigo continua sendo usado
}
}
async getClientRegistration(clientId: string): Promise<ParticipantRegistration | null> {
const cacheKey = `directory:participant:${clientId}`;
const cached = await this.cacheService.get<ParticipantRegistration>(cacheKey);
if (cached) return cached;
// Busca diretamente se não está no cache (ex: novo participante)
const fresh = await this.fetchParticipant(clientId);
if (fresh) {
await this.cacheService.set(cacheKey, fresh, 900); // 15 minutos
}
return fresh;
}
async getPublicJWKS(clientId: string): Promise<JsonWebKeySet> {
const registration = await this.getClientRegistration(clientId);
if (!registration) throw new Error(`Participante ${clientId} não encontrado`);
const cacheKey = `directory:jwks:${clientId}`;
const cached = await this.cacheService.get<JsonWebKeySet>(cacheKey);
if (cached) return cached;
const response = await this.httpService.axiosRef.get<JsonWebKeySet>(registration.jwksUri);
await this.cacheService.set(cacheKey, response.data, 1800); // 30 minutos
return response.data;
}
private async fetchAllParticipants(): Promise<ParticipantRegistration[]> {
const response = await this.httpService.axiosRef.get(
`${this.DIRECTORY_BASE_URL}/participants`,
{ timeout: 10_000 },
);
return response.data;
}
private async fetchParticipant(clientId: string): Promise<ParticipantRegistration | null> {
try {
const response = await this.httpService.axiosRef.get(
`${this.DIRECTORY_BASE_URL}/participants/${clientId}`,
{ timeout: 5_000 },
);
return response.data;
} catch {
return null;
}
}
}
Todas as APIs seguem o padrão definido nas especificações do Open Finance Brasil, disponíveis em openfinancebrasil.org.br. Cada resposta segue um envelope padrão:
// src/common/open-finance-response.ts
interface OpenFinanceResponse<T> {
data: T;
links: {
self: string;
first?: string;
prev?: string;
next?: string;
last?: string;
};
meta: {
totalRecords: number;
totalPages: number;
requestDateTime: string; // ISO 8601
};
}
// src/accounts/accounts.controller.ts
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
import { ConsentGuard } from '../auth/consent.guard';
import { RequiresPermission } from '../auth/requires-permission.decorator';
@Controller('open-banking/v2/accounts')
@UseGuards(MTLSGuard, TokenValidationGuard, ConsentGuard)
export class AccountsController {
constructor(private readonly accountsService: AccountsService) {}
@Get()
@RequiresPermission('ACCOUNTS_READ')
async listAccounts(
@ConsentContext() consent: ValidatedConsent,
): Promise<OpenFinanceResponse<AccountSummary[]>> {
const accounts = await this.accountsService.findByUserId(consent.userId);
return {
data: accounts.map(this.toAccountSummary),
links: { self: '/open-banking/v2/accounts' },
meta: {
totalRecords: accounts.length,
totalPages: 1,
requestDateTime: new Date().toISOString(),
},
};
}
@Get(':accountId/transactions')
@RequiresPermission('ACCOUNTS_TRANSACTIONS_READ')
async getTransactions(
@Param('accountId') accountId: string,
@Query() query: TransactionQueryDto,
@ConsentContext() consent: ValidatedConsent,
): Promise<OpenFinanceResponse<Transaction[]>> {
await this.accountsService.assertOwnership(accountId, consent.userId);
const { data, total, pages } = await this.accountsService.getTransactions(accountId, {
fromBookingDate: query.fromBookingDate,
toBookingDate: query.toBookingDate,
page: query.page ?? 1,
pageSize: Math.min(query.pageSize ?? 25, 1000), // Máximo 1000 por página (spec)
});
return {
data,
links: this.buildPaginationLinks(`/accounts/${accountId}/transactions`, query, pages),
meta: {
totalRecords: total,
totalPages: pages,
requestDateTime: new Date().toISOString(),
},
};
}
private toAccountSummary(account: Account): AccountSummary {
return {
brandName: account.brandName,
companyCnpj: account.companyCnpj,
type: account.type, // CONTA_DEPOSITO_A_VISTA, CONTA_POUPANCA, etc.
compeCode: account.compeCode,
branchCode: account.branchCode,
number: account.number,
checkDigit: account.checkDigit,
accountId: account.id,
};
}
}
// src/auth/consent.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
@Injectable()
export class ConsentGuard implements CanActivate {
constructor(
private readonly consentService: ConsentService,
private readonly reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredPermission = this.reflector.get<OpenFinancePermission>(
'requiredPermission',
context.getHandler(),
);
const request = context.switchToHttp().getRequest();
const token = request.auth.decodedToken;
const result = await this.consentService.validateForApiCall(
token.consent_id,
requiredPermission,
token.client_id,
);
if (!result.isValid) {
throw new ForbiddenException(result.reason);
}
// Injeta o consentimento validado no contexto da requisição
request.validatedConsent = result.consent;
return true;
}
}
A Fase 3 do Open Finance é onde o dinheiro se move. O TPP (Terceiro Prestador) pode iniciar um pagamento Pix em nome do usuário, sem que o usuário precise ir ao app do banco.
// src/payments/payment-initiator.service.ts
import { Injectable } from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
interface PixPaymentRequest {
consentId: string;
localInstrument: 'MANU' | 'DICT' | 'QRDN' | 'QRES';
amount: {
currency: 'BRL';
amount: string; // "500.00" - string para evitar floating point
};
creditorAccount: {
ispb: string;
issuer?: string;
number: string;
accountType: 'CACC' | 'SVGS' | 'SLRY' | 'TRAN';
};
proxy?: string; // chave Pix
remittanceInformation?: string; // máx 140 chars
cnpjInitiator: string; // CNPJ do TPP
}
@Injectable()
export class PaymentInitiatorService {
constructor(
private readonly consentService: ConsentService,
private readonly pixClient: PixSPIClient,
private readonly paymentRepository: PaymentRepository,
private readonly idempotencyService: IdempotencyService,
private readonly eventBus: EventBus,
) {}
async initiatePixPayment(
request: PixPaymentRequest,
idempotencyKey: string,
clientCertThumbprint: string,
): Promise<PaymentResponse> {
// Idempotência: mesmo idempotency-key retorna resultado anterior
const existingResult = await this.idempotencyService.get<PaymentResponse>(idempotencyKey);
if (existingResult) return existingResult;
// Validação profunda do consentimento de pagamento
const consent = await this.validatePaymentConsent(request);
// Geração do EndToEndId único (formato BACEN: Ekkkkkkkkk...)
const endToEndId = this.generateEndToEndId();
const payment = await this.paymentRepository.create({
id: uuidv4(),
consentId: request.consentId,
endToEndId,
amount: request.amount.amount,
currency: request.amount.currency,
status: 'PDNG', // Pending
clientCertThumbprint,
createdAt: new Date(),
});
try {
// Instrução ao SPI (Sistema de Pagamentos Instantâneos)
const pixResult = await this.pixClient.initiatePayment({
endToEndId,
amount: request.amount.amount,
creditorAccount: request.creditorAccount,
proxy: request.proxy,
remittanceInformation: request.remittanceInformation,
initiatorCnpj: request.cnpjInitiator,
});
await this.paymentRepository.updateStatus(payment.id, pixResult.status);
// Consentimento de pagamento único é consumido após uso
await this.consentService.markAsConsumed(request.consentId);
const response: PaymentResponse = {
paymentId: payment.id,
endToEndId,
status: pixResult.status,
statusUpdateDateTime: new Date().toISOString(),
};
await this.idempotencyService.set(idempotencyKey, response, 86_400); // 24 horas
await this.eventBus.publish(new PaymentInitiatedEvent(response));
return response;
} catch (error) {
await this.paymentRepository.updateStatus(payment.id, 'RJCT');
throw this.mapPixError(error);
}
}
private async validatePaymentConsent(request: PixPaymentRequest): Promise<PaymentConsent> {
const consent = await this.consentService.findPaymentConsent(request.consentId);
if (!consent || consent.status !== 'AUTHORISED') {
throw new InvalidConsentError('Consentimento de pagamento inválido ou não autorizado');
}
if (consent.amount !== request.amount.amount) {
throw new InvalidConsentError(
`Valor do pagamento (${request.amount.amount}) difere do consentido (${consent.amount})`,
);
}
if (consent.creditorAccount.number !== request.creditorAccount.number) {
throw new InvalidConsentError('Conta destino difere do consentimento');
}
return consent;
}
private generateEndToEndId(): string {
// Formato BCB: E + ISPB (8 dígitos) + AAAAMMDD + HHmm + 11 chars aleatórios
const ispb = process.env.INSTITUTION_ISPB!;
const now = new Date();
const date = now.toISOString().slice(0, 10).replace(/-/g, '');
const time = now.toTimeString().slice(0, 5).replace(':', '');
const random = crypto.randomBytes(6).toString('hex').toUpperCase();
return `E${ispb}${date}${time}${random}`;
}
}
Open Insurance no Brasil (regulado pela SUSEP) expõe dados de apólices de seguros, planos de previdência e títulos de capitalização. Para uma InsurTech, isso é ouro.
// src/insurance/insurance-aggregator.service.ts
import { Injectable } from '@nestjs/common';
import pLimit from 'p-limit';
interface InsurancePortfolio {
lifeInsurance: LifeInsurancePolicy[];
autoInsurance: AutoInsurancePolicy[];
homeInsurance: HomeInsurancePolicy[];
pensionPlans: PensionPlan[];
capitalizationBonds: CapitalizationBond[];
totalPremiumMonthly: number;
expirationAlerts: ExpirationAlert[];
}
@Injectable()
export class InsuranceAggregatorService {
// Limita concorrência para não sobrecarregar transmissoras
private readonly concurrencyLimit = pLimit(5);
constructor(
private readonly openInsuranceClient: OpenInsuranceAPIClient,
private readonly consentService: ConsentService,
private readonly cacheService: CacheService,
) {}
async aggregatePortfolio(userId: string): Promise<InsurancePortfolio> {
const cacheKey = `insurance:portfolio:${userId}`;
const cached = await this.cacheService.get<InsurancePortfolio>(cacheKey);
if (cached) return cached;
const consents = await this.consentService.findActiveByUserId(userId, 'INSURANCE_READ');
// Busca dados de todas as transmissoras com consentimento ativo em paralelo
const results = await Promise.allSettled(
consents.map(consent =>
this.concurrencyLimit(() => this.fetchFromTransmitter(consent)),
),
);
const portfolio = this.mergeResults(results);
portfolio.expirationAlerts = this.detectExpirations(portfolio);
portfolio.totalPremiumMonthly = this.calculateTotalPremium(portfolio);
await this.cacheService.set(cacheKey, portfolio, 3600); // 1 hora
return portfolio;
}
private async fetchFromTransmitter(consent: Consent): Promise<Partial<InsurancePortfolio>> {
const [life, auto, home, pension] = await Promise.allSettled([
this.openInsuranceClient.getLifeInsurance(consent),
this.openInsuranceClient.getAutoInsurance(consent),
this.openInsuranceClient.getHomeInsurance(consent),
this.openInsuranceClient.getPensionPlans(consent),
]);
return {
lifeInsurance: life.status === 'fulfilled' ? life.value : [],
autoInsurance: auto.status === 'fulfilled' ? auto.value : [],
homeInsurance: home.status === 'fulfilled' ? home.value : [],
pensionPlans: pension.status === 'fulfilled' ? pension.value : [],
};
}
private detectExpirations(portfolio: InsurancePortfolio): ExpirationAlert[] {
const alerts: ExpirationAlert[] = [];
const thirtyDaysFromNow = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
[...portfolio.autoInsurance, ...portfolio.homeInsurance].forEach(policy => {
if (new Date(policy.expirationDate) <= thirtyDaysFromNow) {
alerts.push({
policyId: policy.policyId,
policyType: policy.type,
expirationDate: policy.expirationDate,
daysUntilExpiration: Math.ceil(
(new Date(policy.expirationDate).getTime() - Date.now()) / (24 * 60 * 60 * 1000),
),
});
}
});
return alerts.sort((a, b) => a.daysUntilExpiration - b.daysUntilExpiration);
}
private mergeResults(
results: PromiseSettledResult<Partial<InsurancePortfolio>>[],
): InsurancePortfolio {
return results.reduce<InsurancePortfolio>(
(acc, result) => {
if (result.status === 'fulfilled') {
acc.lifeInsurance.push(...(result.value.lifeInsurance ?? []));
acc.autoInsurance.push(...(result.value.autoInsurance ?? []));
acc.homeInsurance.push(...(result.value.homeInsurance ?? []));
acc.pensionPlans.push(...(result.value.pensionPlans ?? []));
}
return acc;
},
{
lifeInsurance: [],
autoInsurance: [],
homeInsurance: [],
pensionPlans: [],
capitalizationBonds: [],
expirationAlerts: [],
totalPremiumMonthly: 0,
},
);
}
}
O domínio de investimentos abre dados de renda fixa, fundos, renda variável e tesouro direto. Para uma plataforma de gestão de patrimônio, esses dados são o produto.
Eventos são o sistema nervoso do Open Finance. Consentimento criado, revogado, expirado; pagamento iniciado, confirmado, rejeitado; dados sincronizados, cache invalidado. Tudo gera eventos que múltiplos consumidores precisam processar.
// src/events/consent-events.ts
// Todos os eventos seguem o padrão CloudEvents 1.0
interface CloudEvent<T> {
specversion: '1.0';
id: string;
source: string; // ex: "//open-finance/consent-service"
type: string; // ex: "br.org.openfinancebrasil.consent.revoked"
time: string;
datacontenttype: 'application/json';
data: T;
}
interface ConsentRevokedPayload {
consentId: string;
userId: string;
clientId: string;
revokedBy: 'USER' | 'TPP' | 'INSTITUTION' | 'REGULATOR';
revokedAt: string;
permissions: OpenFinancePermission[];
}
interface PaymentInitiatedPayload {
paymentId: string;
consentId: string;
endToEndId: string;
amount: string;
currency: string;
status: PixPaymentStatus;
initiatedAt: string;
scheduledDate?: string;
}
type PixPaymentStatus =
| 'PDNG' // Pending - aguardando processamento
| 'PART' // Partially accepted
| 'ACSP' // Accepted settlement in process
| 'ACSC' // Accepted settlement completed
| 'ACCC' // Accepted creditor agent
| 'RJCT'; // Rejected
// src/events/reliable-event-producer.ts
import { Injectable, Logger } from '@nestjs/common';
import { Kafka, Producer, CompressionTypes } from 'kafkajs';
@Injectable()
export class ReliableEventProducer {
private readonly logger = new Logger(ReliableEventProducer.name);
private producer: Producer;
constructor(
private readonly kafka: Kafka,
private readonly outboxRepository: OutboxRepository,
) {}
async publish<T>(topic: string, event: CloudEvent<T>): Promise<void> {
// Padrão Transactional Outbox: persiste no banco antes de enviar ao Kafka
// Garante que o evento não se perde se o Kafka estiver indisponível
await this.outboxRepository.save({
id: event.id,
topic,
payload: JSON.stringify(event),
status: 'PENDING',
createdAt: new Date(),
});
// Tenta envio imediato; se falhar, o outbox poller vai reenviar
try {
await this.producer.send({
topic,
compression: CompressionTypes.GZIP,
messages: [
{
key: event.id,
value: JSON.stringify(event),
headers: {
'content-type': 'application/cloudevents+json',
'ce-specversion': event.specversion,
'ce-type': event.type,
'ce-source': event.source,
},
},
],
});
await this.outboxRepository.markAsSent(event.id);
} catch (error) {
this.logger.warn(`Kafka indisponível. Evento ${event.id} ficará no outbox para retry.`);
// Não propaga: o outbox garante reentrega
}
}
}
// src/auth/mtls-validation.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
@Injectable()
export class MTLSValidationMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction): void {
// Em produção, o load balancer (AWS ALB/NGINX) injeta o certificado do cliente
// nos headers após verificar o mTLS na camada de rede
const clientCertPEM = req.headers['x-ssl-client-cert'] as string;
if (!clientCertPEM) {
res.status(401).json({
errors: [{
title: 'Certificado de cliente ausente',
detail: 'Autenticação mTLS obrigatória para este endpoint',
}],
});
return;
}
try {
const cert = new crypto.X509Certificate(decodeURIComponent(clientCertPEM));
// Valida que o certificado é ICP-Brasil (cadeia até a AC Raiz)
this.validateICPBrasilChain(cert);
// Computa thumbprint SHA-256 do certificado
// Será comparado com o claim cnf.x5t no JWT
const thumbprint = crypto
.createHash('sha256')
.update(cert.raw)
.digest('base64url');
req['clientCertThumbprint'] = thumbprint;
req['clientCert'] = cert;
next();
} catch (error) {
res.status(401).json({
errors: [{
title: 'Certificado inválido',
detail: 'Certificado mTLS não pôde ser validado',
}],
});
}
}
private validateICPBrasilChain(cert: crypto.X509Certificate): void {
const issuer = cert.issuer;
// Verifica que o emissor é uma AC credenciada pela ICP-Brasil
// Em produção, validar contra lista de ACs do ITI (iti.gov.br)
const ICP_BRASIL_AC_ROOTS = [
'CN=ICP-Brasil, O=ICP-Brasil, C=BR',
'CN=AC SERPRO RFB v5, O=ICP-Brasil, C=BR',
// ... outras ACs credenciadas
];
const isICPBrasil = ICP_BRASIL_AC_ROOTS.some(root => issuer.includes(root.split(',')[0]));
if (!isICPBrasil) {
throw new Error(`Certificado não emitido por AC ICP-Brasil: ${issuer}`);
}
if (new Date(cert.validTo) < new Date()) {
throw new Error('Certificado expirado');
}
}
}
// src/audit/audit-log.service.ts
interface AuditLogEntry {
id: string;
timestamp: string;
organizationId: string;
clientId: string;
consentId?: string;
userId?: string;
action: string;
endpoint: string;
method: string;
statusCode: number;
durationMs: number;
requestId: string;
clientCertThumbprint: string;
previousHash: string; // Hash da entrada anterior (imutabilidade)
hash: string; // Hash desta entrada
}
@Injectable()
export class AuditLogService {
constructor(private readonly auditRepository: AuditRepository) {}
async log(entry: Omit<AuditLogEntry, 'id' | 'hash' | 'previousHash'>): Promise<void> {
const previousEntry = await this.auditRepository.findLatest();
const previousHash = previousEntry?.hash ?? '0'.repeat(64);
const id = uuidv4();
const timestamp = new Date().toISOString();
const payload = JSON.stringify({ ...entry, id, timestamp, previousHash });
const hash = crypto.createHash('sha256').update(payload).digest('hex');
await this.auditRepository.insert({
...entry,
id,
timestamp,
previousHash,
hash,
});
}
}
-- Tabela de auditoria particionada por mês (5 anos de retenção)
CREATE TABLE consent_audit_log (
id UUID NOT NULL,
consent_id UUID NOT NULL,
timestamp TIMESTAMPTZ NOT NULL,
action VARCHAR(100) NOT NULL,
status_code INTEGER NOT NULL,
duration_ms INTEGER NOT NULL,
hash CHAR(64) NOT NULL,
previous_hash CHAR(64) NOT NULL,
client_cert_thumbprint VARCHAR(100) NOT NULL
) PARTITION BY RANGE (timestamp);
-- Criação automática de partições mensais
CREATE TABLE consent_audit_log_2026_01
PARTITION OF consent_audit_log
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
-- Índices nas partições para queries regulatórias
CREATE INDEX CONCURRENTLY ON consent_audit_log_2026_01 (consent_id, timestamp DESC);
CREATE INDEX CONCURRENTLY ON consent_audit_log_2026_01 (action, timestamp DESC);
-- Política de expiração: mover para S3 após 90 dias
-- Usar aws_s3 extension do PostgreSQL ou pg_cron para automação
O Open Finance tem uma característica única: você depende de APIs de terceiros (as transmissoras) que você não controla. Elas podem estar lentas, fora do ar ou retornando dados incorretos.
// src/resilience/circuit-breaker.service.ts
import { Injectable } from '@nestjs/common';
import CircuitBreaker from 'opossum';
@Injectable()
export class TransmitterCircuitBreakerFactory {
private readonly breakers = new Map<string, CircuitBreaker>();
create<T>(transmitterId: string, action: () => Promise<T>): CircuitBreaker<[], T> {
const existing = this.breakers.get(transmitterId);
if (existing) return existing as CircuitBreaker<[], T>;
const breaker = new CircuitBreaker(action, {
timeout: 5000, // Timeout por requisição: 5s (limite BCB: 15s)
errorThresholdPercentage: 50, // Abre com 50% de falhas
resetTimeout: 30000, // Tenta fechar após 30s
volumeThreshold: 5, // Mínimo 5 requisições para calcular %
});
breaker.fallback(() => this.getCachedData(transmitterId));
breaker.on('open', () => {
this.metrics.increment('circuit_breaker.open', { transmitter: transmitterId });
this.alerts.notify(`Circuit breaker aberto para ${transmitterId}`);
});
breaker.on('halfOpen', () => {
this.metrics.increment('circuit_breaker.half_open', { transmitter: transmitterId });
});
this.breakers.set(transmitterId, breaker);
return breaker as CircuitBreaker<[], T>;
}
}
// src/resilience/retry.decorator.ts
interface RetryOptions {
maxAttempts: number;
initialDelayMs: number;
maxDelayMs: number;
retryableErrors: (new (...args: unknown[]) => Error)[];
}
export function WithRetry(options: RetryOptions): MethodDecorator {
return (_target, _key, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
let lastError: Error;
let delayMs = options.initialDelayMs;
for (let attempt = 1; attempt <= options.maxAttempts; attempt++) {
try {
return await originalMethod.apply(this, args);
} catch (error) {
lastError = error as Error;
const isRetryable = options.retryableErrors.some(
ErrorClass => error instanceof ErrorClass,
);
if (!isRetryable || attempt === options.maxAttempts) {
throw error;
}
// Jitter para evitar thundering herd
const jitter = Math.random() * 0.1 * delayMs;
await sleep(delayMs + jitter);
delayMs = Math.min(delayMs * 2, options.maxDelayMs);
}
}
throw lastError!;
};
return descriptor;
};
}
// src/aggregation/resilient-aggregator.service.ts
@Injectable()
export class ResilientAggregatorService {
async aggregateFinancialData(userId: string): Promise<AggregatedData> {
const consents = await this.consentService.findActiveByUserId(userId);
const results = await Promise.allSettled(
consents.map(consent =>
this.fetchWithTimeout(consent, 8000) // 8s max por transmissora
),
);
const successful = results.filter(r => r.status === 'fulfilled');
const failed = results.filter(r => r.status === 'rejected');
if (successful.length === 0 && failed.length > 0) {
throw new AllTransmittersFailed('Nenhuma transmissora respondeu com sucesso');
}
const aggregated = this.mergeData(successful.map(r => (r as PromiseFulfilledResult<any>).value));
// Retorna dados parciais com indicação das fontes que falharam
return {
...aggregated,
metadata: {
sourcesTotal: consents.length,
sourcesSuccess: successful.length,
sourcesFailed: failed.length,
isPartial: failed.length > 0,
failedInstitutions: this.extractFailedInstitutions(failed, consents),
lastUpdated: new Date().toISOString(),
},
};
}
private async fetchWithTimeout<T>(consent: Consent, timeoutMs: number): Promise<T> {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await this.openFinanceClient.fetch(consent, { signal: controller.signal });
return result;
} finally {
clearTimeout(timeoutId);
}
}
}
| SLO | Meta | Janela | Alerta |
|---|---|---|---|
| Disponibilidade APIs Dados | ≥ 99,9% | 30 dias | < 99,5% |
| Disponibilidade API Pagamento | ≥ 99,95% | 30 dias | < 99,8% |
| Latência p95 (dados) | ≤ 1.000ms | 1 hora | > 800ms |
| Latência p95 (pagamento) | ≤ 500ms | 1 hora | > 400ms |
| Revogação de consentimento | ≤ 5s | Instantâneo | > 3s |
| Taxa de erro 5xx | ≤ 0,1% | 1 hora | > 0,05% |
// src/observability/tracing.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
export function initializeTracing(): void {
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'open-finance-api',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION ?? '1.0.0',
'open_finance.institution_id': process.env.INSTITUTION_ISPB ?? '',
'open_finance.environment': process.env.NODE_ENV ?? 'production',
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
}),
instrumentations: [
// Auto-instrumentação para HTTP, PostgreSQL, Redis, Kafka
],
});
sdk.start();
}
// Decorador para adicionar atributos de negócio ao trace
export function TraceOpenFinance(operation: string): MethodDecorator {
return (_target, _key, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = async function (...args: unknown[]) {
const tracer = trace.getTracer('open-finance');
const span = tracer.startSpan(`open_finance.${operation}`);
return context.with(trace.setSpan(context.active(), span), async () => {
try {
const result = await originalMethod.apply(this, args);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
};
return descriptor;
};
}
| Dado | TTL | Estratégia de Invalidação | Motivo |
|---|---|---|---|
| Validação de consentimento | 30s | Evento de revogação | Equilíbrio entre performance e compliance |
| JWKS do Diretório | 30min | TTL-based | Chaves mudam raramente |
| Perfil do participante | 15min | Sync periódica | Mudanças lentas |
| Saldo de conta | 60s | TTL-based | Dado sensível e mutável |
| Dados cadastrais | 5min | TTL-based | Muda raramente |
| Transações (históricas) | 10min | TTL-based | Imutáveis após D+1 |
| Cotação de seguros | 1h | TTL-based | Prêmios mudam diariamente |
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
FROM node:20-alpine AS production
# Cria usuário não-root (segurança)
RUN addgroup -g 1001 -S openfinance && \
adduser -S openfinance -u 1001 -G openfinance
WORKDIR /app
COPY --from=builder --chown=openfinance:openfinance /app/dist ./dist
COPY --from=builder --chown=openfinance:openfinance /app/node_modules ./node_modules
COPY --from=builder --chown=openfinance:openfinance /app/package.json ./
USER openfinance
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]
Vamos simular o fluxo completo de uma fintech de crédito que usa Open Finance para oferecer crédito personalizado.
// src/credit/credit-scoring.service.ts
interface CreditScoreInput {
transactions: Transaction[];
accountBalances: AccountBalance[];
creditOperations: CreditOperation[];
consentPeriodMonths: number;
}
interface CreditScoreResult {
score: number; // 0-1000
riskCategory: 'VERY_LOW' | 'LOW' | 'MEDIUM' | 'HIGH' | 'VERY_HIGH';
estimatedMonthlyIncome: number;
averageMonthlyExpenses: number;
savingsRate: number;
creditUtilization: number;
paymentHistoryScore: number;
stabilityScore: number;
}
@Injectable()
export class BehavioralCreditScoringService {
async calculateScore(userId: string): Promise<CreditScoreResult> {
const portfolio = await this.aggregatorService.aggregateFinancialData(userId);
const monthlyIncomes = this.identifyIncomeTransactions(portfolio.transactions);
const monthlyExpenses = this.categorizeExpenses(portfolio.transactions);
const creditBehavior = this.analyzeCreditBehavior(portfolio.creditOperations);
const averageMonthlyIncome = this.calculateAverage(monthlyIncomes);
const averageMonthlyExpenses = this.calculateAverage(
monthlyExpenses.map(m => m.total),
);
const savingsRate = (averageMonthlyIncome - averageMonthlyExpenses) / averageMonthlyIncome;
const creditUtilization = creditBehavior.averageUtilization;
const paymentHistoryScore = creditBehavior.onTimePaymentRate;
// Algoritmo de scoring ponderado
// (em produção: modelo ML treinado com dados históricos de inadimplência)
const score = Math.round(
savingsRate * 200 +
(1 - creditUtilization) * 300 +
paymentHistoryScore * 300 +
creditBehavior.stabilityScore * 200,
);
return {
score: Math.min(1000, Math.max(0, score)),
riskCategory: this.categorizeRisk(score),
estimatedMonthlyIncome: averageMonthlyIncome,
averageMonthlyExpenses,
savingsRate,
creditUtilization,
paymentHistoryScore,
stabilityScore: creditBehavior.stabilityScore,
};
}
private identifyIncomeTransactions(transactions: Transaction[]): number[] {
const byMonth = this.groupByMonth(transactions);
return Object.values(byMonth).map(monthTransactions =>
monthTransactions
.filter(t => t.creditDebitType === 'CREDITO' && t.amount > 500) // heurística
.reduce((sum, t) => sum + t.amount, 0),
);
}
private categorizeRisk(score: number): CreditScoreResult['riskCategory'] {
if (score >= 800) return 'VERY_LOW';
if (score >= 650) return 'LOW';
if (score >= 500) return 'MEDIUM';
if (score >= 350) return 'HIGH';
return 'VERY_HIGH';
}
}
Uma InsurTech usa Open Finance para precificar seguro auto com base no comportamento financeiro real do cliente, complementando dados de telemetria.
| Anti-Pattern | Problema | Solução |
|---|---|---|
| Verificar consentimento só no login | Consentimento pode ser revogado a qualquer momento | Verificar em cada chamada de API |
| Cache longo de consentimento | Usuário revoga mas sistema continua servindo dados | TTL máximo de 30s + invalidação por evento |
| Pooling de consentimentos | Reutilizar consentimento de um usuário para outro | Consentimento é individual e intransferível |
| Ignorar o certificado no token binding | Vulnerabilidade: token roubado pode ser usado com outro cert | Verificar thumbprint do cert == cnf.x5t no JWT |
| Retry sem backoff em transmissoras | Thundering herd: derruba a transmissora quando ela já está com problemas | Exponential backoff com jitter obrigatório |
| Dados sem pseudoanonimização no log | Violação LGPD: dados pessoais em logs de debug | Mascarar CPF, conta, dados sensíveis nos logs |
| Sync síncrono com Diretório em cada request | Latência inaceitável; Diretório pode estar fora | Cache local com sync periódica em background |
| Imutabilidade sem hash chain | Logs podem ser alterados sem detecção | Hash chain garante integridade da trilha |
| Consentimento sem escopo mínimo | Pedir mais permissões que o necessário (viola LGPD) | Princípio da minimização de dados |
| Aceitar qualquer certificado | Aceitar cert não-ICP-Brasil abre vulnerabilidade | Validar cadeia até AC-Raiz ICP-Brasil |
// ❌ ERRADO: Pede todas as permissões possíveis
const badConsentRequest = {
permissions: [
'ACCOUNTS_READ',
'ACCOUNTS_TRANSACTIONS_READ',
'CREDIT_CARDS_ACCOUNTS_READ',
'LOANS_READ',
'INVESTMENTS_READ',
'INSURANCE_READ',
'PAYMENTS_INITIATE',
// ... tudo mais que existe
],
};
// ✅ CORRETO: Pede apenas o necessário para a funcionalidade
const goodConsentRequest = {
permissions: [
// Para análise de crédito: apenas transações e crédito existente
'ACCOUNTS_READ',
'ACCOUNTS_TRANSACTIONS_READ',
'LOANS_READ',
],
// Explica ao usuário por que cada permissão é necessária
permissionRationale: {
ACCOUNTS_READ: 'Para identificar suas contas e confirmar titularidade',
ACCOUNTS_TRANSACTIONS_READ: 'Para analisar seu histórico financeiro dos últimos 12 meses',
LOANS_READ: 'Para verificar compromissos financeiros existentes',
},
};
1. "Como você garante que o consentimento revogado para de funcionar imediatamente?"
Resposta ideal: combinação de três mecanismos:
2. "Como você lida com a indisponibilidade de uma transmissora?"
Resposta ideal:
isStale: true3. "O que é mTLS e por que o Open Finance exige?"
mTLS = Mutual TLS: ambos os lados apresentam certificados. No Open Finance:
4. "Como você dimensiona para 10.000 TPS?"
5. "Qual a diferença entre Open Banking e Open Finance?"
Open Banking: dados bancários (contas, cartões, crédito, pagamentos) Open Finance: tudo do Open Banking + seguros, previdência, investimentos, câmbio
Brasil foi além: Open Finance aqui inclui todos os domínios financeiros regulados.
1. CLARIFY → Quais dados? Escala? Transmissora ou receptora?
2. REQUIREMENTS → Funcionais + Não-funcionais + Regulatórios
3. ESTIMATE → Volume, armazenamento, TPS
4. HIGH-LEVEL → Diagrama com os principais componentes
5. DEEP-DIVE → Auth Server, Consent Service, API Layer
6. TRADE-OFFS → Por que essas escolhas? O que você sacrificou?
7. OPERABILITY → Como você monitora? Como você debugga?
O Open Finance brasileiro é um dos projetos de modernização financeira mais ambiciosos do mundo. Diferente de iniciativas que param no acesso a dados bancários, o Brasil criou um ecossistema que conecta bancos, seguradoras, gestoras de investimento e fintechs sob um protocolo único.
Para quem constrói nesse ecossistema, os desafios técnicos são reais:
Segurança não é opcional. FAPI 2.0, mTLS, token binding e ICP-Brasil não são detalhes de implementação. São requisitos que definem se você pode operar no ecossistema.
Consentimento é central. Cada linha de código que acessa dados de usuário precisa verificar se existe consentimento ativo, granular e não expirado. Essa verificação precisa ser rápida (cache) mas também imediata na revogação (eventos).
Resiliência é mandatória. Você depende de APIs de terceiros que você não controla. Circuit breakers, timeouts, graceful degradation e dados parciais são a diferença entre um produto que funciona e um que falha junto com qualquer transmissora.
Compliance é produto. Latência p95, disponibilidade, logs auditáveis e retenção de 5 anos não são features técnicas. São o produto que você entrega ao regulador mensalmente.
A fintech que entender isso profundamente — e construir sistemas que honram esses princípios — vai operar no Open Finance com vantagem competitiva durável. As que ignorarem vão tropeçar na regulação, nos SLAs e na confiança do usuário.
O dinheiro, no Brasil de 2026, flui por APIs. Saiba desenhá-las.
Banco Central do Brasil — Resolução Conjunta nº 1, de 4 de maio de 2020 (Marco do Open Finance)
https://www.bcb.gov.br/estabilidadefinanceira/openfinance
Open Finance Brasil — Especificações de APIs (versão 2.x)
https://openfinancebrasil.org.br/especificacoes/
Open Finance Brasil — Perfil de Segurança FAPI 2.0
https://openfinancebrasil.org.br/wp-content/uploads/2022/04/open-banking-brasil-financial-grade-api-security-profile-1_ID3.pdf
SUSEP — Resolução CNSP nº 415, de 20 de dezembro de 2021 (Open Insurance)
https://www.in.gov.br/en/web/dou/-/resolucao-cnsp-n-415-de-20-de-dezembro-de-2021
Banco Central do Brasil — Sistema de Pagamentos Instantâneos (Pix)
https://www.bcb.gov.br/estabilidadefinanceira/pix
IETF RFC 9126 — OAuth 2.0 Pushed Authorization Requests (PAR)
https://tools.ietf.org/html/rfc9126
IETF RFC 9101 — The OAuth 2.0 Authorization Framework: JWT-Secured Authorization Request (JAR)
https://tools.ietf.org/html/rfc9101
IETF RFC 8705 — OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens
https://tools.ietf.org/html/rfc8705
OpenID Foundation — Financial-grade API (FAPI) 2.0 Security Profile
https://openid.net/specs/fapi-2_0-security-profile.html
IETF RFC 7636 — Proof Key for Code Exchange (PKCE)
https://tools.ietf.org/html/rfc7636
Martin Fowler — Patterns of Enterprise Application Architecture. Addison-Wesley, 2002
Martin Fowler — Microservices: a definition of this new architectural term
https://martinfowler.com/articles/microservices.html
Chris Richardson — Microservices Patterns. Manning, 2018
Robert C. Martin (Uncle Bob) — Clean Architecture: A Craftsman's Guide. Prentice Hall, 2017
Gregor Hohpe, Bobby Woolf — Enterprise Integration Patterns. Addison-Wesley, 2003
Sam Newman — Building Microservices, 2nd Edition. O'Reilly, 2021
Alex Xu — System Design Interview, Volume 1 e 2. ByteByteGo, 2020/2022
Apache Kafka — Documentation: Kafka Design
https://kafka.apache.org/documentation/#design
Netflix Engineering Blog — Hystrix: Latency and Fault Tolerance for Distributed Systems
https://netflixtechblog.com/hystrix-dashboard-turbine-stream-aggregator-60985a2e51df
OpenTelemetry — Specification v1.x
https://opentelemetry.io/docs/specs/
OWASP — API Security Top 10
https://owasp.org/www-project-api-security/
Amazon Web Services — Financial Services on AWS
https://aws.amazon.com/financial-services/
Open Finance Brasil GitHub — Especificações técnicas abertas
https://github.com/OpenBanking-Brasil
Febraban Tech — Open Finance: Guia de Implementação para Fintechs
https://febrabantech.febraban.org.br/
BCB Open Finance Portal — Sandbox e ambiente de testes
https://openfinancebrasil.atlassian.net/wiki/spaces/OF/overview
# Consentimentos
POST /open-banking/consents/v3/consents
GET /open-banking/consents/v3/consents/{consentId}
DELETE /open-banking/consents/v3/consents/{consentId}
# Contas
GET /open-banking/accounts/v2/accounts
GET /open-banking/accounts/v2/accounts/{accountId}
GET /open-banking/accounts/v2/accounts/{accountId}/balances
GET /open-banking/accounts/v2/accounts/{accountId}/transactions
GET /open-banking/accounts/v2/accounts/{accountId}/transactions-current
GET /open-banking/accounts/v2/accounts/{accountId}/overdraft-limits
# Cartão de Crédito
GET /open-banking/credit-cards-accounts/v2/accounts
GET /open-banking/credit-cards-accounts/v2/accounts/{creditCardAccountId}
GET /open-banking/credit-cards-accounts/v2/accounts/{creditCardAccountId}/limits
GET /open-banking/credit-cards-accounts/v2/accounts/{creditCardAccountId}/transactions
GET /open-banking/credit-cards-accounts/v2/accounts/{creditCardAccountId}/bills
# Pagamentos (Fase 3)
POST /open-banking/payments/v4/pix/payment-consents
GET /open-banking/payments/v4/pix/payment-consents/{consentId}
POST /open-banking/payments/v4/pix/payments
GET /open-banking/payments/v4/pix/payments/{paymentId}
# Investimentos (Fase 4)
GET /open-banking/bank-fixed-incomes/v1/investments
GET /open-banking/variable-incomes/v1/broker-notes
GET /open-banking/funds/v1/investments
GET /open-banking/treasure-titles/v1/investments
| HTTP Status | Significado | Quando Usar |
|---|---|---|
| 200 | OK | Sucesso com corpo de resposta |
| 201 | Created | Recurso criado (POST) |
| 204 | No Content | Sucesso sem corpo (DELETE) |
| 400 | Bad Request | Requisição malformada |
| 401 | Unauthorized | Token ausente ou inválido |
| 403 | Forbidden | Consentimento insuficiente |
| 404 | Not Found | Recurso não encontrado |
| 405 | Method Not Allowed | Método HTTP não permitido |
| 422 | Unprocessable Entity | Violação de regra de negócio |
| 429 | Too Many Requests | Rate limit atingido |
| 503 | Service Unavailable | Indisponibilidade planejada |
Fase 2 - Dados do Cliente:
ACCOUNTS_READ, ACCOUNTS_BALANCES_READ, ACCOUNTS_TRANSACTIONS_READ
CREDIT_CARDS_ACCOUNTS_READ, CREDIT_CARDS_ACCOUNTS_BILLS_READ
LOANS_READ, FINANCINGS_READ, UNARRANGED_ACCOUNTS_OVERDRAFT_READ
Fase 3 - Pagamentos:
PAYMENTS_INITIATE
Fase 4 - Dados Adicionais:
BANK_FIXED_INCOMES_READ, CREDIT_FIXED_INCOMES_READ
FUNDS_READ, VARIABLE_INCOMES_READ, TREASURE_TITLES_READ
RESOURCES_READ (Open Insurance)
LIFE_PENSION_READ, PENSION_PLAN_READ
☐ Certificados ICP-Brasil instalados e monitorados
☐ FAPI 2.0 profile implementado (PAR, JAR, PKCE, mTLS)
☐ Token binding (cnf.x5t) verificado em cada request
☐ Diretório de Participantes sincronizado a cada 15 minutos
☐ Revogação de consentimento propagada em < 5 segundos
☐ Latência p95 ≤ 1.000ms (requisito BCB)
☐ Disponibilidade ≥ 99,5% mensais (requisito mínimo BCB)
☐ Logs de auditoria com hash chain, retenção 5 anos
☐ Dados PII pseudoanonimizados em logs
☐ Permissões mínimas necessárias (princípio minimização LGPD)
☐ Respostas paginadas com cursor (não offset)
☐ Rate limiting implementado por clientId + endpoint
☐ Sandbox BACEN testado antes de produção