BlendFi

Transaction lifecycle

From quote to settlement — every state a BlendFi transaction can be in, the legal transitions between them, and how to react to each.

Core flow

Transaction lifecycle

A BlendFi transaction moves money in three steps: your end user pays BRL via Pix, BlendFi buys the equivalent USDT on a regulated exchange, and the USDT is settled on-chain to the wallet you specified. This guide shows every state the transaction passes through, when money is irreversible, and what to do at each stop.

The happy path

quote ──► transaction created ──► pending_payment

                                        ▼  (Pix received)
                                  payment_received

                                        ▼  (exchange filled)
                                   buying_crypto

                                        ▼  (on-chain submit)
                                   sending_crypto

                                        ▼  (mined)
                                    completed

Each arrow is a deterministic event. There is no polling required from your side — webhook callbacks announce every transition. In sandbox, you drive these events yourself with the test helpers.

Three concepts

Quotes are short-lived

A quote pins a BRL→USDT rate for ~30 seconds. You convert it into a transaction with `POST /v1/transactions`. Once consumed, it's done.

Transactions are irreversible after `buying_crypto`

Up to `payment_received`, we can refund. Once we've bought crypto, we settle it — refunds become operational, not automatic.

Failures are explicit

Every terminal failure has a `code` telling you why. There is no ambiguous `unknown` state.

State catalogue

StateMeaningMoney stateNext legal states
pending_paymentTransaction created; awaiting Pix from the end userNo money movedpayment_received, cancelled, expired, failed
payment_receivedPix arrived in our settlement accountBRL with us, no USDT yetbuying_crypto, failed
buying_cryptoWe're executing the BRL→USDT order on the exchangeBRL committed, USDT in flightsending_crypto, failed, refunded
sending_cryptoUSDT bought; on-chain transfer submitted to the destination walletUSDT in flightcompleted, failed
completedOn-chain transfer confirmedUSDT delivered(terminal)
expiredThe end user did not pay within the Pix TTLNo money moved(terminal)
cancelledYou cancelled before payment arrivedNo money moved(terminal)
refundedWe refunded BRL to the end user (after partial progress)BRL returned(terminal)
failedSomething broke at one of the upstream providersSee failure_reason(terminal)

Money safety boundaries

Where the no-return point lives

Up to and including payment_received, BlendFi can refund the end user automatically. Once the transaction enters buying_crypto, the BRL has been committed to the exchange — refunds are still possible (state refunded) but they are operational, not automatic, and require your support team to coordinate with us. After sending_crypto the USDT is on-chain; "refund" means returning USDT to a sender wallet, which is a separate transaction your end user has to execute.

Walking the happy path

1. Get a quote

Quotes pin the rate for a short window. Default TTL is 30 seconds.

curl -X POST $BLENDFI_BASE/v1/quotes \
  -H "Authorization: Bearer $BLENDFI_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{
    "user_id": "01J...",
    "source_amount": "100.00",
    "source_currency": "BRL",
    "target_currency": "USDT",
    "destination_wallet_address": "0xabc...",
    "destination_wallet_network": "polygon"
  }'

Response:

{
  "id": "01JQ...",
  "rate": "5.42",
  "source_amount": "100.00",
  "target_amount": "18.4502",
  "expires_at": "2026-04-29T13:00:30Z"
}

If you wait too long, the quote returns 409 quote_expired when you try to consume it. Get a fresh one.

2. Create the transaction

Convert the quote into a transaction. After this call, the transaction is in pending_payment and we're waiting for the end user's Pix.

curl -X POST $BLENDFI_BASE/v1/transactions \
  -H "Authorization: Bearer $BLENDFI_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "content-type: application/json" \
  -d '{"quote_id": "01JQ..."}'

Response:

{
  "id": "01JT...",
  "status": "pending_payment",
  "pix_qr_code": "00020126...",
  "pix_copy_paste": "...",
  "expires_at": "2026-04-29T13:30:00Z"
}

Show the Pix QR code or copy-paste string to your end user. They have until expires_at (typically 30 minutes) to pay.

3. End user pays via Pix

When the Pix arrives, BlendFi transitions the transaction to payment_received. You hear about this in two ways:

  • Webhook (recommended): we POST to your registered webhook URL with the new state.
  • Polling (fallback): GET /v1/transactions/{id} shows the current state.
{
  "id": "01JT...",
  "status": "payment_received",
  "paid_at": "2026-04-29T13:04:12Z",
  ...
}

