Saltar al contenido principal
En esta página, encontrará prácticas recomendadas para implementar transferencias bancarias en su aplicación, cubriendo aspectos de seguridad, experiencia del usuario, tratamiento de errores y cumplimiento regulatorio.

Flujo de dos etapas


El flujo de dos etapas (initiateprocess) ofrece ventajas importantes:

Muestre la tarifa antes de la confirmación

Permitir que el usuario revise la tarifa antes de confirmar evita sorpresas y reduce reclamaciones.

Ejemplo de confirmación

┌─────────────────────────────────────────────────┐
│  Confirmar transferencia                        │
│                                                 │
│  Destinatario: Maria Silva                      │
│  Banco: Bradesco (237)                          │
│                                                 │
│  Valor: R$ 1.500,00                             │
│  Tarifa: R$ 8,50                                │
│  ─────────────────────────                      │
│  Total: R$ 1.508,50                             │
│                                                 │
│  [ Cancelar ]              [ Confirmar ]        │
└─────────────────────────────────────────────────┘

Use la expiración a su favor

La iniciación expira en 24 horas. Esto permite:
  • Guardar la intención del usuario sin comprometer fondos
  • Retomar el flujo si el usuario abandona la pantalla
  • Validar datos sin impacto en el saldo

Tratamiento de horario


Valide el horario en el cliente

Antes de llamar a la API, verifique si está dentro del horario de funcionamiento:
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;

  // Lun-Vie (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 al usuario

Si está fuera del horario, informe cuándo podrá realizarse la transferencia:
TED disponible de lunes a viernes, de 06:30 a 17:00.
Próximo horario disponible: lunes, 06:30.

Considere los feriados

El plugin no valida feriados bancarios, por eso mantenga un calendario actualizado:
const feriadosBancarios2026 = [
  '2026-01-01', // Año Nuevo
  '2026-02-16', // Carnaval
  '2026-02-17', // Carnaval
  '2026-04-03', // Viernes Santo
  '2026-04-21', // Tiradentes
  '2026-05-01', // Día del Trabajo
  '2026-06-04', // Corpus Christi
  '2026-09-07', // Independencia
  '2026-10-12', // Nuestra Señora Aparecida
  '2026-11-02', // Día de los Muertos
  '2026-11-15', // Proclamación de la República
  '2026-12-25', // Navidad
];

Idempotencia


Use claves de idempotencia

Para operaciones de escritura, siempre envíe el header X-Idempotency-Key:
POST /v1/transfers/initiate
X-Idempotency-Key: 7c9e6679-7425-40de-944b-e07fc1f90ae7
Esto protege contra:
  • Doble clic del usuario
  • Retry automático en caso de timeout
  • Fallas de red que ocultan respuestas de éxito

Genere claves en el cliente

Genere la clave de idempotencia antes de hacer la solicitud y guárdela hasta confirmar el resultado:
async function initiateTransfer(data) {
  const idempotencyKey = crypto.randomUUID();

  // Guarda la clave 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') {
      // Puede reenviar con la misma clave
      return retryWithSameKey(data, idempotencyKey);
    }
    throw error;
  }
}

Tratamiento de errores


Mapee errores a mensajes amigables

const errorMessages = {
  'BTF-0010': 'Transferencias disponibles de lunes a viernes, de 06:30 a 17:00.',
  'BTF-0011': 'Ha alcanzado el límite diario de transferencias.',
  'BTF-0012': 'Una transferencia idéntica fue enviada hace pocos segundos.',
  'BTF-2001': 'Saldo insuficiente para esta transferencia.',
  'BTF-0500': 'No encontramos la cuenta de destino. Verifique los datos.',
  'BTF-1000': 'Servicio temporalmente no disponible. Intente nuevamente en algunos minutos.',
};

function getUserMessage(error) {
  return errorMessages[error.code] || 'Ocurrió un error. Intente nuevamente.';
}

Diferencie errores recuperables

CódigoRecuperableAcción
BTF-0001NoCorrija los datos y reenvíe
BTF-0010Espere el horario de funcionamiento
BTF-0011Espere reset del límite (medianoche)
BTF-0012NoEspere o use otra combinación
BTF-2001Deposite fondos e intente nuevamente
BTF-1000Retry con 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);
    }
  }
}

Seguridad


Encriptación en tránsito

Todas las comunicaciones utilizan TLS 1.3 o superior:
  • JD Consultores SPB (SOAP sobre HTTPS)
  • Midaz Ledger (gRPC con TLS)
  • CRM y Plugin Fees (HTTPS)
  • Webhooks (HTTPS obligatorio)

Encriptación en reposo

Los datos sensibles son encriptados con AES-256-GCM:
  • Credenciales de JD
  • PII (nombre, CPF/CNPJ, datos bancarios)
  • Secretos de webhook

Valide datos en el servidor

