Saltar al contenido principal

Documentation Index

Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt

Use this file to discover all available pages before exploring further.

Esta guía es para desarrolladores que implementan la integración del plugin Bank Transfer. Cubre los patrones y decisiones que van más allá de las llamadas individuales a endpoints: idempotencia, estrategia de reintentos, manejo de estados y validación de webhooks. Para los parámetros de los endpoints y los esquemas de respuesta, consulte la Referencia de API.

Idempotencia


Cada solicitud mutante (initiate, process, cancel) requiere un header X-Idempotency. Si envía la misma clave dos veces, el plugin devuelve la respuesta original sin crear una operación duplicada. Reglas:
  • Use un UUID v4 o un identificador de negocio único (por ejemplo, su ID de orden interno)
  • Longitud máxima: 255 caracteres
  • Las claves tienen alcance por organización — la misma clave de dos organizaciones diferentes se trata como dos solicitudes distintas
  • Las respuestas en caché se devuelven durante 24 horas
  • Las respuestas reemitidas son byte-idénticas a la original (mismo código de estado y mismo body); la respuesta no expone actualmente un header para distinguir replays de ejecuciones nuevas, así que diseñe su cliente para ser seguro en cualquier caso
POST /v1/transfers/initiate
X-Organization-Id: 019c9ac2-3f5d-7df9-9215-bdccc1451def
X-Idempotency: 7f3d9a1b-4e2c-4f8a-b3d1-9e6f2a4c8b7e
No reutilice claves de idempotencia en diferentes operaciones. Una clave usada para iniciar una transferencia no debe reutilizarse para procesarla o cancelarla.

Detección de duplicados

Más allá de las claves de idempotencia, el plugin detecta duplicados basados en el contenido. Genera un hash a partir de la organización (del header X-Organization-Id), senderAccountId, datos del destinatario y monto, y lo almacena en Redis durante 5 minutos (por defecto 300 segundos, configurable vía DUPLICATE_GUARD_TTL_SEC). Si ya se envió una transferencia coincidente dentro de la ventana, la solicitud es rechazada con 409 BTF-0012. Esto captura los casos en que el cliente envía la misma transferencia con una clave de idempotencia diferente — por ejemplo, después de un timeout donde no se recibió la respuesta original.

Estrategia de reintentos


Use backoff exponencial para errores transitorios. No todos los errores deben reintentarse.
Estado HTTPTipo de error¿Reintentar?Notas
400Error de validaciónNoCorrija la solicitud antes de reintentar
404No encontradoNoEl recurso no existe
409DuplicadoNoIdempotente — use la respuesta original
410ExpiradoNoCree una nueva iniciación
422Regla de negocioNoHorario, límites — la condición debe cambiar primero
429Límite de tasaEspere el valor del header Retry-After (segundos)
500Error internoReintentar con backoff
503No disponibleReintentar con backoff
Programación de backoff recomendada para 5xx/503: 0s, 5s, 25s, 60s, 120s (5 intentos en total).
Cuando JD SPB no está disponible, la respuesta es HTTP 503 y el campo error.code lleva el código del proveedor JD sin transformar (por ejemplo, TRANSPORT para fallas de transporte o ACE95 para timeouts) — las fallas de la cadena JD no se envuelven en un código BTF-. Después de agotar los reintentos, la transferencia debe marcarse para reconciliación manual. No siga reintentando indefinidamente — la red JD SPB tiene horarios operativos definidos.

Manejo de estados


Máquina de estados de TED OUT

Las transferencias siguen una progresión estricta. Una vez que una transferencia sale de CREATED o PENDING, no puede cancelarse.
Máquina de estados TED OUT
Qué hacer en cada estado:
EstadoSignificadoAcción recomendada
CREATEDConfirmado por el usuario, en cola para envíoMostrar “Procesando” en la interfaz; hacer polling o esperar webhook
PENDINGEnviado a JD, esperando reconocimientoMostrar “Procesando”; no permitir cancelación
PROCESSINGJD aceptó y está enrutando la transferenciaMostrar “Procesando”; SLA típico menor a 10 minutos
COMPLETEDLiquidadoMostrar confirmación con confirmationNumber
REJECTEDJD rechazó (datos inválidos, violación de regla)Mostrar error al usuario; fondos ya liberados
FAILEDJD inaccesible o timeoutMostrar error; fondos ya liberados; permitir reintento si se desea
CANCELLEDCancelado antes del envíoMostrar confirmación de cancelación

