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-Signatureheader. 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/cancelwhile the conversion is inawaiting_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 afterfunded. Payload includesfailure_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-Idorconversion_idas 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-Keyon every mutatingPOST. Safe retry of an already-successful call returns the same response with no side effects.- Idempotent webhook handler. Use
X-Blendfi-Event-Idorconversion_idas 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/cancelto 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-Keyon 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_idcaptured in every error log: your key for support. - Retry policy on
5xxand429with exponential backoff and the sameIdempotency-Key. - Handle the end customer's KYC states:
not_started,pending,approved,rejected,expired. - UX for
conversion.failed: surfacefailure_reasonto the end customer; support takes over from there. - Quote expiration handling: countdown in the UI; quote again at accept if needed.
Next steps
- Build a USDT → Pix payout: the other direction.
- Conversion: the full state machine map.
- Idempotency: the semantics of the
Idempotency-Keyheader. - Errors and retries: the safety net underpinning everything above.
