Saltar al contenido principal
Esta guía es para desarrolladores que implementan la integración del plugin TED. 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
La respuesta incluye un header X-Idempotency-Replayed: true cuando la respuesta se sirve desde la caché.
POST /v1/transfers/initiate
X-Tenant-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-Tenant-Id), senderAccountId, datos del destinatario y monto, y lo almacena en Redis durante 60 segundos (configurable). 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; verifique /health/ready si persiste
Programación de backoff recomendada para 5xx/503: 0s, 5s, 25s, 60s, 120s (5 intentos en total).
Para 503 BTF-1000 (JD SPB no disponible): 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 un header X-Signature con una firma HMAC-SHA256. Siempre valide antes de procesar.
X-Signature: sha256=a1b2c3d4e5f6...
Calcule el HMAC sobre los bytes del cuerpo de la solicitud sin procesar (no JSON.stringify — la representación en bytes debe coincidir exactamente) usando su webhookSecret, luego compare usando una función de tiempo constante.
const crypto = require('crypto');
const express = require('express');

function validateWebhook(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const receivedSignature = signature.replace('sha256=', '');

  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature),
    Buffer.from(receivedSignature)
  );
}

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

    if (!validateWebhook(req.body, signature, process.env.WEBHOOK_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

def validate_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()

    received = signature.replace('sha256=', '')

    return hmac.compare_digest(expected, received)
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strings"
)

func validateWebhook(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := hex.EncodeToString(mac.Sum(nil))
    received := strings.TrimPrefix(signature, "sha256=")
    return hmac.Equal([]byte(expected), []byte(received))
}

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.data.transferId,
  event: payload.event,
});

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 disponibleBTF-1000”Servicio de transferencias temporalmente no disponible. Intente nuevamente en unos minutos.”Reintentar con backoff
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
  • GET /health/ready 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