Pular para o conteúdo principal
Nesta página, você encontrará práticas recomendadas para implementar transferências bancárias na sua aplicação, cobrindo aspectos de segurança, experiência do usuário, tratamento de erros e conformidade regulatória.

Fluxo de duas etapas


O fluxo de duas etapas (initiateprocess) oferece vantagens importantes:

Exiba a tarifa antes da confirmação

Permitir que o usuário revise a tarifa antes de confirmar evita surpresas e reduz reclamações.

Exemplo de confirmação

┌─────────────────────────────────────────────────┐
│  Confirmar transferência                        │
│                                                 │
│  Destinatário: Maria Silva                      │
│  Banco: Bradesco (237)                          │
│                                                 │
│  Valor: R$ 1.500,00                             │
│  Tarifa: R$ 8,50                                │
│  ─────────────────────────                      │
│  Total: R$ 1.508,50                             │
│                                                 │
│  [ Cancelar ]              [ Confirmar ]        │
└─────────────────────────────────────────────────┘

Use a expiração a seu favor

A iniciação expira em 24 horas. Isso permite:
  • Salvar a intenção do usuário sem comprometer fundos
  • Retomar o fluxo se o usuário abandonar a tela
  • Validar dados sem impacto no saldo

Tratamento de horário


Valide o horário no cliente

Antes de chamar a API, verifique se está dentro do horário de funcionamento:
function isWithinOperatingHours() {
  const now = new Date();
  const brasiliaOffset = -3 * 60; // UTC-3
  const utc = now.getTime() + now.getTimezoneOffset() * 60000;
  const brasilia = new Date(utc + brasiliaOffset * 60000);

  const day = brasilia.getDay();
  const hour = brasilia.getHours();
  const minute = brasilia.getMinutes();
  const time = hour * 60 + minute;

  // Seg-Sex (1-5), 06:30-17:00
  const isWeekday = day >= 1 && day <= 5;
  const isWithinHours = time >= 390 && time <= 1020; // 6:30=390, 17:00=1020

  return isWeekday && isWithinHours;
}

Comunique claramente ao usuário

Se estiver fora do horário, informe quando a transferência poderá ser realizada:
TED disponível de segunda a sexta, das 06:30 às 17:00.
Próximo horário disponível: segunda-feira, 06:30.

Considere feriados

O plugin não valida feriados bancários, por isso, mantenha um calendário atualizado:
const feriadosBancarios2026 = [
  '2026-01-01', // Confraternização Universal
  '2026-02-16', // Carnaval
  '2026-02-17', // Carnaval
  '2026-04-03', // Sexta-feira Santa
  '2026-04-21', // Tiradentes
  '2026-05-01', // Dia do Trabalho
  '2026-06-04', // Corpus Christi
  '2026-09-07', // Independência
  '2026-10-12', // Nossa Senhora Aparecida
  '2026-11-02', // Finados
  '2026-11-15', // Proclamação da República
  '2026-12-25', // Natal
];

Idempotência


Use chaves de idempotência

Para operações de escrita, sempre envie o header X-Idempotency-Key:
POST /v1/transfers/initiate
X-Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Isso protege contra:
  • Duplo clique do usuário
  • Retry automático em caso de timeout
  • Falhas de rede que mascaram respostas de sucesso

Gere chaves no cliente

Gere a chave de idempotência antes de fazer a requisição e armazene-a até confirmar o resultado:
async function initiateTransfer(data) {
  const idempotencyKey = crypto.randomUUID();

  // Salva a chave localmente
  await localStorage.setItem(`transfer:${data.senderAccountId}`, idempotencyKey);

  try {
    const response = await api.post('/v1/transfers/initiate', data, {
      headers: { 'X-Idempotency-Key': idempotencyKey },
    });
    return response.data;
  } catch (error) {
    if (error.code === 'NETWORK_ERROR') {
      // Pode reenviar com a mesma chave
      return retryWithSameKey(data, idempotencyKey);
    }
    throw error;
  }
}

Tratamento de erros


Mapeie erros para mensagens amigáveis

