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

# Guia do desenvolvedor

> Implemente integrações Bank Transfer corretamente — idempotência, estratégia de retry, gerenciamento de estado e validação de webhooks confiáveis.

Este guia é para desenvolvedores que implementam a integração com o plugin Bank Transfer. Ele cobre os padrões e decisões que vão além de chamadas individuais de endpoint: idempotência, estratégia de retry, gerenciamento de estado e validação de webhooks.

Para parâmetros de endpoint e esquemas de resposta, consulte a [Referência da API](/pt/reference/midaz/plugins/ted/initiate-transfer).

## Idempotência

***

Toda requisição de mutação (initiate, process, cancel) requer um cabeçalho `X-Idempotency`. Se você enviar a mesma chave duas vezes, o plugin retorna a resposta original sem criar uma operação duplicada.

**Regras:**

* Use um UUID v4 ou um identificador de negócio único (ex: o ID interno do seu pedido)
* Comprimento máximo: 255 caracteres
* As chaves têm escopo conforme a organização efetiva — a mesma chave de duas organizações diferentes é tratada como duas requisições distintas
* As respostas em cache são retornadas durante a janela de idempotência configurada (`IDEMPOTENCY_RETRY_WINDOW_SEC`, padrão 300 segundos)
* Respostas reenviadas são byte-idênticas à original (mesmo status code e mesmo body); a resposta atualmente não expõe um cabeçalho que distinga replays de execuções novas, então projete seu cliente para ser seguro em qualquer caso

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

<Warning>
  Não reutilize chaves de idempotência em operações diferentes. Uma chave usada para iniciar uma transferência não deve ser reutilizada para processá-la ou cancelá-la.
</Warning>

### Detecção de duplicatas

Além das chaves de idempotência, o plugin detecta duplicatas baseadas em conteúdo. Ele gera um fingerprint a partir de:

* `senderAccountId`
* dados do destinatário (ISPB, agência, conta, documento do titular)
* valor
* finalidade

O fingerprint é armazenado no Redis por 5 minutos (padrão 300 segundos, configurável via `DUPLICATE_GUARD_TTL_SEC`). A organização não faz parte do fingerprint — o isolamento de tenant vem do prefixo da chave Redis. Se uma transferência correspondente já foi enviada dentro da janela, a requisição é rejeitada com `409 BTF-0012`.

Isso captura casos em que o cliente envia a mesma transferência com uma chave de idempotência diferente — por exemplo, após um timeout em que a resposta original não foi recebida.

## Estratégia de retry

***

Use backoff exponencial para erros transientes. Nem todos os erros devem ser retentados.

| Status HTTP | Retry? | Observações                                                                           |
| ----------- | ------ | ------------------------------------------------------------------------------------- |
| `400`       | Não    | Erro de validação — corrija a requisição antes de tentar novamente                    |
| `404`       | Não    | Não encontrado — o recurso não existe                                                 |
| `409`       | Não    | Duplicata — idempotente; use a resposta original                                      |
| `410`       | Não    | Expirado — crie uma nova iniciação                                                    |
| `422`       | Não    | Regra de negócio (horário de funcionamento, limites) — a condição deve mudar primeiro |
| `429`       | Sim    | Limite de taxa — aguarde o valor do cabeçalho `Retry-After` (em segundos)             |
| `500`       | Sim    | Erro interno — retry com backoff                                                      |
| `503`       | Sim    | Indisponível — retry com backoff                                                      |

**Cronograma de backoff recomendado para 5xx/503:** 0s, 5s, 25s, 60s, 120s (5 tentativas no total).

<Note>
  Quando o JD SPB está indisponível, a resposta é `HTTP 503` e o campo `error.code` carrega o código do fornecedor JD sem transformação (por exemplo, `TRANSPORT` para falhas de transporte ou `ACE95` para timeouts) — falhas da cadeia JD não são envelopadas em um código `BTF-`. Após esgotar as tentativas, a transferência deve ser sinalizada para reconciliação manual. Não continue tentando indefinidamente — a rede JD SPB tem horários de funcionamento definidos.
</Note>