Máquina de estados de iniciación

La entidad PaymentInitiation (creada por el endpoint initiate) tiene su propio ciclo de vida antes de que se cree un Transfer.
Máquina de estados de iniciación

Máquina de estados de TED IN

Máquina de estados TED IN

Máquina de estados de P2P

P2P no tiene estado PENDING. La liquidación es atómica e instantánea.
Máquina de estados P2P

Polling vs. webhooks

Prefiera los webhooks para el estado en tiempo real. Si los webhooks aún no están configurados, haga polling en GET /v1/transfers/{transferId} con un máximo de 10 intentos usando el mismo programación de backoff que los reintentos. Después de 10 minutos sin un estado terminal (COMPLETED, REJECTED, FAILED, CANCELLED), marque la transferencia para revisión manual. Consulte Obtener Transferencia y Webhooks.

Integración de webhooks


Para los esquemas de payload de eventos y la lista completa de eventos, consulte Webhooks.

Validación de firma

Cada solicitud de webhook incluye dos headers que su endpoint debe usar para verificar la autenticidad:
  • X-Webhook-Signature — firma HMAC-SHA256 en formato sha256=<hex>
  • X-Webhook-Timestamp — timestamp Unix en segundos (UTC) de cuando el plugin construyó la solicitud
El plugin calcula la firma así:
X-Webhook-Signature: sha256=hex(HMAC_SHA256(WEBHOOK_SIGNING_SECRET, <timestamp> + "." + <raw_body>))
El payload firmado es el valor del timestamp (la cadena literal enviada en X-Webhook-Timestamp), seguido de un único punto ASCII (.), seguido de los bytes crudos del cuerpo de la solicitud — exactamente como se recibieron, antes de cualquier parseo o re-codificación JSON. Use los bytes que llegaron por la red, no una versión re-serializada del objeto parseado. Para validar:
  1. Lea X-Webhook-Signature y X-Webhook-Timestamp de los headers de la solicitud.
  2. Construya el payload firmado: timestamp + "." + rawBody.
  3. Calcule HMAC-SHA256 sobre el payload firmado usando su WEBHOOK_SIGNING_SECRET y codifique el resultado en hex.
  4. Anteponga sha256= y compare contra X-Webhook-Signature usando una función de igualdad de tiempo constante.
  5. Rechace la solicitud si el timestamp está fuera de una ventana de frescura aceptable (una tolerancia de 5 minutos es típica) para prevenir replay.
El plugin también establece X-Webhook-Event-Type, X-Webhook-Routing-Key y X-Webhook-Delivery-Attempt para observabilidad — estos no forman parte del payload firmado y no deben usarse para autenticación.
const crypto = require('crypto');
const express = require('express');

const TOLERANCE_SECONDS = 300; // 5 minutos

function validateWebhook(rawBody, timestamp, signature, secret) {
  if (!timestamp || !signature) return false;

  const ageSeconds = Math.abs(Math.floor(Date.now() / 1000) - parseInt(timestamp, 10));
  if (Number.isNaN(ageSeconds) || ageSeconds > TOLERANCE_SECONDS) return false;

  const signedPayload = Buffer.concat([
    Buffer.from(timestamp, 'utf8'),
    Buffer.from('.', 'utf8'),
    rawBody,
  ]);

  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');

  const expectedBuf = Buffer.from(expected);
  const receivedBuf = Buffer.from(signature);
  if (expectedBuf.length !== receivedBuf.length) return false;

  return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}

// Use raw body — not req.body (parsed JSON)
app.post('/webhooks/ted',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const timestamp = req.headers['x-webhook-timestamp'];
    const signature = req.headers['x-webhook-signature'];

    if (!validateWebhook(req.body, timestamp, signature, process.env.WEBHOOK_SIGNING_SECRET)) {
      return res.status(401).send('Invalid signature');
    }

    const payload = JSON.parse(req.body.toString());
    // process payload...
    res.status(200).send('OK');
  }
);
import hmac
import hashlib
import time

TOLERANCE_SECONDS = 300  # 5 minutos