const errorMessages = {
  'BTF-0010': 'Transferências disponíveis de segunda a sexta, das 06:30 às 17:00.',
  'BTF-0011': 'Você atingiu o limite diário de transferências.',
  'BTF-0012': 'Uma transferência idêntica foi enviada há poucos segundos.',
  'BTF-2001': 'Saldo insuficiente para esta transferência.',
  'BTF-0500': 'Não encontramos a conta de destino. Verifique os dados.',
  'BTF-1000': 'Serviço temporariamente indisponível. Tente novamente em alguns minutos.',
};

function getUserMessage(error) {
  return errorMessages[error.code] || 'Ocorreu um erro. Tente novamente.';
}

Diferencie erros recuperáveis

CódigoRecuperávelAção
BTF-0001NãoCorrija os dados e reenvie
BTF-0010SimAguarde o horário de funcionamento
BTF-0011SimAguarde reset do limite (meia-noite)
BTF-0012NãoAguarde ou use outra combinação
BTF-2001SimDeposite fundos e tente novamente
BTF-1000SimRetry com backoff exponencial

Implemente retry inteligente

async function withRetry(fn, maxAttempts = 3) {
  const retryableCodes = ['BTF-1000', 'BTF-0501', 'BTF-2000', 'BTF-3000'];

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await fn();
    } catch (error) {
      const isRetryable = retryableCodes.includes(error.code);
      const isLastAttempt = attempt === maxAttempts;

      if (!isRetryable || isLastAttempt) {
        throw error;
      }

      const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
      await sleep(delay);
    }
  }
}

Segurança


Criptografia em trânsito

Todas as comunicações utilizam TLS 1.3 ou superior:
  • JD Consultores SPB (SOAP sobre HTTPS)
  • Midaz Ledger (gRPC com TLS)
  • CRM e Plugin Fees (HTTPS)
  • Webhooks (HTTPS obrigatório)

Criptografia em repouso

Dados sensíveis são criptografados com AES-256-GCM:
  • Credenciais da JD
  • PII (nome, CPF/CNPJ, dados bancários)
  • Segredos de webhook

Valide dados no servidor

Nunca confie apenas na validação do cliente:
func validateRecipient(r RecipientDetails) error {
    // ISPB: exatamente 8 dígitos
    if !regexp.MustCompile(`^\d{8}$`).MatchString(r.ISPB) {
        return ErrInvalidISPB
    }

    // HolderDocument: CPF (11) ou CNPJ (14)
    if !isValidCPF(r.HolderDocument) && !isValidCNPJ(r.HolderDocument) {
        return ErrInvalidDocument
    }

    // Valor: positivo, máximo 2 casas decimais
    if r.Amount <= 0 || !hasMaxTwoDecimals(r.Amount) {
        return ErrInvalidAmount
    }

    return nil
}

Proteja dados sensíveis

  • Não logue CPF/CNPJ em texto claro
  • Use hash (SHA-256) para identificação em logs
  • Mascare dados na interface: ***.***.***-00

Valide webhooks

Sempre verifique a assinatura HMAC-SHA256 antes de processar:
app.post('/webhooks/ted', (req, res) => {
  if (!validateSignature(req)) {
    logger.warn('Invalid webhook signature', { ip: req.ip });
    return res.status(401).send('Unauthorized');
  }
  // ...
});

Prevenção de fraudes


Detecção de duplicados

O plugin implementa janela de deduplicação configurável (padrão: 60 segundos):
  • Chave: SHA256(organizationId + senderAccountId + recipient + amount)
  • Armazenamento: Redis com TTL configurável
  • Ação: Requisições duplicadas retornam 409 Conflict

Rate limiting

Proteção contra abuso por tenant:
ConfiguraçãoValor padrão
AlgoritmoToken Bucket
Limite100 requisições/minuto por organização
Resposta429 Too Many Requests com header Retry-After

Isolamento multi-tenant

Todas as consultas são filtradas por organization_id, garantindo que um tenant nunca acesse dados de outro. O cache em Redis também utiliza prefixos por tenant.

Experiência do usuário


Mostre o progresso

Para TED OUT, mantenha o usuário informado:
[✓] Transferência iniciada
[✓] Fundos reservados
[○] Aguardando confirmação do banco
[ ] Concluída

Use webhooks para atualização em tempo real

Configure webhooks e use WebSocket ou push notifications para atualizar a interface:
// Servidor recebe webhook
socket.to(transfer.userId).emit('transfer:updated', {
  transferId: transfer.id,
  status: 'COMPLETED',
});