## Gerenciamento de estado

***

### Máquina de estados do TED OUT

As transferências seguem uma progressão estrita. Uma vez que uma transferência sai de `CREATED` ou `PENDING`, ela não pode ser cancelada.

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

**O que fazer em cada estado:**

| Estado       | Significado                                      | Ação recomendada                                             |
| ------------ | ------------------------------------------------ | ------------------------------------------------------------ |
| `CREATED`    | Confirmado pelo usuário, aguardando envio        | Exibir "Processando" na UI; aguardar polling ou webhook      |
| `PENDING`    | Enviado para a JD, aguardando reconhecimento     | Exibir "Processando"; não permitir cancelamento              |
| `PROCESSING` | JD aceitou e está roteando a transferência       | Exibir "Processando"; SLA típico inferior a 10 minutos       |
| `COMPLETED`  | Liquidada                                        | Exibir confirmação com `confirmationNumber`                  |
| `REJECTED`   | JD rejeitou (dados inválidos, violação de regra) | Exibir erro ao usuário; fundos já liberados                  |
| `FAILED`     | JD inacessível ou timeout                        | Exibir erro; fundos já liberados; permitir retry se desejado |
| `CANCELLED`  | Cancelada antes do envio                         | Exibir confirmação de cancelamento                           |

### Máquina de estados da iniciação

A entidade `PaymentInitiation` (criada pelo endpoint de initiate) tem seu próprio ciclo de vida antes de um `Transfer` ser criado.

<Frame>
  <img src="https://mintcdn.com/lerian-49cb71fc/dIivJl2jpQJ0JZcX/images/pt/d2/ted-state-machine-initiation.svg?fit=max&auto=format&n=dIivJl2jpQJ0JZcX&q=85&s=20908a77c31f3abac85dcae620356e87" alt="Máquina de estados da iniciação" width="1106" height="410" data-path="images/pt/d2/ted-state-machine-initiation.svg" />
</Frame>

### Máquina de estados do TED IN

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

### Máquina de estados do P2P

O P2P não tem um estado `PENDING`. A liquidação é atômica e instantânea.

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

### Polling vs. webhooks

Prefira webhooks para status em tempo real. Se os webhooks ainda não estiverem configurados, faça polling em `GET /v1/transfers/{transferId}` com no máximo 10 tentativas usando o mesmo cronograma de backoff do retry. Após 10 minutos sem um estado terminal (`COMPLETED`, `REJECTED`, `FAILED`, `CANCELLED`), sinalize a transferência para revisão manual.

Consulte [Obter Transferência](/pt/reference/midaz/plugins/ted/retrieve-transfer) e [Webhooks](/pt/rails/ted/jd/ted-webhooks).

## Integração com webhooks

***

Para esquemas de payload de eventos e a lista completa de eventos, consulte [Webhooks](/pt/rails/ted/jd/ted-webhooks).

### Validação de assinatura

Toda requisição de webhook inclui cabeçalhos que seu endpoint usa para verificar a autenticidade:

* `X-Webhook-Signature` — assinatura HMAC-SHA256 versionada no formato `v1,sha256=<hex>`
* `X-Webhook-Timestamp` — timestamp Unix em segundos (UTC) de quando o plugin construiu a requisição
* `X-Webhook-Event` — o tipo de evento (por exemplo, `transfer.completed`); não faz parte da assinatura

O plugin calcula a assinatura assim:

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

A string assinada é o prefixo literal `v1:`, seguido do valor do timestamp (a string enviada em `X-Webhook-Timestamp`), um único ponto ASCII (`.`), e os **bytes brutos do corpo da requisição** — exatamente como recebidos, antes de qualquer análise ou recodificação JSON. Use os bytes que vieram pelo fio, não uma versão re-serializada do objeto parseado.

Para validar:

