UnifiWallet Payments

Payments

An intent to be paid, with the fee, rate and destination frozen at the moment it is created.

A payment is an intent to be paid a specific amount, with terms fixed at the moment it is created. Everything that decides what settles — the fee, the rate, the destination — is written onto it and never recalculated.

By default a payment settles the moment the buyer pays. If you ship goods, you can hold it instead and charge when it despatches — cancelling a hold costs nothing and owes nobody a refund.

Creating one

The expandable reference for this call — URL, every field, the response, errors, and samples in Node.js, TypeScript, Python, Go, PHP and curl — is POST /v1/payments.

const payment = await call('POST', '/v1/payments', {
  amount: '129.00',
  currency: 'USD',
  merchant_order_id: 'order_2208',
  description: 'Annual licence',

  line_items: [
    { name: 'Annual licence', quantity: 1, unit_amount: '129.00' },
  ],

  customer: {
    reference: 'cus_88213',
    email: 'buyer@example.com',
    address: { country: 'DE' },
  },

  tax_behavior: 'exclusive',
  tax_rate_percent: 19,

  return_url: 'https://shop.example.com/thanks',
  cancel_url: 'https://shop.example.com/basket',
  expires_in: 1800,

  metadata: { campaign: 'spring' },
});

Amounts are strings

Every monetary value in this API is a decimal string, and none of them is ever a float. 0.1 + 0.2 is not 0.3 in any language with IEEE floats, and on an 18-decimal token that error is not academic.

On chain, amounts are integer base units. A token's decimals are per chain, deliberately: USDT is 6 decimals on Ethereum and 18 on BNB Chain, and assuming otherwise mis-prices a payment by a factor of a trillion.

Fields worth knowing

FieldNotes
amountRequired. Decimal string in the presentment currency.
currencyRequired. Fiat code or asset symbol.
merchant_order_idYour reference. Unique per account, and how you search later.
return_urlMust be on a domain you have verified. A hint, not proof of payment.
expires_inSeconds. Shorter means less rate exposure; too short strands slow buyers.
fee_payermerchant or buyer. Grossing up is done for you.
gas_payerbuyer (default) or merchant. Who carries the network fee — added to their total, or taken from your settlement.
capture_methodautomatic settles at once. manual holds the money until you charge it.
hold_window_hoursFor a held payment: how long before the buyer may reclaim. Defaults to your account setting.
settlementOverride the destination or asset for this payment.
metadataYours. Returned on the payment and every webhook about it.

Return URLs are checked

return_url, cancel_url and failure_url must be on a domain you have verified with a DNS record. Without that check a stolen key could send your buyers to a site the attacker controls, at the moment they are least suspicious — they have just paid and expect a confirmation page.

The snapshot

Read it once and you will understand the safety model. Every field that decides where money goes is frozen here:

"snapshot": {
  "fee_plan_version": "plan_std_v3",
  "fee_bps": 100,
  "rate_mode": "fixed_rate",
  "settlement": {
    "path": "direct",
    "destination_id": "dst_9k2Qm",
    "resolved_address": "0x8f2a4c...",
    "asset": "USDC",
    "chain_id": 8453
  },
  "confirmations_required": 20,
  "gas_base_units": "4200",
  "gas_payer": "buyer",
  "tax": { "behavior": "exclusive", "total": "24.51", "jurisdiction": "DE" },
  "totals": { "subtotal": "129.00", "tax": "24.51", "total": "153.51" }
}

Changing your settlement address, your fee plan or your accepted assets has no effect on a payment that already exists. That is not tidiness — it is what stops a compromised account redirecting or repricing money already on its way.

Under and over

Neither can happen when the buyer pays from a wallet: the router reverts below the required amount and returns anything above it inside the same transaction.

A deposit address is different, because a plain transfer cannot be told to revert. Short lands as underpaid with the shortfall recorded, and the buyer can send the rest to the same address until it expires — the sum is what counts, not any single transfer. It must arrive in full: the deposit address enforces the exact amount on chain, so there is no tolerance to be had there. Over is forwarded to you whole, and payment.overpaid names the excess for you to return.

underpayment_tolerance_bps on your account is about rate movement, not shortfall: it is how far the market may drift between the quote and the transaction before a fixed-rate payment is repriced.

Cancelling and expiring

A payment nobody has paid can be cancelled. One that has money on chain cannot — refund it instead. Payments expire on their own at expires_at, and you can expire one early if the buyer abandons the basket.

Neither reaches into the buyer's wallet. An order they signed before the deadline can still be mined, and a transfer to a deposit address can still land. When that happens you get payment.late_settled rather than silence: the money is at your address, and it is yours to fulfil or refund.

Reading them back

GET /v1/payments?status=completed&limit=50
GET /v1/payments?merchant_order_id=order_2208
GET /v1/payments/pay_3nQ8xK2mLp4R
GET /v1/payments/pay_3nQ8xK2mLp4R/events

The last one is the status history with reasons — the fastest answer to “why is this payment stuck”, and better than inferring it from the current state.