Desafio 15: Payment Initiation (PISP) — Iniciando Pagamentos por Open Finance
🇧🇷 Iniciador de Transação de Pagamento
🇬🇧 Payment Initiation Service Provider
O PISP (ou ISP) é a figura introduzida pela Fase 3 do Open Finance Brasil que permite que terceiros iniciem pagamentos diretamente da conta bancária do cliente — sem cartão, sem boleto, sem PIX manual. É a base do Pix Automático e Pagamento por Débito em Conta moderno.
Switch: TypeScript vs Go
O que é um PISP?
| Caso de Uso | Exemplo |
|---|---|
| Checkout e-commerce | Pagar com 1 clique |
| Assinaturas recorrentes | Netflix, Spotify |
| Food delivery | iFood, Rappi |
| Mobilidade | Uber, 99 |
| Marketplaces | Mercado Livre, Shopee |
Ecossistema Completo
Fluxo Detalhado
APIs de Pagamento
| Endpoint | Descrição |
|---|---|
POST /consents | Consentimento único |
POST /recurring-consents | Para recorrência |
POST /pix/payments | Inicia PIX |
GET /pix/payments/{id} | Consulta status |
POST /automatic-payments | Pix Automático |
Domain — Payment Entity
typescript
export enum PaymentStatus {
CREATED = 'CREATED',
PENDING = 'PDNG',
ACCEPTED_CREDIT = 'ACSC',
REJECTED = 'RJCT',
CANCELLED = 'CANC',
}
export class PaymentInitiation extends Entity<string> {
public confirm(endToEndId: string): void {
this.props.status = PaymentStatus.ACCEPTED_CREDIT;
this.props.endToEndId = endToEndId;
this.props.confirmedAt = new Date();
}
public reject(reason: string): void {
this.props.status = PaymentStatus.REJECTED;
this.props.rejectionReason = reason;
}
public canBeProcessed(): boolean {
return !this.isConfirmed() && !this.isRejected()
&& new Date() <= this.props.expiresAt;
}
}Payment Consent (diferente do Consent de dados)
typescript
export class PaymentConsent {
public canBeUsed(): boolean {
return this.isAuthorised()
&& new Date() <= this.props.expirationDateTime;
}
public validatePayment(amount: number): boolean {
if (!this.canBeUsed()) return false;
if (this.props.type === PaymentConsentType.SINGLE) {
return this.props.amount === amount;
}
if (this.props.recurringPolicy?.maxAmountPerTransaction) {
return amount <= this.props.recurringPolicy.maxAmountPerTransaction;
}
return true;
}
public consume(): void {
if (this.props.type === PaymentConsentType.SINGLE) {
this.props.status = PaymentConsentStatus.CONSUMED;
}
}
}Payment Initiator Service
typescript
export class PaymentInitiatorService {
public async initiate(input: InitiatePaymentInput, pispClientId: string) {
// 1. Idempotência
const existing = await this.idempotencyService.check(input.idempotencyKey);
if (existing) return right(existing);
// 2. Valida consentimento
const consent = await this.fapiClient.getPaymentConsent(pispClientId, input.consentId);
if (!consent.canBeUsed()) return left(new ConsentNotActiveError());
if (!consent.validatePayment(input.amount)) return left(new ExceedsLimitError());
// 3. Fraud check
const fraudCheck = await this.fraudService.preInitiationCheck({ ... });
if (fraudCheck.isHighRisk) return left(new FraudDetectedError());
// 4. Cria pagamento
const payment = PaymentInitiation.create({ ... });
await this.paymentRepo.save(payment);
// 5. Envia ao banco via FAPI
const result = await this.fapiClient.initiatePayment(pispClientId, { ... });
if (result.value.status === 'ACSC') payment.confirm(result.value.endToEndId);
else payment.reject(result.value.reason);
// 6. Consome consent (single)
if (consent.type === 'SINGLE') consent.consume();
// 7. Publica evento
await this.eventPublisher.publish('payment.initiated', { ... });
return right(payment);
}
}FAPI Client — Comunicação com Bancos
typescript
export class FAPIClient {
public async initiatePayment(clientId: string, payment: FAPIPaymentRequest) {
const bankEndpoint = await this.directoryService.findConsentEndpoint(payment.consentId);
const token = await this.getAccessToken(clientId, payment.consentId);
// Monta payload e assina com JWS (PS256)
const signedRequest = await this.signRequest({
data: { consentId: payment.consentId, amount: payment.amount, ... }
}, clientId);
const response = await this.tlsClient.post(
`${bankEndpoint}/open-banking/payments/v1/payments`,
{ headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/jwt' }, body: signedRequest }
);
return this.mapPaymentResponse(await response.json());
}
private generateClientAssertion(clientId: string, audience: string): string {
return sign({ iss: clientId, sub: clientId, aud: `${audience}/token`, jti: uuidv4(), exp: now + 60 },
this.privateKey, { algorithm: 'PS256', header: { kid: this.keyId, typ: 'JWT' } });
}
}Fluxo de Recorrência (Pix Automático)
Comparação: TypeScript vs Go
| Aspecto | TypeScript | Go |
|---|---|---|
| FAPI/JWT | jose, jsonwebtoken | golang-jwt/jwt |
| mTLS | TLS nativo | net/http mTLS |
| Performance | ~3K req/s | ~30K req/s |
| Memory | ~500MB | ~50MB |
| Latência P99 | 30-100ms | 5-20ms |
| Ecossistema | Rico (SDKs prontos) | Menos libs FAPI |
Casos Reais
- Mercado Pago (Go) — Maior PISP, 30K+ TPS, Pix Automático
- PicPay (Go + TS) — 40M+ usuários, recorrência massiva
- iFood (Go) — Checkout em escala, multi-bank
- Pluggy/Belvo (Go) — Infraestrutura PISP B2B
Como testar
bash
# TypeScript
pnpm --filter @banking/pisp dev
# Go
cd packages/backend/pisp-go
go run .
# Iniciar pagamento
curl -X POST http://localhost:3007/api/v1/payments/initiate \
-H "Content-Type: application/json" \
-H "x-pisp-client-id: fintech-abc" \
-d '{"consentId":"uuid","idempotencyKey":"uuid","amount":5000,"creditor":{"name":"Loja","document":"12345678901","account":{"ispb":"12345678","number":"12345","accountType":"CACC"}}}'Lições aprendidas
- PISP = Próxima fronteira — Após PIX e Open Finance
- Pix Automático — Recorrência sem nova aprovação
- FAPI obrigatório — mTLS + PS256 + PKCE
- Idempotência crítica — Pagamentos nunca duplicados
- Webhooks com JWS — Validação de assinatura
- State machine — CREATED → PDNG → ACSC (ou RJCT)
- Reconciliação diária — Com cada banco detentor
- Go domina alta escala — 5-15x mais rápido
- Consent de pagamento ≠ Consent de dados — Regras diferentes
- Smart routing — Escolhe melhor banco para cada pagamento