BlendFi

Implemente um onramp Pix → USDT

Caminho de ponta a ponta para uma integração onramp. Da cotação ao webhook de conclusão.

Este tutorial cobre uma integração onramp completa: o cliente final paga via Pix e a BlendFi liquida USDT no endereço de destino que você controla. Use como ponto de partida para sua implementação.

Pré-requisitos

  • Chave de API ativa. Veja Sandbox e chaves.
  • Cliente final cadastrado e com KYC de plataforma aprovado. Veja Fluxo de KYC.
  • Endereço Polygon que você controla para receber USDT (qualquer wallet funciona no sandbox).
  • Endpoint de webhook configurado com verificação de assinatura via header X-Blendfi-Signature. Veja Verificação de assinatura.

1. Crie a cotação

Sua organização chama POST /v1/quotes com o cliente final, o tipo pix_onramp, o valor em BRL e o endereço de destino on-chain.

curl -X POST $BLENDFI_BASE/v1/quotes \
  -H "Authorization: Bearer $BLENDFI_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{
    "user_id": "usr_01J...",
    "transaction_type": "pix_onramp",
    "source_amount": "100.00",
    "source_currency": "BRL",
    "target_currency": "USDT",
    "destination_wallet_address": "0xYOUR_POLYGON_ADDRESS",
    "destination_wallet_network": "polygon"
  }'
const quoteRes = await fetch(`${BASE}/v1/quotes`, {
  method: "POST",
  headers: { ...auth, "idempotency-key": crypto.randomUUID() },
  body: JSON.stringify({
    user_id: "usr_01J...",
    transaction_type: "pix_onramp",
    source_amount: "100.00",
    source_currency: "BRL",
    target_currency: "USDT",
    destination_wallet_address: "0xYOUR_POLYGON_ADDRESS",
    destination_wallet_network: "polygon",
  }),
});
const quote = await quoteRes.json();
quote = requests.post(
    f"{BASE}/v1/quotes",
    headers={**auth, "idempotency-key": str(uuid.uuid4())},
    json={
        "user_id": "usr_01J...",
        "transaction_type": "pix_onramp",
        "source_amount": "100.00",
        "source_currency": "BRL",
        "target_currency": "USDT",
        "destination_wallet_address": "0xYOUR_POLYGON_ADDRESS",
        "destination_wallet_network": "polygon",
    },
).json()

A resposta carrega id, exchange_rate, source_amount, target_amount, expires_at. Mostre target_amount ao cliente final como o valor de USDT que ele vai receber. A cotação vale 15 minutos.

📋 Schema completo em Reference (em breve)

A documentação detalhada do payload aparece aqui em conjunto com a disponibilidade da API. Os campos do exemplo acompanham o desenho conceitual em Cotação.

2. Aceite a cotação e mostre o QR Pix

Confirmação do cliente final: aceite a cotação com POST /v1/quotes/:id/accept. Em uma única chamada atômica, a BlendFi cria a conversão, emite o QR Pix, reserva o limite e abre a janela de 15 minutos.

curl -X POST $BLENDFI_BASE/v1/quotes/$QUOTE_ID/accept \
  -H "Authorization: Bearer $BLENDFI_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

A resposta é a conversão completa, com id, status='awaiting_deposit', pix_qr_code, pix_tx_id, deposit_window_expires_at. Renderize pix_qr_code ao cliente final junto com o valor exato em BRL e o prazo restante.

Recomendações de UX:

  • Renderize o QR Pix em destaque com o valor exato em BRL.
  • Mostre um contador regressivo até deposit_window_expires_at.
  • Ofereça um botão "cancelar" que chama POST /v1/conversions/:id/cancel enquanto a conversão estiver em awaiting_deposit.

3. Cliente final paga o QR Pix (fora da API)

Em produção, o cliente final escaneia o QR no app do banco e paga. Em sandbox, use o test helper para simular o pagamento.

# Apenas no sandbox
curl -X POST $BLENDFI_BASE/v1/test_helpers/conversions/$CONVERSION_ID/simulate-onramp-pix-paid \
  -H "Authorization: Bearer $BLENDFI_KEY" \
  -H "Idempotency-Key: $(uuidgen)"

4. Receba o webhook

A BlendFi entrega um webhook conforme o desfecho. Verifique a assinatura no header X-Blendfi-Signature (formato t=<unix>,v1=<hex>: HMAC-SHA256 sobre {t}.{raw_body}, codificado em hexadecimal).

Eventos possíveis:

  • conversion.completed: caminho feliz. O Pix foi confirmado e o USDT liquidou no endereço de destino.
  • conversion.failed: erro irrecuperável após funded. Payload inclui failure_reason.

