Buy walkthrough
On a BUY, the merchant’s USDT is locked in escrow and the customer pays fiat outside Sinbad. The customer’s wallet signs one transaction to create the escrow, and later one to mark the trade paid. This walkthrough assumes an ordinary customer wallet: it needs POL for network fees, but it needs no USDT and no token approval. See Wallets and gas for signing and fees.
Before you start
- A customer session for the customer. See Create a session.
- A saved payment account for the payment method the customer will use. See Saved payment accounts.
- A customer wallet with enough POL to pay network fees for two or three transactions.
1. Ask for an offer
Call POST /v1/offers/best with side set to BUY.
The two amount modes mean different things:
- With
FIAT,valueis the market value of the USDT in the selected currency, before merchant margin and fees. Sinbad converts it to USDT at the market rate, then calculates the final fiat payment, which can differ from the amount entered. The offer returns the resulting USDT quantity inbreakdownInput.amount. - With
USDT,valueis an exact token quantity.
The response holds one instant offer or, if nothing matches exactly, up to three alternatives. Each offer carries its pricing, display data and a breakdownInput.
An amount outside every listing’s limits is not an error. The call returns 200 with empty results, so show a “no offer” state and let the customer change the amount. If the only match is a listing that uses the customer’s own wallet, the call fails with 400 VALIDATION_ERROR and details.reason: "SELF_TRADE_NOT_ALLOWED".
Offer and breakdown requests share a rate limit per customer. See Limits and limitations.
2. Create the breakdown
Call POST /v1/trade-breakdowns with the chosen offer’s breakdownInput, exactly as received, and an Idempotency-Key header. Send every field unchanged, including paymentMethodId and savedPaymentMethodId. A changed selection can return 400 or 409, and a selection that no longer exists can return 404. In either case, ask for a fresh offer.
Sinbad responds with 201:
breakdownIdandstatus: "AWAITING_FUNDING".expiresAt, 300 seconds (5 minutes) after creation.tradeDetails, with the final numbers.escrowTransaction, the prepared transaction for the customer’s wallet to sign.
Show tradeDetails to the customer and continue only if they agree. Repeating the call with the same key and body returns the same breakdown and the same deadline.
If the merchant can no longer cover the amount, the call fails with 409 INSUFFICIENT_LIQUIDITY. Ask for a new offer.
Creating a breakdown also holds the customer’s open trade on the BUY side. To price again, create a new breakdown with a new Idempotency-Key. See One open trade per side for when that replaces the earlier breakdown and when it fails.
3. Sign and submit the escrow transaction
Have the customer’s wallet sign escrowTransaction, then call POST /v1/transactions with its preparationId and an Idempotency-Key. The customer has until expiresAt to do this.
If the wallet has too little POL, the call fails with 400 INSUFFICIENT_BALANCE and details.kind: "NATIVE_GAS". A 202 response means Sinbad started tracking the transaction. It does not mean the trade exists yet. For nonces, gas, retries and unknown outcomes, see Wallets and gas.
4. Wait for the trade
The trade exists only after the escrow is confirmed on chain. Either:
- Poll
GET /v1/transactions/{preparationId}untiltransaction.statusisCONFIRMED, or - Poll
GET /v1/trade-breakdowns/{breakdownId}untilstatusisCONFIRMEDandtradeIdis set.
The breakdown’s other statuses are AWAITING_FUNDING, EXPIRED and REVIEW_REQUIRED. You can also use the trade.created and trade.updated webhooks. The new trade starts in CREATED.
Trade status can lag the chain, so keep polling until confirmed and reconcile with GET /v1/trades/{tradeId}. Keep polling the breakdown even if you never saw a transaction hash, because it also tells you when the breakdown has expired.
5. The customer pays the fiat
Read the trade with GET /v1/trades/{tradeId}. While the trade is CREATED, paymentInstructions holds the merchant’s receiving account: paymentMethodId, displayLabel and a data object. It appears only after funding is confirmed.
The customer sends exactly pricing.customerFiatAmount, in the trade’s fiat currency, from the saved account they chose. This payment happens outside Sinbad. The merchant may check that the sender’s name matches the customer’s. A mismatch is a dispute reason (name_does_not_match_payment).
Sinbad cancels an unpaid BUY trade automatically after a configured window. Ask Sinbad for the window in your environment, and do not assume the cancellation happens at an exact time.
6. Upload proof of payment
Upload a receipt image so the merchant can find the payment quickly. Call POST /v1/trades/{tradeId}/proof with contentType (image/png or image/jpeg) and the image as dataBase64, without a data: prefix. Proof does not mark the trade as paid. Limits and the other proof calls are on Payment proof.
7. Mark the trade as paid
After the customer has actually sent the money, mark the trade paid:
- Call
POST /v1/trades/{tradeId}/actions/{action}/executionwith themarkPaidaction. The call takes no body and noIdempotency-Key, and every call returns a new prepared transaction. - Have the wallet sign the prepared transaction.
- Submit it with
POST /v1/transactions, which does need anIdempotency-Key.
When the transaction is confirmed, the trade becomes PAID. Trade actions have no signing deadline, but the trade must still be CREATED. See markPaid and Trade actions.
8. The merchant releases
The merchant checks the payment and releases the USDT. The customer signs nothing for this step. The trade becomes RELEASED, the customer’s wallet receives pricing.recipientAmountUsdt, and the customer’s BUY side is free for a new trade.
What can go wrong
Next steps
- Disputes for what to do when payment and release do not match.
- Trades and history to list a customer’s trades.
- Go-live checklist before you send real customers to production.