Nunca confíe solo en la validación del cliente:
func validateRecipient(r RecipientDetails) error {
    // ISPB: exactamente 8 dígitos
    if !regexp.MustCompile(`^\d{8}$`).MatchString(r.ISPB) {
        return ErrInvalidISPB
    }

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

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

    return nil
}

Proteja datos sensibles

  • No registre CPF/CNPJ en texto claro
  • Use hash (SHA-256) para identificación en logs
  • Enmascare datos en la interfaz: ***.***.***-00

Valide webhooks

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

Prevención de fraudes


Detección de duplicados

El plugin implementa ventana de deduplicación configurable (por defecto: 60 segundos):
  • Clave: SHA256(organizationId + senderAccountId + recipient + amount)
  • Almacenamiento: Redis con TTL configurable
  • Acción: Solicitudes duplicadas retornan 409 Conflict

Rate limiting

Protección contra abuso por tenant:
ConfiguraciónValor por defecto
AlgoritmoToken Bucket
Límite100 solicitudes/minuto por organización
Respuesta429 Too Many Requests con header Retry-After

Aislamiento multi-tenant

Todas las consultas son filtradas por organization_id, garantizando que un tenant nunca acceda a datos de otro. El cache en Redis también utiliza prefijos por tenant.

Experiencia del usuario


Muestre el progreso

Para TED OUT, mantenga al usuario informado:
[✓] Transferencia iniciada
[✓] Fondos reservados
[○] Esperando confirmación del banco
[ ] Completada

Use webhooks para actualización en tiempo real

Configure webhooks y use WebSocket o push notifications para actualizar la interfaz:
// Servidor recibe webhook
socket.to(transfer.userId).emit('transfer:updated', {
  transferId: transfer.id,
  status: 'COMPLETED',
});

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

Ofrezca comprobante

Después de la conclusión, permita la descarga del comprobante con:
  • Fecha y hora de la transferencia
  • Datos del remitente y destinatario
  • Valor y tarifa
  • Número de confirmación
  • Código de control (para TED)

Reconciliación


Use identificadores consistentes

CampoUso
transferIdIdentificador interno (Lerian)
confirmationNumberNúmero legible para el usuario
controlNumberRastreo en SPB (JD)

Mantenga historial local

Sincronice periódicamente con la API para garantizar consistencia:
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());
}

Límites y cuotas


Monitoree el uso de límites

Consulte el uso actual antes de iniciar transferencias:
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 límites proactivamente

Límite diario: R$ 50.000,00
Utilizado hoy: R$ 45.000,00
Disponible: R$ 5.000,00

Cumplimiento con BACEN


Horario de funcionamiento

El sistema valida automáticamente el horario del BACEN para TED:
  • Días hábiles: Lunes a viernes
  • Horario: 06:30 a 17:00 (horario de Brasilia, UTC-3)
  • Feriados: Sistema consulta calendario de feriados nacionales
Las transferencias P2P no están sujetas a esta restricción y funcionan 24/7.

Límites de transferencia

El plugin respeta los límites configurados por cuenta y por organización:
  • Límite por transacción: Configurable por tenant (por defecto: R$ 50.000)
  • Límite diario: Suma de todas las transferencias del día
  • Límite mensual: Control de volumen agregado

Auditoría y rastreabilidad

Todo cambio de estado es registrado en la tabla TransferStatusHistory:
  • Estado anterior y nuevo: Transición completa
  • Timestamp: Fecha y hora exacta del cambio
  • Motivo: Razón de la transición (especialmente para errores)
  • Autor: Sistema, usuario o administrador
Retención obligatoria: Datos mantenidos por 5 años conforme Resolución 4.753/2019 del BACEN.

Implemente audit trail

Registre todas las acciones relacionadas a transferencias:
{
  "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..."
}

Cumplimiento con LGPD


Minimización de datos

Recolecte solo datos estrictamente necesarios. CPF/CNPJ son encriptados y utilizados solo para validación y cumplimiento.

Derecho al olvido

Endpoint administrativo para anonimización de datos personales:
DELETE /admin/v1/transfers/{id}/anonymize
Este endpoint sustituye datos personales por valores genéricos (ej: “ANONIMIZADO”), manteniendo la integridad transaccional para auditoría.

Retención limitada

Tipo de datoPeríodo de retención
Datos transaccionales5 años (requisito BACEN)
Logs de aplicación90 días
Datos de auditoría5 años (anonimizados después de 2 años)

Certificaciones y estándares


El plugin fue desarrollado siguiendo las mejores prácticas de la industria:
  • OWASP Top 10: Mitigación de vulnerabilidades comunes
  • CIS Benchmarks: Configuración segura de infraestructura
  • ISO 27001: Prácticas de gestión de seguridad de la información
  • SOC 2 Type II: Controles de seguridad, disponibilidad y confidencialidad
Lerian mantiene certificaciones SOC 2 Type II e ISO 27001, auditadas anualmente por terceros independientes.