> ## 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.

# Guía para desarrolladores

> Implementa correctamente las integraciones de Bank Transfer: idempotencia, estrategia de reintentos, manejo de estados y patrones de validación de webhooks para transferencias confiables.

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

Para los parámetros de los endpoints y los esquemas de respuesta, consulta la [Referencia de API](/es/reference/interfaces/ted-jd/initiate-transfer).

## Idempotencia

***

Cada solicitud que modifica datos (initiate, process, cancel) requiere un encabezado `X-Idempotency`. Si envías la misma clave dos veces, el plugin devuelve la respuesta original sin crear una operación duplicada.

**Reglas:**

* Usa un UUID v4 o un identificador de negocio único (p. ej. el ID de pedido de tu sistema)
* Longitud máxima: 255 caracteres
* El plugin limita cada clave a la organización efectiva. La misma clave desde dos organizaciones cuenta como dos solicitudes distintas.
* El plugin devuelve una respuesta en caché durante la ventana de idempotencia configurada (`IDEMPOTENCY_RETRY_WINDOW_SEC`, predeterminado 300 segundos)
* Una respuesta repetida es idéntica byte a byte a la original: mismo código de estado, mismo cuerpo. La respuesta no tiene ningún encabezado que marque una repetición, así que diseña tu cliente para que sea seguro en cualquiera de los dos casos.

```http theme={null}
POST /v1/transfers/initiate
X-Organization-Id: 019c9ac2-3f5d-7df9-9215-bdccc1451def
X-Idempotency: 7f3d9a1b-4e2c-4f8a-b3d1-9e6f2a4c8b7e
```

<Warning>
  No reutilices claves de idempotencia entre operaciones distintas. No reutilices una clave de initiate para hacer process o cancel de la misma transferencia.
</Warning>

### Detección de duplicados

Más allá de las claves de idempotencia, el plugin detecta duplicados basados en el contenido. Construye una huella a partir de:

* `senderAccountId`
* los datos del receptor (ISPB, sucursal, cuenta, documento del titular)
* el monto
* el propósito

El plugin guarda la huella en Redis durante 5 minutos. El valor predeterminado es 300 segundos. Los operadores lo ajustan por tenant a través del ajuste del systemplane `idempotency.duplicate_guard_ttl_seconds`. La organización no forma parte de la huella. El aislamiento entre tenants viene del prefijo de la clave de Redis. El plugin rechaza la solicitud con `409 BTF-0012` si el cliente ya envió una transferencia coincidente dentro de la ventana.

Esto detecta los casos en que el cliente envía la misma transferencia con una clave de idempotencia distinta. Un ejemplo es un reintento después de un timeout, cuando el cliente no recibió la respuesta original.

## Estrategia de reintentos

***

Usa backoff exponencial para los errores transitorios. No reintentes todos los errores.

| Estado HTTP | ¿Reintentar? | Notas                                                                                |
| ----------- | ------------ | ------------------------------------------------------------------------------------ |
| `400`       | No           | Error de validación — corrige la solicitud antes de reintentar                       |
| `404`       | No           | No encontrado — el recurso no existe                                                 |
| `409`       | No           | Duplicado — idempotente; usa la respuesta original                                   |
| `410`       | No           | Expirado — crea un nuevo inicio                                                      |
| `422`       | No           | Regla de negocio (horario de operación, límites) — la condición debe cambiar primero |
| `429`       | Sí           | Rate limit — espera el valor del encabezado `Retry-After` (segundos)                 |
| `500`       | Sí           | Error interno — reintenta con backoff                                                |
| `503`       | Sí           | No disponible — reintenta con backoff                                                |

**Programa de backoff recomendado para 5xx/503:** 0s, 5s, 25s, 60s, 120s (5 intentos en total).

<Note>
  Cuando JD SPB no está disponible, la respuesta es `HTTP 503`. El campo `error.code` lleva entonces el código propio del proveedor JD, por ejemplo `TRANSPORT` para fallas de transporte o `ACE95` para timeouts. El plugin no envuelve las fallas de la cadena de JD en un código `BTF-`. Marca la transferencia para conciliación manual cuando se agoten los reintentos. No reintentes sin límite. La red de JD SPB tiene un horario de operación definido.
