BlendFi

Build a PIX → USDT checkout

End-to-end path for an onramp integration. From quote to completion webhook.

This tutorial covers a full onramp integration: the end customer pays via Pix and BlendFi settles USDT to the destination address you control. Use it as a starting point for your implementation.

Prerequisites

  • Active API key. See Sandbox and keys.
  • End customer registered with platform KYC approved. See KYC flow.
  • Polygon address you control to receive USDT (any wallet works in sandbox).
  • Webhook endpoint configured with signature verification using the X-Blendfi-Signature header. See Signature verification.

1. Create the quote

Your organization calls POST /v1/quotes with the end customer, the pix_onramp type, the BRL amount, and the on-chain destination address.

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()

The response carries id, exchange_rate, source_amount, target_amount, expires_at. Show target_amount to the end customer as the USDT they will receive. The quote is valid for 15 minutes.

📋 Full schema in Reference (coming soon)

Detailed payload documentation will appear here alongside API availability. The fields in the example follow the conceptual design at Quote.

2. Accept the quote and show the Pix QR

End customer's confirmation: accept the quote with POST /v1/quotes/:id/accept. In a single atomic call, BlendFi creates the conversion, issues the Pix QR, reserves the limit, and opens the 15-minute window.

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

The response is the full conversion, with id, status='awaiting_deposit', pix_qr_code, pix_tx_id, deposit_window_expires_at. Render pix_qr_code to the end customer alongside the exact BRL amount and the remaining window.

UX recommendations:

  • Render the Pix QR prominently with the exact BRL amount.
  • Show a countdown to deposit_window_expires_at.
  • Offer a "cancel" button that calls POST /v1/conversions/:id/cancel while the conversion is in awaiting_deposit.

3. End customer pays the Pix QR (out of band)

In production, the end customer scans the QR in their bank app and pays. In sandbox, use the test helper to simulate the payment.

# Sandbox only
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. Receive the webhook

BlendFi delivers a webhook based on the outcome. Verify the signature in the X-Blendfi-Signature header (format t=<unix>,v1=<hex>: HMAC-SHA256 over {t}.{raw_body}, hex-encoded).

Possible events:

  • conversion.completed: happy path. Pix confirmed and USDT settled to the destination address.
  • conversion.failed: irrecoverable error after funded. Payload includes failure_reason.

Minimal handler in 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);
  },
);

Full verification in curl, Node, and Python: Signature verification.

Recommendations:

  • Treat the handler as idempotent; the same delivery may arrive more than once.
  • Use X-Blendfi-Event-Id or conversion_id as a deduplication key.

📋 Full schema in Reference (coming soon)

Exact payloads land in the webhooks section once the conversion.* event catalog is published.

5. Happy path (conversion.completed)

Received conversion.completed. The Pix was confirmed and BlendFi delivered USDT on-chain to the destination address. Show confirmation to the end customer. The payload includes the USDT transaction hash for your internal reference or audit.

End of the nominal flow.

6. Error handling and idempotency

  • Idempotency-Key on every mutating POST. Safe retry of an already-successful call returns the same response with no side effects.
  • Idempotent webhook handler. Use X-Blendfi-Event-Id or conversion_id as a deduplication key.
  • Signature verification. Reject any delivery whose signature does not match.
  • Lock handling. A lock error at accept means the end customer already has another open onramp conversion. Have the UX deal with the existing conversion, not create a new one. See Per-user, per-type lock.
  • Cancellation. If the end customer gives up before paying, call POST /v1/conversions/:id/cancel to release the reserve and lock.
  • Reconciliation. On incident, read GET /v1/conversions?status=... to reconstruct the state of what is open.

Failure variants in sandbox

Test helpers let you exercise error paths:

# Window expires without payment
curl -X POST $BLENDFI_BASE/v1/test_helpers/conversions/$CONVERSION_ID/expire \
  -H "Authorization: Bearer $BLENDFI_KEY" -H "Idempotency-Key: $(uuidgen)"

# On-chain settlement fails after payment
curl -X POST $BLENDFI_BASE/v1/test_helpers/conversions/$CONVERSION_ID/fail-crypto \
  -H "Authorization: Bearer $BLENDFI_KEY" -H "Idempotency-Key: $(uuidgen)"

Common pitfalls

Reusing Idempotency-Key

If you use the same Idempotency-Key for two different operations, the second returns 409 idempotency_key_reused. Generate a fresh UUID per logical operation.

Polling instead of webhooks

Don't poll GET /v1/conversions/:id in a tight loop. Webhooks announce every state change; polling is for occasional reconciliation.

Body re-serialization in your webhook handler

Most invalid-signature support tickets come from middleware re-parsing the body before verification. Use express.raw() (Node) or request.get_data() (Flask).

Production checklist

Before switching from sk_test_… to sk_live_…:

  • Idempotency-Key on every mutating call: same key for retries, fresh key for new operations.
  • Webhook signature verification with constant-time compare and 300-second replay window.
  • Idempotent webhook handler: dedup on X-Blendfi-Event-Id, ack in under 500 ms.
  • request_id captured in every error log: your key for support.
  • Retry policy on 5xx and 429 with exponential backoff and the same Idempotency-Key.
  • Handle the end customer's KYC states: not_started, pending, approved, rejected, expired.
  • UX for conversion.failed: surface failure_reason to the end customer; support takes over from there.
  • Quote expiration handling: countdown in the UI; quote again at accept if needed.

Next steps

On this page