4. BlendFi buys USDT and settles on-chain

This is automatic. The transaction moves through buying_crypto (exchange order) → sending_crypto (on-chain submit) → completed (mined). Each transition is delivered as a webhook.

The terminal completed payload includes the on-chain transaction hash you can show your end user:

{
  "id": "01JT...",
  "status": "completed",
  "usdt_tx_hash": "0xabc123...",
  "completed_at": "2026-04-29T13:08:05Z"
}

Failure modes

Every terminal failure is announced with a failure_reason. The four you'll actually encounter:

failure_reasonAt which stateWhat happenedYour action
payment_timeoutpending_paymentexpiredEnd user did not pay before the Pix expirationTell the user to retry with a fresh quote
exchange_rejectedbuying_cryptofailedThe exchange could not fill the BRL→USDT order at the locked rateRefund initiated automatically; we contact you
chain_revertsending_cryptofailedThe on-chain transaction reverted (gas, RPC, network issue)Operational refund — file a ticket with the request_id
kyc_blockedpending_paymentfailedThe end user's KYC status changed mid-flow (revoked, expired)Handle in your UI; user must re-verify

If you ever see a failed state without a failure_reason, treat it as internal_error — file a ticket with the transaction ID.

Driving the state machine in sandbox

Sandbox doesn't actually accept Pix payments or talk to a real exchange. Instead, it gives you test_helpers endpoints that map 1:1 to real state transitions, so your integration sees the same event sequence it will see in production.

TXID=01JT...
H='-H "Authorization: Bearer $BLENDFI_KEY"'
I='-H "Idempotency-Key: $(uuidgen)"'

# Drive the happy path
curl -X POST $BLENDFI_BASE/v1/test_helpers/transactions/$TXID/pay $H $I
curl -X POST $BLENDFI_BASE/v1/test_helpers/transactions/$TXID/fill-hedge $H $I
curl -X POST $BLENDFI_BASE/v1/test_helpers/transactions/$TXID/send-crypto $H $I
curl -X POST $BLENDFI_BASE/v1/test_helpers/transactions/$TXID/confirm-crypto $H $I

Failure variants you can simulate:

EndpointEffect
POST /v1/test_helpers/transactions/{id}/expireSkips the Pix wait → expired
POST /v1/test_helpers/transactions/{id}/fail-hedgeSimulates exchange_rejectedfailed
POST /v1/test_helpers/transactions/{id}/fail-cryptoSimulates chain_revertfailed

Use these to validate your retry, error display, and refund handling before you touch production.

Webhooks vs polling

For partner-grade reliability, register a webhook URL. We POST every state transition to it with the full transaction body and a signed header you can verify.

If a webhook is unreachable, BlendFi retries with exponential backoff (1m → 1h → 6h → 24h). You can also call GET /v1/transactions/{id} at any time to read the canonical state — there is no rate-limit penalty for "is it done yet?" polling within reasonable bounds.

Polling cadence

Polling once per second per transaction is fine. Polling every 100ms across thousands of transactions is not — you'll trip rate_limit_exceeded. If you find yourself wanting tighter polling, that's a webhook signal.

FAQ

How long do quotes last? Default TTL is 30 seconds. The exact expires_at is in the quote response — don't hard-code 30s, read the field.

What if the end user pays late, after the Pix expiration? The transaction is already expired. The Pix bank reverses the late payment. We do not auto-create a new transaction.

Can I cancel a pending_payment transaction before the user pays? Yes. POST /v1/transactions/{id}/cancel (with an idempotency key) moves it to cancelled. After the Pix arrives, cancellation requires a refund flow.

What happens if the on-chain network is congested and the transfer takes hours? The state stays sending_crypto. We retry with bumped gas if the original transaction is stuck. We don't time out into failed from chain congestion alone — only from explicit revert or RPC error.

Are partial fills possible on the exchange? No. We only accept all-or-nothing fills. A partial fill is treated as a rejection and the transaction moves to failed with failure_reason: exchange_rejected.

Why is there a separate payment_received and buying_crypto instead of going straight from one to the other? The two events have different latency profiles. Pix usually settles in under 10 seconds; the exchange order can take up to a minute on busy days. Exposing them separately lets your UI show "received your payment, now converting…" — partners say their end users prefer this over a single ambiguous spinner.

On this page