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' },
});
payment = 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
| Field | Notes |
|---|---|
amount | Required. Decimal string in the presentment currency. |
currency | Required. Fiat code or asset symbol. |
merchant_order_id | Your reference. Unique per account, and how you search later. |
return_url | Must be on a domain you have verified. A hint, not proof of payment. |
expires_in | Seconds. Shorter means less rate exposure; too short strands slow buyers. |
fee_payer | merchant or buyer. Grossing up is done for you. |
gas_payer | buyer (default) or merchant. Who carries the network fee — added to their total, or taken from your settlement. |
capture_method | automatic settles at once. manual holds the money until you charge it. |
hold_window_hours | For a held payment: how long before the buyer may reclaim. Defaults to your account setting. |
settlement | Override the destination or asset for this payment. |
metadata | Yours. 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:
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
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.