1. Leia `X-Webhook-Signature` e `X-Webhook-Timestamp` dos cabeçalhos da requisição.
2. Construa a string assinada: `"v1:" + timestamp + "." + rawBody`.
3. Calcule `HMAC-SHA256` sobre a string assinada usando seu `WEBHOOK_SIGNING_SECRET` e codifique o resultado em hex.
4. Anteponha `v1,sha256=` e compare contra `X-Webhook-Signature` usando uma função de igualdade de tempo constante.
5. Rejeite a requisição se o timestamp estiver fora de uma janela de frescor aceitável (uma tolerância de 5 minutos é típica) para prevenir replay.

Além de `X-Webhook-Signature` e `X-Webhook-Timestamp`, o plugin define apenas `X-Webhook-Event` (o tipo de evento). Ele não envia `X-Webhook-Event-Type`, `X-Webhook-Routing-Key` nem `X-Webhook-Delivery-Attempt`.

<AccordionGroup>
  <Accordion title="JavaScript">
    ```javascript theme={null}
    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('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 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 = 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 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("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>

### Processamento idempotente de webhooks

Seu endpoint pode receber o mesmo evento mais de uma vez (entrega at-least-once). Use `transferId` + `event` como chave composta 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
}
```

## Padrões de tratamento de erros

***

Mapeie códigos de erro da API para ações voltadas ao usuário. Consulte a [lista completa de erros](/pt/reference/midaz/plugins/ted/ted-error-list) para todos os códigos.

| Cenário                                           | Mensagem ao usuário                                                                         | Ação                                                                                                                                  |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| **Fora do horário de funcionamento** (`BTF-0010`) | "Transferências disponíveis seg–sex, 06:30–17:00 (Brasília). Próxima janela: {horário}"     | Exibir próximo horário disponível                                                                                                     |
| **Limite diário excedido** (`BTF-0011`)           | "Limite diário de transferências atingido. Tente novamente amanhã."                         | Exibir limite restante                                                                                                                |
| **Transferência duplicada** (`BTF-0012`)          | "Esta transferência já foi enviada."                                                        | Retornar o `transferId` original                                                                                                      |
| **Dados do destinatário inválidos** (`BTF-0001`)  | "Verifique os dados do destinatário e tente novamente."                                     | Destacar campos inválidos                                                                                                             |
| **Iniciação expirada** (`BTF-0202`)               | "Sessão expirada. Por favor, inicie uma nova transferência."                                | Reiniciar o fluxo de iniciação                                                                                                        |
| **JD SPB indisponível** (`TRANSPORT`, HTTP `503`) | "Serviço de transferência temporariamente indisponível. Tente novamente em alguns minutos." | Retry com backoff; detecte via `503` + código de fornecedor JD sem transformação (`TRANSPORT`, `ACE95`, …), não por um prefixo `BTF-` |
| **Midaz indisponível** (`BTF-2000`)               | "Serviço temporariamente indisponível. Tente novamente em alguns minutos."                  | Retry com backoff                                                                                                                     |

As respostas de erro seguem esta estrutura:

```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"
    }
  }
}
```

## Checklist para entrar em produção

***

Antes de habilitar a integração em produção:

* [ ] `X-Idempotency` é enviado em cada requisição de initiate, process e cancel
* [ ] Lógica de retry implementada com backoff exponencial para erros 5xx/503
* [ ] Endpoint de webhook implantado e retornando `200` em até 5 segundos
* [ ] Validação de assinatura ativa no endpoint de webhook
* [ ] Deduplicação de eventos de webhook implementada usando `transferId + event`
* [ ] Horários de funcionamento validados no lado do cliente antes de chamar initiate (reduz 422s desnecessários)
* [ ] Tanto `transferId` quanto `confirmationNumber` armazenados para reconciliação
* [ ] Estados terminais (`COMPLETED`, `REJECTED`, `FAILED`, `CANCELLED`) tratados na UI
* [ ] Expiração da iniciação (24h) tratada — o usuário é solicitado a reiniciar se a janela encerrar
* [ ] Readiness do serviço monitorado no seu sistema de alertas para deployments BYOC
* [ ] Redis está disponível e monitorado — o serviço não aceitará requisições se o Redis estiver inacessível
* [ ] `PLUGIN_AUTH_ENABLED=true` configurado em produção, com um `PLUGIN_AUTH_ADDRESS` válido (HTTPS)
