Payments
The internal transfer model, the fee that separates amount from net_amount, and what a receipt number is for
A payment is what a completed session leaves behind: an immutable record of value that moved from one Coinland account to another. It is the authoritative object in this API, and the thing every webhook tells you to come and read.
The internal transfer model
Both sides of a Coinland Pay payment are Coinland accounts -- the customer's, and yours. So the payment is a debit and a credit in Coinland's own ledger, posted together:
customer balance ── amount ──▶ Coinland fee (fee_amount)
──▶ your balance (net_amount)Nothing touches a blockchain. That has four consequences worth designing around:
- It settles in one hop. There is no pending, no confirmation count, no mempool. By the time the
widget tells the customer it worked, the money is in your balance and
GET /payments/{id}will answer. - There is no network fee, at any size. A 4 USDT payment costs the same to move as a 4,000 USDT one, which makes small payments viable in a way an on-chain rail does not.
- It cannot be reversed. No chargebacks, and no reverse call in this API. A refund is a transfer you send back, at your discretion, from your own balance.
- Only completed payments exist here. There is no "pending payment" object to poll. A session that
has not been paid simply has
payment: null, and the transfer either happens atomically or does not happen at all.
Payments credit a dedicated business wallet, kept separate from your personal spot and trading balance so a business's takings do not mix with personal funds. It is the wallet to look in when you are checking whether a payment arrived.
Moving your takings out
Your business wallet holds what you have been paid; it is not itself a payout surface. Getting money out is two steps, and the second one is whatever you already do:
Transfer business to main, in the business console. This is a move between two of our own ledgers, so it is instant and free — no network fee, no venue, no waiting.
Trade or withdraw from your main wallet, exactly as before. Nothing about that flow changed.
There is no withdraw-from-business path, deliberately
You cannot withdraw straight out of the business wallet, and that absence is the design rather than a gap: it keeps one withdrawal flow, the one you already know, instead of adding a second payout surface with its own rules. Transfer first, then withdraw.
Amount, fee and net
`amount` is the ORDER VALUE, not what the payer was debited
If you already reconcile on amount, read this. amount is the figure you priced. What the
customer was actually debited is charged_amount, and under fee_bearer: "customer" it is larger.
For your own books net_amount was always the right field and still is; for "what did my customer
pay", switch to charged_amount.
Four amounts and the flag that ties them together. Confusing them is the most common reconciliation bug on any payment rail:
Prop
Type
Who bears the fee
You choose, per business, whether you absorb Coinland's fee or add it to the customer's total. One identity holds in both configurations, and it is the whole accounting model:
charged_amount - net_amount == fee_amountfee_bearer | The customer is debited | You receive |
|---|---|---|
merchant | charged_amount == amount | net_amount == amount - fee_amount |
customer | charged_amount == amount + fee_amount | net_amount == amount |
So reconcile your order totals against amount, your books against net_amount, and any
"what did the customer pay" question against charged_amount. Using one field for all three is
what makes a ledger drift.
Where your fee rate comes from
Your rate is not a fixed number and you cannot set it. It comes from a tier ladder that moves automatically on your rolling 30-day volume and payment count — trade more and you move down the ladder by yourself. Your current rung and your progress toward the next one are shown in the business console.
Your rate is fixed when the session is created
The effective rate and the fee bearer are frozen onto a checkout session at creation, and the confirm settles on that snapshot. The consequence worth designing around:
A rate change takes effect on your next session, never on one that is already open.
That matters because the ladder moves on its own, on a nightly sweep, while sessions can live up to 24 hours. Without the freeze, the total shown in the widget and the amount eventually debited could disagree. With it, a customer looking at an open checkout pays the terms they were quoted.
It is the same guarantee the coin quotes on a USD-priced session already carry,
over the same window — the session's own expires_at. One deadline, not two.
Because the terms are already fixed by the time money moves, a rate change never rewrites history
either: an old payment's fee_amount is settled forever, and a report you ran last month gives the
same answer today.
All four amounts are decimal strings, and they should stay strings all the way into your database. Parse them with a decimal type, never a float -- see going live.
Holding only stablecoin
You can opt in, per business, to have every coin you receive automatically sold into USDT, so your balance stays in one asset instead of accumulating whatever your customers paid with. It is off by default; you turn it on in the business console. USDT is currently the only permitted target — the list is operator-controlled, not something you pick from.
What happens after a payment settles:
- Coinland moves that payment's
net_amountfrom your business wallet to your spot wallet, places an ordinary spot market sell there on your behalf, and moves the proceeds back to the business wallet. - It runs on a periodic sweep shortly after settlement, and the sell settles asynchronously — so there is an in-progress state before the USDT lands back. It is not instant, and not on a guaranteed interval.
A conversion is a trade, not a transfer
It executes at the live market price and pays the normal spot trading commission. It is not free, and it is not rate-locked: the quotes on a USD-priced session and your fee terms are both fixed when the session is created, but this sell happens afterwards, at whatever the market is then. Nothing about the conversion is quoted in advance.
Three cases are left alone rather than converted, and in each the coin simply stays in your balance:
| Case | What happens |
|---|---|
| The payment already arrived in your target coin | Nothing to sell |
| The amount is below the market's minimum | Left unconverted as dust, immediately -- waiting cannot help |
| The market is unavailable, or the sell fails | Retried every 15 minutes, up to 5 attempts, then given up on permanently -- and the coin is returned to your business wallet |
A conversion is layered on top of a settled payment, never a condition of one
This is the property to take away. The payment is final the moment it settles, and a payment's
correctness never depends on a market being open. A conversion that is skipped, fails, or never
runs leaves the payment completed and the money yours — you simply hold the original coin. There
is no reversal and no retro-charge.
Nothing notifies you either way: there is no email and no webhook for a conversion. The outcome and
its reason (already-stable, below-min, market-unavailable, target-invalid,
attempts-exhausted) are recorded in the business console, which is where to look if a coin you
expected to be converted is still sitting in your balance.
The dust threshold is the trading pair's own market minimum and is not published on any
merchant-facing surface — you cannot compute in advance whether a given payment will convert. What
you get is the outcome, recorded as below-min.
Because the sell is an ordinary order in your own account, it appears in your normal trading history
alongside anything else you trade, which is where its commission is reconcilable. Each conversion
record also carries the order_id of the trade it became, so a specific payment can be tied to a
specific trade. Note that this fee is the trading commission and is entirely separate from
fee_amount, which is Coinland's fee on the payment itself.
The setting is configured and read in the business console. It is deliberately not part of the
merchant API: nothing in GET /me or on a payment object reports it, because a conversion is
something that happens to your balance afterwards rather than a property of the payment.
Receipt numbers
Every payment gets a human-friendly receipt number alongside its UUID:
CLP-8F3K2M9QIt is designed to be said out loud and typed by hand: it is what a customer will quote in a support email, and what your support agent will paste into a search box. Unlike the UUID, it is short enough to survive that trip.
It is also a lookup id. GET /api/pay/v1/payments/{id} takes either:
# By UUID
curl .../api/pay/v1/payments/b92e4d17-6c38-4a05-9f2b-1e7d3c8a5049 \
-H "Authorization: Bearer $COINLAND_PAY_KEY"
# By receipt number -- same record
curl .../api/pay/v1/payments/CLP-8F3K2M9Q \
-H "Authorization: Bearer $COINLAND_PAY_KEY"A receipt number is not a secret and not a credential. Knowing one proves nothing on its own, which is what the signed receipt token is for.
Listing payments
GET /api/pay/v1/payments returns your payments newest first, cursor-paginated:
curl "https://my.coinlandexchange.com/api/pay/v1/payments?limit=100¤cy=usdt&from=2026-08-01T00:00:00Z" \
-H "Authorization: Bearer $COINLAND_PAY_KEY"{
"data": [
{
"id": "b92e4d17-6c38-4a05-9f2b-1e7d3c8a5049",
"receipt_no": "CLP-8F3K2M9Q",
"session_id": "3a7f21e8-9c04-4d6b-8e15-7b2a9f3c1d60",
"reference_id": "order-10492",
"status": "completed",
"currency": "usdt",
"amount": "24.90",
"charged_amount": "24.90",
"fee_amount": "0.12",
"fee_bearer": "merchant",
"net_amount": "24.78",
"metadata": { "cart_id": "c_88213" },
"receipt": "v1.eyJwYXltZW50X2lkIjoi...",
"paid_at": "2026-08-11T12:04:31Z"
}
],
"next_cursor": "eyJwYWlkX2F0IjoiMjAyNi0wOC0xMVQxMjowNDozMVoifQ"
}Page by following next_cursor until it comes back null. Do not count pages or assume a page size:
pass the cursor you were given, and stop when there is not one.
from and to bound paid_at, and currency filters to one coin. Together they are how you build a
daily settlement report:
const API = "https://my.coinlandexchange.com";
export async function* allPayments(params: Record<string, string>) {
let cursor: string | null = null;
do {
const query = new URLSearchParams({ ...params, limit: "100" });
if (cursor) query.set("cursor", cursor);
const page = await fetch(`${API}/api/pay/v1/payments?${query}`, {
headers: { Authorization: `Bearer ${process.env.COINLAND_PAY_KEY}` },
}).then((r) => r.json());
yield* page.data;
cursor = page.next_cursor; // stop when it comes back null
} while (cursor);
}Reconciling
Payments carry your reference_id, so reconciliation does not need any id you did not choose:
- Read a day's payments with
from/to. - Match each
reference_idto an order in your system. - Assert
amountequals what you charged for that order in that coin. - Sum
net_amountper coin, and compare against the credits on your business wallet.
An order with no payment was never paid. A payment with no order is the one to investigate, and it
almost always means a reference_id was reused or generated somewhere you did not expect.