Pular para o conteúdo principal
Este guia é para desenvolvedores que implementam a integração com o plugin TED. 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
A resposta inclui um cabeçalho X-Idempotency-Replayed: true quando a resposta é servida do cache.
POST /v1/transfers/initiate
X-Tenant-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-Tenant-Id), senderAccountId, dados do destinatário e valor, e o armazena no Redis por 60 segundos (configurável). 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; verifique /health/ready se persistir
Cronograma de backoff recomendado para 5xx/503: 0s, 5s, 25s, 60s, 120s (5 tentativas no total).
Para 503 BTF-1000 (JD SPB indisponível): 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 um cabeçalho X-Signature com uma assinatura HMAC-SHA256. Sempre valide antes de processar.
X-Signature: sha256=a1b2c3d4e5f6...
Calcule o HMAC sobre os bytes brutos do corpo da requisição (não JSON.stringify — a representação em bytes deve corresponder exatamente) usando seu webhookSecret, depois compare usando uma função de tempo 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))
}

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

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ívelBTF-1000”Serviço de transferência temporariamente indisponível. Tente novamente em alguns minutos.”Retry com backoff
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
  • GET /health/ready 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