> ## 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 do Bank Transfer corretamente: idempotência, estratégia de novas tentativas, tratamento de estado e padrões de validação de webhook para transferências confiáveis.

Este guia é para desenvolvedores que implementam a integração com o plugin Bank Transfer. Ele cobre os padrões e as decisões que vão além de chamadas isoladas a endpoints: idempotência, estratégia de novas tentativas, tratamento de estado e validação de webhook.

Para parâmetros de endpoint e schemas de resposta, veja a [Referência da API](/pt/reference/interfaces/ted-jd/initiate-transfer).

## Idempotência

***

Cada requisição que altera dados (initiate, process, cancel) exige um header `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 (por exemplo, o ID do pedido no seu sistema)
* Comprimento máximo: 255 caracteres
* O plugin dá a cada chave o escopo da organização efetiva. A mesma chave vinda de duas organizações conta como duas requisições distintas.
* O plugin retorna uma resposta em cache durante a janela de idempotência configurada (`IDEMPOTENCY_RETRY_WINDOW_SEC`, padrão de 300 segundos)
* Uma resposta reenviada é idêntica byte a byte à original: mesmo código de status, mesmo corpo. A resposta não tem header que marque um reenvio, então projete seu cliente para ficar seguro nos dois casos.

```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 entre operações diferentes. Não reutilize uma chave de initiate para processar ou cancelar a mesma transferência.
</Warning>

### Detecção de duplicatas

Além das chaves de idempotência, o plugin detecta duplicatas por conteúdo. Ele monta uma impressão digital a partir de:

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

O plugin guarda a impressão digital no Redis por 5 minutos. O padrão é 300 segundos. Os operadores ajustam isso por tenant pela configuração `idempotency.duplicate_guard_ttl_seconds` do systemplane. A organização não faz parte da impressão digital. O isolamento de tenant vem do prefixo da chave no Redis. O plugin rejeita a requisição com `409 BTF-0012` se o cliente já enviou uma transferência correspondente dentro da janela.

Isso pega os casos em que o cliente envia a mesma transferência com uma chave de idempotência diferente. Um exemplo é uma nova tentativa após um timeout, quando o cliente não recebeu a resposta original.

## Estratégia de novas tentativas

***

Use backoff exponencial para erros transitórios. Não repita a tentativa em todo erro.

| Status HTTP | Nova tentativa? | Observações                                                                           |
| ----------- | --------------- | ------------------------------------------------------------------------------------- |
| `400`       | Não             | Erro de validação — corrija a requisição antes de tentar de novo                      |
| `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             | Rate limit — aguarde o valor do header `Retry-After` (em segundos)                    |
| `500`       | Sim             | Erro interno — tente de novo com backoff                                              |
| `503`       | Sim             | Indisponível — tente de novo 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`. O campo `error.code` carrega então o código bruto do fornecedor JD, por exemplo `TRANSPORT` para falhas de transporte ou `ACE95` para timeouts. O plugin não embrulha as falhas da cadeia JD em um código `BTF-`. Sinalize a transferência para conciliação manual depois que as novas tentativas se esgotam. Não tente de novo sem limite. A rede JD SPB tem horário de funcionamento definido.
</Note>

## Tratamento de estado

***

### Máquina de estados da TED OUT

As transferências seguem uma progressão estrita. Você não pode cancelar uma transferência depois que ela sai de `CREATED` ou `PENDING`.

<Frame>
  <img src="https://mintcdn.com/lerian-49cb71fc/TGLv2g3qhXqf0dqb/images/pt/d2/ted-state-machine-ted-out.svg?fit=max&auto=format&n=TGLv2g3qhXqf0dqb&q=85&s=cde3d78777bd42df90e2a2bfd49b2dca" alt="Máquina de estados da 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`    | Confirmada pelo usuário, na fila para envio         | Mostre "Processando" na interface; consulte ou aguarde o webhook                |
| `PENDING`    | Enviada à JD, aguardando confirmação de recebimento | Mostre "Processando"; não permita cancelamento                                  |
| `PROCESSING` | A JD aceitou e está roteando a transferência        | Mostre "Processando"; SLA típico abaixo de 10 minutos                           |
| `COMPLETED`  | Liquidada                                           | Mostre a confirmação com `confirmationNumber`                                   |
| `REJECTED`   | A JD rejeitou (dados inválidos, violação de regra)  | Mostre o erro ao usuário; os recursos já foram liberados                        |
| `FAILED`     | JD inacessível ou tempo esgotado                    | Mostre o erro; os recursos já foram liberados; permita nova tentativa se quiser |
| `CANCELLED`  | Cancelada antes do envio                            | Mostre a confirmação do cancelamento                                            |

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

O endpoint initiate cria uma entidade `PaymentInitiation`. Essa entidade tem seu próprio ciclo de vida antes de o plugin criar um `Transfer`.

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

### Máquina de estados da TED IN

<Frame>
  <img src="https://mintcdn.com/lerian-49cb71fc/TGLv2g3qhXqf0dqb/images/pt/d2/ted-state-machine-ted-in.svg?fit=max&auto=format&n=TGLv2g3qhXqf0dqb&q=85&s=d625848e85370f573073a3382685311a" alt="Máquina de estados da 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 estado `PENDING`. A liquidação é atômica e instantânea.