Handler mínimo em Node:

import express from "express";
import crypto from "node:crypto";

const app = express();
const SECRET = process.env.BLENDFI_WEBHOOK_SECRET;

app.post(
  "/blendfi-webhooks",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const sigHdr = req.header("x-blendfi-signature") ?? "";
    const parts = Object.fromEntries(
      sigHdr.split(",").map((p) => p.split("=", 2)),
    );
    const ts = parts.t;
    const sig = parts.v1;
    const expected = crypto
      .createHmac("sha256", SECRET)
      .update(`${ts}.${req.body.toString("utf8")}`)
      .digest("hex");
    if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
      return res.status(401).send("bad signature");
    }

    res.sendStatus(200);
    const event = JSON.parse(req.body.toString("utf8"));
    enqueue(event);
  },
);

Verificação completa em curl, Node e Python: Verificação de assinatura.

Recomendações:

  • Trate o handler como idempotente; a mesma entrega pode chegar mais de uma vez.
  • Use X-Blendfi-Event-Id ou conversion_id como chave de controle de duplicatas.

📋 Schema completo em Reference (em breve)

Os payloads exatos vão na seção de webhooks quando o catálogo de eventos conversion.* for publicado.

5. Caminho feliz (conversion.completed)

Recebeu conversion.completed. O Pix foi confirmado e a BlendFi entregou USDT on-chain no endereço de destino. Mostre confirmação ao cliente final. O payload inclui o hash da transação USDT para sua referência interna ou de auditoria.

Fim do fluxo nominal.

6. Tratamento de erros e idempotência

  • Idempotency-Key em todo POST mutador. Retentativa segura de uma chamada já bem-sucedida retorna a mesma resposta sem efeitos colaterais.
  • Handler de webhook idempotente. Use X-Blendfi-Event-Id ou conversion_id como chave de controle de duplicatas.
  • Verificação de assinatura. Rejeite qualquer entrega cuja assinatura não confira.
  • Tratamento de lock. Erro de lock no aceite indica que o cliente final já tem outra conversão onramp aberta. Leve a UX a tratar a conversão existente, não a criar uma nova. Veja Lock por usuário e tipo.
  • Cancelamento. Se o cliente final desistir antes de pagar, chame POST /v1/conversions/:id/cancel para liberar reserva e lock.
  • Reconciliação. Em incidente, leia GET /v1/conversions?status=... para reconstruir o estado do que está aberto.

Variantes de falha no sandbox

Os test helpers permitem exercitar caminhos de erro:

# Janela expira sem pagamento
curl -X POST $BLENDFI_BASE/v1/test_helpers/conversions/$CONVERSION_ID/expire \
  -H "Authorization: Bearer $BLENDFI_KEY" -H "Idempotency-Key: $(uuidgen)"

# Liquidação on-chain falha após o pagamento
curl -X POST $BLENDFI_BASE/v1/test_helpers/conversions/$CONVERSION_ID/fail-crypto \
  -H "Authorization: Bearer $BLENDFI_KEY" -H "Idempotency-Key: $(uuidgen)"

Armadilhas comuns

Reuso de Idempotency-Key

Se você usar a mesma Idempotency-Key para duas operações diferentes, a segunda retorna 409 idempotency_key_reused. Gere um UUID novo por operação lógica.

Polling em vez de webhooks

Não consulte GET /v1/conversions/:id em loop apertado. Os webhooks anunciam toda mudança de estado; polling fica para reconciliação ocasional.

Re-serialização do body no handler de webhook

A maioria dos chamados sobre assinatura inválida vem de middleware re-parseando o body antes da verificação. Use express.raw() (Node) ou request.get_data() (Flask).

Checklist de produção

Antes de migrar de sk_test_… para sk_live_…:

  • Idempotency-Key em toda chamada mutadora: mesma chave para retentativas, chave nova para operações novas.
  • Verificação de assinatura de webhook com comparação em tempo constante e janela de replay de 300 s.
  • Handler de webhook idempotente: controle de duplicatas pelo X-Blendfi-Event-Id, resposta em menos de 500 ms.
  • request_id capturado em todos os logs de erro: sua chave para o suporte.
  • Política de retentativa em 5xx e 429 com backoff exponencial e a mesma Idempotency-Key.
  • Tratamento dos estados de KYC do cliente final: not_started, pending, approved, rejected, expired.
  • UX para conversion.failed: mostre o failure_reason ao cliente final; o suporte assume a partir daí.
  • Tratamento de expiração de cotação: countdown na UI; cote de novo no aceite se necessário.

Próximos passos

Nesta página