BlendFi

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-Signature header. 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 to standby.
  • Show expected_source_amount prominently. End customers who tweak the amount on the fly cause standby.
  • Offer a "cancel" button that calls POST /v1/conversions/:id/cancel while the conversion is in awaiting_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 includes standby_reason, received_amount, expected_source_amount. See Step 5b.
  • conversion.failed: irrecoverable error after funded or liquidated. Payload includes failure_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_id as 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_reasonUsual decision
under_fundedLiquidate 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_fundedLiquidate at the received amount (end customer gets extra BRL) or contact support.
window_expiredLiquidate (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-Key on every mutating POST. 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

On this page