</Note>

## Manejo de estados

***

### Máquina de estados de TED OUT

Las transferencias siguen una progresión estricta. No puedes cancelar una transferencia después de que sale de `CREATED` o `PENDING`.

<Frame>
  <img src="https://mintcdn.com/lerian-49cb71fc/vdBt8wfgjsNRO1rf/images/es/d2/ted-state-machine-ted-out.svg?fit=max&auto=format&n=vdBt8wfgjsNRO1rf&q=85&s=31c1ad27f5ef8b57785647c305ad048c" alt="Máquina de estados de TED OUT" width="1116" height="552" data-path="images/es/d2/ted-state-machine-ted-out.svg" />
</Frame>

**Qué hacer en cada estado:**

| Estado       | Significado                                          | Acción recomendada                                                          |
| ------------ | ---------------------------------------------------- | --------------------------------------------------------------------------- |
| `CREATED`    | Confirmada por el usuario, en cola para el envío     | Muestra "Procesando" en la interfaz; consulta o espera el webhook           |
| `PENDING`    | Enviada a JD, a la espera del acuse de recibo        | Muestra "Procesando"; no permitas la cancelación                            |
| `PROCESSING` | JD aceptó y está enrutando la transferencia          | Muestra "Procesando"; SLA típico por debajo de 10 minutos                   |
| `COMPLETED`  | Liquidada                                            | Muestra la confirmación con `confirmationNumber`                            |
| `REJECTED`   | JD rechazó (datos inválidos, violación de una regla) | Muestra el error al usuario; los fondos ya se liberaron                     |
| `FAILED`     | JD inalcanzable o con timeout                        | Muestra el error; los fondos ya se liberaron; permite reintentar si quieres |
| `CANCELLED`  | Cancelada antes del envío                            | Muestra la confirmación de la cancelación                                   |

### Máquina de estados del inicio

El endpoint initiate crea una entidad `PaymentInitiation`. Esta entidad tiene su propio ciclo de vida antes de que el plugin cree una `Transfer`.

<Frame>
  <img src="https://mintcdn.com/lerian-49cb71fc/vdBt8wfgjsNRO1rf/images/es/d2/ted-state-machine-initiation.svg?fit=max&auto=format&n=vdBt8wfgjsNRO1rf&q=85&s=c140e7174ecccd297d3e2aa2a3fb1f5d" alt="Máquina de estados del inicio" width="1028" height="410" data-path="images/es/d2/ted-state-machine-initiation.svg" />
</Frame>

### Máquina de estados de TED IN

<Frame>
  <img src="https://mintcdn.com/lerian-49cb71fc/vdBt8wfgjsNRO1rf/images/es/d2/ted-state-machine-ted-in.svg?fit=max&auto=format&n=vdBt8wfgjsNRO1rf&q=85&s=3eb68733abe7f46206608acc031ea24b" alt="Máquina de estados de TED IN" width="870" height="410" data-path="images/es/d2/ted-state-machine-ted-in.svg" />
</Frame>

### Máquina de estados de P2P

P2P no tiene un estado `PENDING`. La liquidación es atómica e instantánea.

<Frame>
  <img src="https://mintcdn.com/lerian-49cb71fc/vdBt8wfgjsNRO1rf/images/es/d2/ted-state-machine-ted-p2p.svg?fit=max&auto=format&n=vdBt8wfgjsNRO1rf&q=85&s=41c7e6800e69b98bd25a0e705fe6e224" alt="Máquina de estados de P2P" width="805" height="461" data-path="images/es/d2/ted-state-machine-ted-p2p.svg" />
</Frame>

### Polling frente a webhooks

Prefiere los webhooks para el estado en tiempo real. Si todavía no configuraste webhooks, consulta `GET /v1/transfers/{transferId}`. Usa como máximo 10 intentos con el mismo programa de backoff que los reintentos. Marca la transferencia para revisión manual después de 10 minutos sin un estado terminal (`COMPLETED`, `REJECTED`, `FAILED`, `CANCELLED`).

