Skip to main content
Version: v2

Building a Payment UI

Patterns for building the screen a user sees after an order is created: deposit instructions, countdowns, status tracking, and refunds. Based on the pitfalls integrators hit most often.

The pending-order route

A common pattern is a shareable/reloadable route keyed only by unique_reference (e.g. /order/ABCDEF123456) that renders from GET /orders/{unique_reference}/.

This works, with one important caveat:

Persist the order mode yourself

Mode flags are not reliably reconstructible from a detail read alone — DeFi detail responses may omit is_defi, and CeFi fixed/floating intent is not echoed back as a flag. If your payment UI branches on mode (fixed-rate countdown, DeFi explorer links, recreate flow), store the mode alongside the reference at creation time instead of inferring it from the GET response.

What the detail response does reliably give you: status, amounts, addresses and extra IDs, payment_window_minutes, fixed_rate_deadline, rate, transaction hashes, and (CeFi only) amount_usd.

Deposit instructions

Render from the create/detail response:

  • deposit_address — the address the user must pay. Always show copy-to-clipboard and a QR code.
  • deposit_address_extra_id — when non-empty, the memo/tag is mandatory; display it as prominently as the address (funds sent without it can be delayed or lost).
  • deposit_amount + deposit_currency — the exact amount to send. For DeFi floating orders amounts may be null — show "send any amount within limits" UX based on the pair's limits from GET /rate/?is_defi=true.

Countdown: which deadline to show

Two fields describe the payment deadline:

FieldTypeUse
fixed_rate_deadlineUTC timestamp or nullWhen present, this is the authoritative moment the quoted rate stops being honored — count down to it
payment_window_minutesintegerFallback window from created_on when no fixed deadline exists

Recommended logic:

  1. If fixed_rate_deadline is set → countdown to it (it equals created_on + payment_window_minutes).
  2. Else (fixed_rate_deadline: null) → the order is floating-rate: there is no rate deadline. Show "rate determined when your deposit arrives" instead of a countdown. CeFi floating orders carry payment_window_minutes: 0; DeFi floating orders may carry a small nominal window (e.g. 1) — ignore it and key off the null fixed_rate_deadline.

The deadline applies to the user's deposit, not to Nexchange's processing time afterwards.

When the countdown hits zero

An expired unpaid order will move to CANCELED (see Order Lifecycle). Offer a recreate flow: fetch a fresh quote for the same pair/amount (refresh-on-submit), create a new order, and route the user to the new reference. Never let the user pay against an expired order's address.

Status tracking

Poll GET /orders/{unique_reference}/ while the order is in a non-terminal status — cadence and stop conditions in Order Tracking, or subscribe to Webhooks.

Map statuses to user-facing stages rather than exposing raw values — a suggested grouping:

UI stageStatuses
Awaiting paymentINITIAL
Payment detectedUNCONFIRMED PAYMENT
ProcessingPAID, PRE-RELEASE, PENDING KYC
Sending to youRELEASED
DoneCOMPLETED
ExpiredCANCELED
Refund in progressINITIATED REFUND, REFUND FAILED
RefundedREFUNDED

Show withdraw_transaction with an explorer link once populated (RELEASED onward).

"I have paid" and cancellation

  • Optional accelerator: PATCH {"marked_as_paid": true, "deposit_transaction": "<hash>"} after the user confirms sending — speeds up crediting but is never required (chain monitoring detects deposits automatically).
  • Cancel an unpaid order with PATCH {"marked_as_paid": false}.
  • Both are described in Update Order.

Refund UX

  • Collect refund_address at creation whenever possible (mandatory for DeFi orders whose destination chain is non-EVM — Solana, Tron, Bitcoin). It must be an address on the deposit currency's chain — refunds return where funds came from. For chains that need a memo/tag, also collect refund_address_extra_id — refund transactions carry it.
  • REFUNDED is a terminal outcome of a failed swap, not a variant of success: the user got their deposit back instead of the swapped funds. Present it that way ("swap could not be completed — your funds were returned"), with the refund destination shown.
  • REFUND FAILED is transient — refunds are retried (INITIATED REFUND → REFUND FAILED → INITIATED REFUND → REFUNDED). Keep the user informed rather than surfacing it as a dead end.

Safe retries

Retry semantics differ by mode (201 vs 200):

  • DeFi: creation is idempotent — re-sending an identical request returns 200 with the existing order. On a client-side timeout, just retry.
  • CeFi: every create mints a new order. On a timeout, first check whether the order already exists (list recent orders for the pair with status=INITIAL) before re-creating — otherwise a reload can leave the user with two orders and two deposit addresses.

Next steps