def validate_webhook(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
    if not timestamp or not signature:
        return False

    try:
        age = abs(int(time.time()) - int(timestamp))
    except ValueError:
        return False
    if age > TOLERANCE_SECONDS:
        return False

    signed_payload = timestamp.encode() + b"." + raw_body
    expected = "sha256=" + hmac.new(
        secret.encode(),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(expected, signature)
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strconv"
    "time"
)

const toleranceSeconds = 300 // 5 minutos

func validateWebhook(rawBody []byte, timestamp, signature, secret string) bool {
    if timestamp == "" || signature == "" {
        return false
    }

    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil {
        return false
    }
    if diff := time.Now().Unix() - ts; diff < -toleranceSeconds || diff > toleranceSeconds {
        return false
    }

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(timestamp))
    mac.Write([]byte("."))
    mac.Write(rawBody)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(expected), []byte(signature))
}

Procesamiento idempotente de webhooks

Su endpoint puede recibir el mismo evento más de una vez (entrega al menos una vez). Use transferId + event como clave compuesta para deduplicar.
const alreadyProcessed = await db.webhookEvents.exists({
  transferId: payload.transferId,
  event: payload.type,
});

if (alreadyProcessed) {
  return res.status(200).send('OK'); // acknowledge without reprocessing
}

Patrones de manejo de errores


Mapee los códigos de error de la API a acciones orientadas al usuario. Consulte la lista completa de errores para todos los códigos.
EscenarioCódigoMensaje para el usuarioAcción
Fuera del horario operativoBTF-0010”Transferencias disponibles lun–vie, 06:30–17:00 (Brasilia). Próxima ventana: Mostrar próxima hora disponible
Límite diario excedidoBTF-0011”Límite diario de transferencias alcanzado. Intente mañana.”Mostrar límite restante
Transferencia duplicadaBTF-0012”Esta transferencia ya fue enviada.”Devolver transferId original
Datos de destinatario inválidosBTF-0001”Verifique los datos del destinatario e intente nuevamente.”Resaltar campos inválidos
Iniciación expiradaBTF-0202”Sesión expirada. Por favor, inicie una nueva transferencia.”Reiniciar el flujo de iniciación
JD SPB no disponibleTRANSPORT (HTTP 503)“Servicio de transferencias temporalmente no disponible. Intente nuevamente en unos minutos.”Reintentar con backoff; detecte vía 503 + código de proveedor JD sin transformar (TRANSPORT, ACE95, …), no por un prefijo BTF-
Midaz no disponibleBTF-2000”Servicio temporalmente no disponible. Intente nuevamente en unos minutos.”Reintentar con backoff
Las respuestas de error siguen esta estructura:
{
  "error": {
    "code": "BTF-0010",
    "message": "Transfers can only be initiated Monday-Friday between 06:30 and 17:00 Brasília time",
    "fields": {
      "currentTime": "2026-01-21T18:30:00-03:00",
      "nextAvailableTime": "2026-01-22T06:30:00-03:00"
    }
  }
}

Lista de verificación para salida en producción


Antes de habilitar la integración en producción:
  • X-Idempotency se envía en cada solicitud de initiate, process y cancel
  • Lógica de reintentos implementada con backoff exponencial para errores 5xx/503
  • Endpoint de webhook desplegado y devolviendo 200 en menos de 5 segundos
  • Validación de firma activa en el endpoint de webhook
  • Deduplicación de eventos de webhook implementada usando transferId + event
  • Horario de funcionamiento validado en el lado del cliente antes de llamar a initiate (reduce los 422 innecesarios)
  • Tanto transferId como confirmationNumber almacenados para reconciliación
  • Estados terminales (COMPLETED, REJECTED, FAILED, CANCELLED) gestionados en la interfaz
  • Expiración de iniciación (24h) manejada — se solicita al usuario que reinicie si la ventana expira
  • Readiness del servicio monitoreado en su sistema de alertas para despliegues BYOC
  • Redis está disponible y monitoreado — el servicio no aceptará solicitudes si Redis es inalcanzable
  • PLUGIN_AUTH_ENABLED=true configurado en producción, con PLUGIN_AUTH_ADDRESS, PLUGIN_AUTH_CLIENT_ID y PLUGIN_AUTH_CLIENT_SECRET válidos