Consulta [Get Transfer](/es/reference/interfaces/ted-jd/retrieve-transfer) y [Webhooks](/es/interfaces/ted-jd/ted-webhooks).

## Integración de webhooks

***

Para los esquemas de payload de los eventos y la lista completa de eventos, consulta [Webhooks](/es/interfaces/ted-jd/ted-webhooks).

### Validación de la firma

Cada solicitud de webhook incluye encabezados que tu endpoint usa para verificar la autenticidad:

* `X-Webhook-Signature`: firma HMAC-SHA256 versionada con la forma `v1,sha256=<hex>`
* `X-Webhook-Timestamp`: marca de tiempo Unix en segundos (UTC) del momento en que el plugin construyó la solicitud
* `X-Webhook-Event`: el tipo de evento (por ejemplo, `transfer.completed`). Este encabezado no forma parte de la firma.

El plugin calcula la firma así:

```
X-Webhook-Signature: v1,sha256=hex(HMAC_SHA256(WEBHOOK_SIGNING_SECRET, "v1:" + <timestamp> + "." + <raw_body>))
```

La cadena firmada tiene cuatro partes en orden: el prefijo `v1:`, el valor de la marca de tiempo de `X-Webhook-Timestamp`, un punto ASCII (`.`), y luego los **bytes del cuerpo de la solicitud sin procesar**. Usa los bytes del cuerpo exactamente como llegan por la red. No los analices ni los vuelvas a codificar antes.

Para validar:

1. Lee `X-Webhook-Signature` y `X-Webhook-Timestamp` de los encabezados de la solicitud.
2. Construye la cadena firmada: `"v1:" + timestamp + "." + rawBody`.
3. Calcula `HMAC-SHA256` sobre la cadena firmada con tu `WEBHOOK_SIGNING_SECRET`, y luego codifica el resultado en hexadecimal.
4. Antepón `v1,sha256=`, y luego compáralo con `X-Webhook-Signature` con una función de igualdad de tiempo constante.
5. Rechaza la solicitud si la marca de tiempo queda fuera de una ventana de frescura aceptable (una tolerancia de 5 minutos es lo típico) para evitar repeticiones.

Aparte de `X-Webhook-Signature` y `X-Webhook-Timestamp`, el plugin define solo `X-Webhook-Event` (el tipo de evento). No envía `X-Webhook-Event-Type`, `X-Webhook-Routing-Key` ni `X-Webhook-Delivery-Attempt`.

<AccordionGroup>
  <Accordion title="JavaScript">
    ```javascript theme={null}
    const crypto = require('crypto');
    const express = require('express');

    const TOLERANCE_SECONDS = 300; // 5 minutes

    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('v1:', 'utf8'),
        Buffer.from(timestamp, 'utf8'),
        Buffer.from('.', 'utf8'),
        rawBody,
      ]);

      const expected = 'v1,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');
      }
    );
    ```
  </Accordion>

  <Accordion title="Python">
    ```python theme={null}
    import hmac
    import hashlib
    import time

    TOLERANCE_SECONDS = 300  # 5 minutes

    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 = b"v1:" + timestamp.encode() + b"." + raw_body
        expected = "v1,sha256=" + hmac.new(
            secret.encode(),
            signed_payload,
            hashlib.sha256,
        ).hexdigest()

        return hmac.compare_digest(expected, signature)
    ```
  </Accordion>

  <Accordion title="Go">
    ```go theme={null}
    import (
        "crypto/hmac"
        "crypto/sha256"
        "encoding/hex"
        "strconv"
        "time"
    )

    const toleranceSeconds = 300 // 5 minutes

    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("v1:"))
        mac.Write([]byte(timestamp))
        mac.Write([]byte("."))
        mac.Write(rawBody)
        expected := "v1,sha256=" + hex.EncodeToString(mac.Sum(nil))

        return hmac.Equal([]byte(expected), []byte(signature))
    }
    ```
  </Accordion>