// Cliente atualiza a UI
socket.on('transfer:updated', (data) => {
  updateTransferStatus(data.transferId, data.status);
});

Ofereça comprovante

Após a conclusão, permita download do comprovante com:
  • Data e hora da transferência
  • Dados do remetente e destinatário
  • Valor e tarifa
  • Número de confirmação
  • Código de controle (para TED)

Reconciliação


Use identificadores consistentes

CampoUso
transferIdIdentificador interno (Lerian)
confirmationNumberNúmero legível para o usuário
controlNumberRastreamento no SPB (JD)

Mantenha histórico local

Sincronize periodicamente com a API para garantir consistência:
async function syncTransfers() {
  const lastSync = await getLastSyncTimestamp();
  const transfers = await api.get('/v1/transfers', {
    params: { fromDate: lastSync },
  });

  for (const transfer of transfers.data) {
    await upsertLocalTransfer(transfer);
  }

  await setLastSyncTimestamp(new Date());
}

Limites e quotas


Monitore o uso de limites

Consulte o uso atual antes de iniciar transferências:
async function checkLimits(accountId, amount) {
  const limits = await api.get(`/accounts/${accountId}/limits`);

  if (limits.dailyUsed + amount > limits.dailyLimit) {
    return {
      allowed: false,
      reason: 'daily_limit',
      available: limits.dailyLimit - limits.dailyUsed,
    };
  }

  return { allowed: true };
}

Comunique limites proativamente

Limite diário: R$ 50.000,00
Utilizado hoje: R$ 45.000,00
Disponível: R$ 5.000,00

Conformidade com BACEN


Horário de funcionamento

O sistema valida automaticamente o horário do BACEN para TED:
  • Dias úteis: Segunda a sexta-feira
  • Horário: 06:30 às 17:00 (horário de Brasília, UTC-3)
  • Feriados: Sistema consulta calendário de feriados nacionais
Transferências P2P não estão sujeitas a essa restrição e funcionam 24/7.

Limites de transferência

O plugin respeita os limites configurados por conta e por organização:
  • Limite por transação: Configurável por tenant (padrão: R$ 50.000)
  • Limite diário: Soma de todas as transferências do dia
  • Limite mensal: Controle de volume agregado

Auditoria e rastreabilidade

Toda mudança de estado é registrada na tabela TransferStatusHistory:
  • Status anterior e novo: Transição completa
  • Timestamp: Data e hora exata da mudança
  • Motivo: Razão da transição (especialmente para erros)
  • Autor: Sistema, usuário ou administrador
Retenção obrigatória: Dados mantidos por 5 anos conforme Resolução 4.753/2019 do BACEN.

Implemente audit trail

Registre todas as ações relacionadas a transferências:
{
  "timestamp": "2026-01-21T14:30:00-03:00",
  "action": "TRANSFER_INITIATED",
  "userId": "user-123",
  "transferId": "transfer-456",
  "ip": "192.168.1.1",
  "userAgent": "Mozilla/5.0..."
}

Conformidade com LGPD


Minimização de dados

Colete apenas dados estritamente necessários. CPF/CNPJ são criptografados e utilizados apenas para validação e conformidade.

Direito ao esquecimento

Endpoint administrativo para anonimização de dados pessoais:
DELETE /admin/v1/transfers/{id}/anonymize
Este endpoint substitui dados pessoais por valores genéricos (ex: “ANONIMIZADO”), mantendo a integridade transacional para auditoria.

Retenção limitada

Tipo de dadoPeríodo de retenção
Dados transacionais5 anos (requisito BACEN)
Logs de aplicação90 dias
Dados de auditoria5 anos (anonimizados após 2 anos)

Certificações e padrões


O plugin foi desenvolvido seguindo as melhores práticas da indústria:
  • OWASP Top 10: Mitigação de vulnerabilidades comuns
  • CIS Benchmarks: Configuração segura de infraestrutura
  • ISO 27001: Práticas de gestão de segurança da informação
  • SOC 2 Type II: Controles de segurança, disponibilidade e confidencialidade
A Lerian mantém certificações SOC 2 Type II e ISO 27001, auditadas anualmente por terceiros independentes.