Pular para o conteúdo 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.

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.

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 por organização — a mesma chave de duas organizações diferentes é tratada como duas requisições distintas
  • As respostas em cache são retornadas por 24 horas
  • 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
POST /v1/transfers/initiate
X-Organization-Id: 019c9ac2-3f5d-7df9-9215-bdccc1451def
X-Idempotency: 7f3d9a1b-4e2c-4f8a-b3d1-9e6f2a4c8b7e
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.

Detecção de duplicatas

Além das chaves de idempotência, o plugin detecta duplicatas baseadas em conteúdo. Ele gera um hash a partir da organização (do cabeçalho X-Organization-Id), senderAccountId, dados do destinatário e valor, e o armazena no Redis por 5 minutos (padrão 300 segundos, configurável via DUPLICATE_GUARD_TTL_SEC). 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 HTTPTipo de erroRetry?Observações
400Erro de validaçãoNãoCorrija a requisição antes de tentar novamente
404Não encontradoNãoO recurso não existe
409DuplicataNãoIdempotente — use a resposta original
410ExpiradoNãoCrie uma nova iniciação
422Regra de negócioNãoHorário de funcionamento, limites — a condição deve mudar primeiro
429Limite de taxaSimAguarde o valor do cabeçalho Retry-After (em segundos)
500Erro internoSimRetry com backoff
503IndisponívelSimRetry com backoff
Cronograma de backoff recomendado para 5xx/503: 0s, 5s, 25s, 60s, 120s (5 tentativas no total).
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.

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.
Máquina de estados TED OUT
O que fazer em cada estado:
EstadoSignificadoAção recomendada
CREATEDConfirmado pelo usuário, aguardando envioExibir “Processando” na UI; aguardar polling ou webhook
PENDINGEnviado para a JD, aguardando reconhecimentoExibir “Processando”; não permitir cancelamento
PROCESSINGJD aceitou e está roteando a transferênciaExibir “Processando”; SLA típico inferior a 10 minutos
COMPLETEDLiquidadaExibir confirmação com confirmationNumber
REJECTEDJD rejeitou (dados inválidos, violação de regra)Exibir erro ao usuário; fundos já liberados
FAILEDJD inacessível ou timeoutExibir erro; fundos já liberados; permitir retry se desejado
CANCELLEDCancelada antes do envioExibir 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.
Máquina de estados da iniciação

Máquina de estados do TED IN

Máquina de estados TED IN

Máquina de estados do P2P

O P2P não tem um estado PENDING. A liquidação é atômica e instantânea.
Máquina de estados P2P

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 e Webhooks.

Integração com webhooks


Para esquemas de payload de eventos e a lista completa de eventos, consulte Webhooks.

Validação de assinatura

Toda requisição de webhook inclui dois cabeçalhos que seu endpoint deve usar para verificar a autenticidade:
  • X-Webhook-Signature — assinatura HMAC-SHA256 no formato sha256=<hex>
  • X-Webhook-Timestamp — timestamp Unix em segundos (UTC) de quando o plugin construiu a requisição
O plugin calcula a assinatura assim:
X-Webhook-Signature: sha256=hex(HMAC_SHA256(WEBHOOK_SIGNING_SECRET, <timestamp> + "." + <raw_body>))
O payload assinado é o valor do timestamp (a string literal enviada em X-Webhook-Timestamp), seguido de um único ponto ASCII (.), seguido dos bytes brutos do corpo da requisição — exatamente como recebidos, antes de qualquer parseo ou re-codificaçã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 o payload assinado: timestamp + "." + rawBody.
  3. Calcule HMAC-SHA256 sobre o payload assinado usando seu WEBHOOK_SIGNING_SECRET e codifique o resultado em hex.
  4. Anteponha 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.
O plugin também define X-Webhook-Event-Type, X-Webhook-Routing-Key e X-Webhook-Delivery-Attempt para observabilidade — esses não fazem parte do payload assinado e não devem ser usados para autenticação.
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))
}

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.
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 para todos os códigos.
CenárioCódigoMensagem ao usuárioAção
Fora do horárioBTF-0010”Transferências disponíveis seg–sex, 06:30–17:00 (Brasília). Próxima janela: Exibir próximo horário disponível
Limite diário excedidoBTF-0011”Limite diário de transferências atingido. Tente novamente amanhã.”Exibir limite restante
Transferência duplicadaBTF-0012”Esta transferência já foi enviada.”Retornar o transferId original
Dados do destinatário inválidosBTF-0001”Verifique os dados do destinatário e tente novamente.”Destacar campos inválidos
Iniciação expiradaBTF-0202”Sessão expirada. Por favor, inicie uma nova transferência.”Reiniciar o fluxo de iniciação
JD SPB indisponívelTRANSPORT (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ívelBTF-2000”Serviço temporariamente indisponível. Tente novamente em alguns minutos.”Retry com backoff
As respostas de erro seguem esta estrutura:
{
  "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"
    }
  }
}

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 PLUGIN_AUTH_ADDRESS, PLUGIN_AUTH_CLIENT_ID e PLUGIN_AUTH_CLIENT_SECRET válidos