<Frame>
  <img src="https://mintcdn.com/lerian-49cb71fc/TGLv2g3qhXqf0dqb/images/pt/d2/ted-state-machine-ted-p2p.svg?fit=max&auto=format&n=TGLv2g3qhXqf0dqb&q=85&s=bb68bc415340b81464c9d459b1a6db62" alt="Máquina de estados do 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 você ainda não configurou webhooks, consulte `GET /v1/transfers/{transferId}`. Use no máximo 10 tentativas com o mesmo cronograma de backoff das novas tentativas. Sinalize a transferência para revisão manual depois de 10 minutos sem estado terminal (`COMPLETED`, `REJECTED`, `FAILED`, `CANCELLED`).

Veja [Obter transferência](/pt/reference/interfaces/ted-jd/retrieve-transfer) e [Webhooks](/pt/interfaces/ted-jd/ted-webhooks).

## Integração de webhook

***

Para os schemas de payload dos eventos e a lista completa de eventos, veja [Webhooks](/pt/interfaces/ted-jd/ted-webhooks).

### Validação da assinatura

Cada requisição de webhook inclui headers 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 montou a requisição
* `X-Webhook-Event`: o tipo do evento (por exemplo, `transfer.completed`). Este header 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 tem quatro partes, nesta ordem: o prefixo `v1:`, o valor do timestamp vindo de `X-Webhook-Timestamp`, um ponto ASCII (`.`) e então os **bytes brutos do corpo da requisição**. Use os bytes do corpo exatamente como chegam pela rede. Não os analise nem os recodifique antes.

Para validar:

1. Leia `X-Webhook-Signature` e `X-Webhook-Timestamp` nos headers da requisição.
2. Monte a string assinada: `"v1:" + timestamp + "." + rawBody`.
3. Calcule `HMAC-SHA256` sobre a string assinada com seu `WEBHOOK_SIGNING_SECRET` e codifique o resultado em hexadecimal.
4. Coloque `v1,sha256=` na frente e compare com `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 validade aceitável (uma tolerância de 5 minutos é típica) para evitar replay.

Além de `X-Webhook-Signature` e `X-Webhook-Timestamp`, o plugin define apenas `X-Webhook-Event` (o tipo do 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 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>

### Processamento idempotente de webhook

Seu endpoint pode receber o mesmo evento mais de uma vez (entrega pelo menos uma vez). 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 os códigos de erro da API para ações voltadas ao usuário. Veja a [lista completa de erros](/pt/reference/interfaces/ted-jd/ted-error-list) para todos os códigos.

| Cenário                                           | Mensagem para o usuário                                                                     | Ação                                                                                                                                |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| **Fora do horário de funcionamento** (`BTF-0010`) | "Transferências disponíveis de seg. a sex., 06:30–17:00 (Brasília). Próxima janela: {time}" | Mostre o próximo horário disponível                                                                                                 |
| **Limite diário excedido** (`BTF-0011`)           | "Limite diário de transferência atingido. Tente de novo amanhã."                            | Mostre o limite restante                                                                                                            |
| **Transferência duplicada** (`BTF-0012`)          | "Esta transferência já foi enviada."                                                        | Retorne o `transferId` original                                                                                                     |
| **Dados do destinatário inválidos** (`BTF-0001`)  | "Verifique os dados do destinatário e tente de novo."                                       | Destaque os campos inválidos                                                                                                        |
| **Iniciação expirada** (`BTF-0202`)               | "Sessão expirada. Comece uma nova transferência."                                           | Reinicie o fluxo de iniciação                                                                                                       |
| **JD SPB indisponível** (`TRANSPORT`, HTTP `503`) | "Serviço de transferência temporariamente indisponível. Tente de novo em alguns minutos."   | Nova tentativa com backoff; detecte pelo `503` + código bruto do fornecedor JD (`TRANSPORT`, `ACE95`, …), não por um prefixo `BTF-` |
| **Midaz indisponível** (`BTF-2000`)               | "Serviço temporariamente indisponível. Tente de novo em alguns minutos."                    | Nova tentativa 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 de entrada em produção

***

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

* [ ] Envie `X-Idempotency` em cada requisição de initiate, process e cancel
* [ ] Lógica de nova tentativa implementada com backoff exponencial para erros 5xx/503
* [ ] Endpoint de webhook com o deploy feito e retornando `200` em até 5 segundos
* [ ] Validação de assinatura ativa no endpoint de webhook
* [ ] Deduplicação de eventos de webhook implementada com `transferId + event`
* [ ] Horário de funcionamento validado no cliente antes de chamar initiate (reduz 422 desnecessários)
* [ ] `transferId` e `confirmationNumber` armazenados para conciliação
* [ ] Estados terminais (`COMPLETED`, `REJECTED`, `FAILED`, `CANCELLED`) tratados na interface
* [ ] Expiração da iniciação (24h) tratada: peça ao usuário para recomeçar quando a janela passar
* [ ] Readiness do serviço monitorada no seu sistema de alertas para deploys BYOC
* [ ] Redis acessível e monitorado: o serviço rejeita requisições quando o Redis está fora
* [ ] `PLUGIN_AUTH_ENABLED=true` configurado em produção, com um `PLUGIN_AUTH_ADDRESS` válido (HTTPS)