</AccordionGroup>

### Procesamiento idempotente de webhooks

Tu endpoint puede recibir el mismo evento más de una vez (entrega al menos una vez). Usa `transferId` + `event` como clave compuesta para deduplicar.

```javascript theme={null}
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

***

Asigna los códigos de error de la API a acciones de cara al usuario. Consulta la [lista completa de errores](/es/reference/interfaces/ted-jd/ted-error-list) para todos los códigos.

| Escenario                                          | Mensaje de cara al usuario                                                                       | Acción                                                                                                                             |
| -------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- |
| **Fuera del horario de operación** (`BTF-0010`)    | "Transferencias disponibles de lunes a viernes, 06:30–17:00 (Brasilia). Próxima ventana: {time}" | Muestra el próximo horario disponible                                                                                              |
| **Límite diario excedido** (`BTF-0011`)            | "Límite diario de transferencias alcanzado. Inténtalo de nuevo mañana."                          | Muestra el límite restante                                                                                                         |
| **Transferencia duplicada** (`BTF-0012`)           | "Esta transferencia ya se envió."                                                                | Devuelve el `transferId` original                                                                                                  |
| **Datos del receptor inválidos** (`BTF-0001`)      | "Revisa los datos del receptor e inténtalo de nuevo."                                            | Resalta los campos inválidos                                                                                                       |
| **Inicio expirado** (`BTF-0202`)                   | "La sesión expiró. Empieza una nueva transferencia."                                             | Reinicia el flujo de inicio                                                                                                        |
| **JD SPB no disponible** (`TRANSPORT`, HTTP `503`) | "Servicio de transferencias no disponible temporalmente. Inténtalo de nuevo en unos minutos."    | Reintenta con backoff; detéctalo por `503` + el código propio del proveedor JD (`TRANSPORT`, `ACE95`, …), no por un prefijo `BTF-` |
| **Midaz no disponible** (`BTF-2000`)               | "Servicio no disponible temporalmente. Inténtalo de nuevo en unos minutos."                      | Reintenta con backoff                                                                                                              |

Las respuestas de error siguen esta estructura:

```json theme={null}
{
  "error": {
    "code": "BTF-0010",
    "service": "plugin",
    "category": "deterministic",
    "message": "Transfers can only be initiated Monday-Friday between 06:30 and 17:00 Brasília time",
    "requestId": "6d3e2a68-1f2b-4c3d-9e4f-5a6b7c8d9e0f",
    "fields": {
      "currentTime": "2026-01-21T18:30:00-03:00",
      "nextAvailableTime": "2026-01-22T06:30:00-03:00"
    }
  }
}
```

## Lista de verificación para salir a producción

***

Antes de habilitar la integración en producción:

* [ ] Envía `X-Idempotency` en cada solicitud de initiate, process y cancel
* [ ] Lógica de reintentos implementada con backoff exponencial para los errores 5xx/503
* [ ] Endpoint de webhook desplegado y que devuelve `200` en menos de 5 segundos
* [ ] Validación de la firma activa en el endpoint de webhook
* [ ] Deduplicación de eventos de webhook implementada con `transferId + event`
* [ ] Horario de operación validado del lado del cliente antes de llamar a initiate (reduce los 422 innecesarios)
* [ ] `transferId` y `confirmationNumber` almacenados para la conciliación
* [ ] Estados terminales (`COMPLETED`, `REJECTED`, `FAILED`, `CANCELLED`) manejados en la interfaz
* [ ] Expiración del inicio (24h) manejada: pide al usuario que reinicie cuando pase la ventana
* [ ] Preparación del servicio monitoreada en tu sistema de alertas para los despliegues BYOC
* [ ] Redis alcanzable y monitoreado: el servicio rechaza solicitudes cuando Redis está caído
* [ ] `PLUGIN_AUTH_ENABLED=true` configurado en producción, con un `PLUGIN_AUTH_ADDRESS` válido (HTTPS)
