Build a USDT → Pix payout
End-to-end path for an offramp integration. From quote to completion webhook, including standby handling.
This tutorial covers a full offramp integration, from the first quote to the completion webhook. Use it as a starting point for your implementation.
Prerequisites
- Active API key. See Sandbox and keys.
- End customer registered with KYC approved. See KYC flow.
- Destination Pix key registered on the end customer.
- Webhook endpoint configured with signature verification using the
X-Blendfi-Signatureheader. See Webhooks integration.
Step 1: create the quote
Your organization calls POST /v1/quotes with the end customer, the pix_offramp type, the amount in USDT (source_amount) or BRL (target_amount), and the destination Pix key.
POST /v1/quotes HTTP/1.1
Authorization: Bearer sk_test_…
Idempotency-Key: <uuid>
Content-Type: application/json
{
"user_id": "usr_01J...",
"transaction_type": "pix_offramp",
"source_amount": "100.00",
"pix_key": "..."
}The response carries id, exchange_rate, source_amount, target_amount, expires_at. Show target_amount to the end customer as the BRL value they will receive. The rate is valid for 5 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.
Step 2: accept the quote and show the deposit address
End customer's confirmation: accept the quote with POST /v1/quotes/:id/accept. In a single atomic call, BlendFi creates the conversion, issues the deposit address, reserves the limit, and opens the 15-minute window.
POST /v1/quotes/qt_01J.../accept HTTP/1.1
Authorization: Bearer sk_test_…
Idempotency-Key: <uuid>The response is the full conversion, with id, status='awaiting_deposit', deposit_address, deposit_address_network, deposit_window_expires_at. Show deposit_address with a QR to the end customer, alongside expected_source_amount and the remaining window.
UX recommendations:
- Render a countdown to
deposit_window_expires_at. After the deadline, the deposit goes tostandby. - Show
expected_source_amountprominently. End customers who tweak the amount on the fly cause standby. - Offer a "cancel" button that calls
POST /v1/conversions/:id/cancelwhile the conversion is inawaiting_deposit.
Step 3: end customer transfers USDT (out of band)
The on-chain deposit happens outside the API. Your integration calls nothing in this step; it only waits for the webhook.
Step 4: receive the webhook
BlendFi delivers a webhook based on the outcome. Verify the signature in the X-Blendfi-Signature header (the signed payload is {t}.{raw_body}, HMAC-SHA256 with the per-endpoint secret).
Possible events:
conversion.completed: happy path. Pix delivered to the recipient.conversion.standby: divergence (under, over, or window expired). Payload includesstandby_reason,received_amount,expected_source_amount. See Step 5b.conversion.failed: irrecoverable error afterfundedorliquidated. Payload includesfailure_reason.conversion.abandoned: 7 days in standby with no action. Manual resolution with BlendFi.
Recommendations:
- Treat the handler as idempotent: the same delivery may arrive more than once.
- Use
conversion_idas a deduplication key on your side.
📋 Full schema in Reference (coming soon)
Exact payloads land in the webhooks section once the conversion.* event catalog is published.
Step 5a: happy path (conversion.completed)
Received conversion.completed. The Pix is delivered. Show confirmation to the end customer. The payload includes the end-to-end Pix identifier for your internal reference or audit.
End of the nominal flow.
Step 5b: standby path (conversion.standby)
Received conversion.standby. The conversion is stopped with received_amount in custody. Decide based on standby_reason:
standby_reason | Usual decision |
|---|---|
under_funded | Liquidate at the received amount (end customer ends up with less BRL than expected, communicate the divergence) or contact support for a case-by-case resolution. |
over_funded | Liquidate at the received amount (end customer gets extra BRL) or contact support. |
window_expired | Liquidate (a new rate will be quoted) or contact support. |
To liquidate:
POST /v1/conversions/cnv_01J.../liquidate HTTP/1.1
Authorization: Bearer sk_test_…
Idempotency-Key: <uuid>BlendFi quotes a new rate server-side over the current received_amount and dispatches settlement. The response carries the conversion with liquidation_quote_id populated and status liquidated. You will still receive conversion.completed (or conversion.failed) once Pix settlement finishes.
To wait and see: do nothing for up to 7 days. After the deadline, the conversion becomes abandoned and resolution is manual. See Abandoned conversion.
To cancel before any deposit arrives (a different scenario from standby): POST /v1/conversions/:id/cancel, valid only in awaiting_deposit. Once a deposit arrives, cancel is no longer an option. See Cancellation.
Step 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
conversion_id(and event type) as a deduplication key. - Signature verification. Reject any delivery whose signature does not match. See Concepts → Webhooks.
- Lock handling. A lock error at accept means the end customer already has another open offramp conversion. Have the UX deal with the existing conversion, not create a new one. See Per-user, per-type lock.
- Edge reconciliation. On incident, read
GET /v1/conversions?status=...to reconstruct the state of what is open.
Next steps
- Conversion and states: the full state machine map.
- Fees model: how the quoted rate is built.
- Idempotency: the semantics of the
Idempotency-Keyheader.
