Skip to navigation

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.

FieldValue
tokenSymbolUSDT
fiatCurrencyIdThe fiat currency the customer pays in.
amount.modeFIAT or USDT.
amount.valueA decimal string.
paymentMethodIdRequired. The catalog payment method.
savedPaymentMethodIdOptional. The customer’s saved account.

The two amount modes mean different things:

  • With FIAT, value is 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 in breakdownInput.amount.
  • With USDT, value is 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:

  • breakdownId and status: "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.

POST /v1/transactions
Idempotency-Key: tx-<preparationId>
Content-Type: application/json
{ "preparationId": "<escrowTransaction.preparationId>", "signedTransaction": "0x02..." }

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:

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:

  1. Call POST /v1/trades/{tradeId}/actions/{action}/execution with the markPaid action. The call takes no body and no Idempotency-Key, and every call returns a new prepared transaction.
  2. Have the wallet sign the prepared transaction.
  3. Submit it with POST /v1/transactions, which does need an Idempotency-Key.
POST /v1/trades/{tradeId}/actions/markPaid/execution
-> { "transaction": { "preparationId": "...", "purpose": "markPaid", ... } }
POST /v1/transactions
Idempotency-Key: tx-<preparationId>
{ "preparationId": "<transaction.preparationId>", "signedTransaction": "0x02..." }

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

SituationWhat to do
The customer has not paid yet and changes their mindWhile the trade is CREATED, prepare cancelPrePaid, sign it and submit it. The trade ends CANCELED and the locked USDT returns to the merchant.
The customer paid but the merchant has not releasedAfter the dispute window has passed, the customer can open a dispute. See Disputes.
You are unsure whether an action is allowedavailableActions on the trade is only a hint. Preparing an action checks the current chain state and timing.
The customer needs to talk to the merchantUse Chat and images. It works throughout the trade.

Next steps