One-time payments with a link
Somebody owes you a specific amount, once. An invoice, a deposit on a job, a bill agreed over a phone call, a booking. You want to send them a URL, have them pay, and be told when it is done.
There is no page to build and no integration to write. This is the fastest thing in the product to get working and it is worth knowing exactly where its edges are.
Is a link the right shape?
| Payment link | Payment button | |
|---|---|---|
| Where the payer is | On a page we host, at a URL you sent | On your own checkout |
| Amount | Fixed on the link, when you create it | From your cart, per purchase |
| Payers | One | One |
| Address | One, belonging to the link | One per checkout session |
| Closes when | Paid, or a date passes | Paid, or the countdown expires |
| You write | Nothing | A few lines of JavaScript |
A link is not a factory that mints a session per visitor. It owns an address and a running total. That is why it has no countdown — a countdown is a per-visitor timer and a link has no visitors, only payers.
If you are billing programmatically, at volume, from a system that already knows the total, use the payment button instead. Links are for the cases where a human decided the amount.
1. Create it
curl -X POST $CHAINOS/v1/payments/links \
-H "X-API-Key: $CHAINOS_PAYMENT_SECRET" \
-H "Content-Type: application/json" \
-d '{
"mode": "fixed",
"chain": "base",
"asset": "USDC",
"amount": "1250.00",
"title": "Invoice 4418",
"description": "Consulting, August 2026",
"collectEmail": true,
"successUrl": "https://your-company.example/paid",
"metadata": { "invoiceId": "INV-4418", "customerId": "cust_88213" }
}'
{
"id": "0b1f…",
"slug": "k3Xq7RfN2p",
"url": "https://pay.your-company.example/pay/k3Xq7RfN2p",
"mode": "fixed",
"amountRaised": "0",
"status": "active",
"address": null
}
Send url. That is the whole integration.
fixed linkA guessable slug on an invoice publishes what one customer was charged. Anyone who can guess
/pay/invoice-4418 learns your client's rate. Custom slugs are honoured on donation links
only, where the page is meant to be found.
The fields that decide how it behaves
| Field | Why it matters |
|---|---|
chain + asset | Fixed at creation. They are what the address was derived for and cannot be changed |
amount | A string, in the asset's own units: "1250.00". More decimal places than the asset carries is refused, not rounded |
collectEmail | Turn it on. Without an address there is nobody to send the receipt to and nobody to contact if they underpay |
metadata | Your own identifiers, echoed on every webhook. Put your invoice id here |
closesAt | Optional on a fixed link. Useful for a quote that expires |
successUrl | Where the payer lands afterwards. Yours; nothing invents one |
amount is a decimal string and never a JSON number. JSON has one numeric type and it is
a double, and this figure decides whether an invoice is settled.
2. Choose the chain deliberately
The payer pays on the chain you picked. They cannot change it, and they will not necessarily tell you they could not.
curl -s $CHAINOS/v1/payments/readiness -H "X-API-Key: $CHAINOS_PAYMENT_SECRET"
Checkout runs on seven chains — Bitcoin, Ethereum, BNB Smart Chain, Polygon, Avalanche, Base and TRON. Solana, Stellar and XRP are refused at creation with the reason stated.
For an invoice the practical shortlist is:
| Base or Polygon, USDC | Cheap to receive, cheap to sweep, native issuance |
| TRON, USDT | What a great many counterparties already hold |
| BSC, USDT | Cheap, widely held. Note USDT is 18 decimals here, not 6 |
| Ethereum | Only for large amounts. A small invoice costs more in gas to collect than it is worth |
A session below the chain's economic minimum is refused at creation, naming a cheaper chain.
nativeMinimum and estimatedSweepFee on the readiness response are the numbers to steer by,
and this is the only moment it is free to fix.
"1,250 USDC" is ambiguous — USDC exists on five chains here and the same address format covers five of them. The hosted page says Send only USDC on Base above the fold; your covering email should say it too. A payer who sends on Ethereum to a Base-quoted address has sent real money to a real address you control, which is recoverable, and it will cost both of you a day.
3. Know when it is paid
Payments through a link fire the same events as any other checkout, with linkId set and
createdVia: "link".
curl -X POST $CHAINOS/v1/webhooks \
-H "X-API-Key: $CHAINOS_KEY" -H "Content-Type: application/json" \
-d '{ "url": "https://your-company.example/webhooks/chainos",
"events": ["payment_confirmed", "payment_underpaid", "payment_late",
"payment_link_closed"] }'
async function handle(event) {
const invoiceId = event.data.metadata?.invoiceId;
switch (event.event) {
case 'payment_confirmed':
// The only event that marks an invoice paid. `detected` can still be
// reorganised away.
await markPaid(invoiceId, event.data.amountPaid, event.data.txid);
break;
case 'payment_underpaid':
// Real money arrived, just not enough. The link stays open.
await askForRemainder(invoiceId, event.data.amountRemaining);
break;
case 'payment_late':
// Paid after the closing date. Still real, still credited.
await reviewLatePayment(invoiceId);
break;
case 'payment_link_closed':
await recordClosure(invoiceId, event.data.closedReason);
break;
}
}
Verify the signature over the raw bytes of the request, not a re-serialised object. See Webhooks.
If you would rather poll than receive, GET /v1/payments/links/{id} carries amountRaised,
paymentCount and status. amountRaised is confirmed money only — a total that counted
unconfirmed deposits could go backwards on a page somebody is watching.
4. What the payer gets
An invoice is issued and emailed automatically on the first confirmation, numbered
INV-2026-000123. The number is per account, per year, so it does not tell your customer how
many invoices the whole platform has issued.
It is a hosted HTML page with a print stylesheet rather than a PDF. Resending
(POST /v1/payments/invoices/{id}/resend) mints a new link and stops the old one working,
which is what you want when the first email went astray.
5. Closing, pausing, and the deadline that is not a deadline
A fixed link closes itself when it is paid. closedReason says which condition fired:
closedReason | |
|---|---|
paid | It took its payment |
date | closesAt passed |
manual | Somebody closed it |
Closing is irreversible and freezes the total. To take a page down temporarily, pause it — the total is kept and resuming puts it back.
curl -X POST $CHAINOS/v1/payments/links/$ID/pause -H "X-API-Key: $KEY"
curl -X POST $CHAINOS/v1/payments/links/$ID/resume -H "X-API-Key: $KEY"
curl -X POST $CHAINOS/v1/payments/links/$ID/close -H "X-API-Key: $KEY"
The address keeps being monitored until monitorUntil, weeks later, and late funds are
credited. A customer who pays an invoice three days after you closed it has sent real money.
This is the single most common source of "the customer says they paid and we have no record"
in any crypto integration. Subscribe to payment_late and give it a handler, not a default
branch.
6. Editing
PATCH /v1/payments/links/{id} changes presentation and closing conditions: title,
description, successUrl, collectEmail, minAmount, maxAmount, closesAt, maxUses,
metadata.
It does not change the chain, the asset, the mode or a fixed link's amount. Those are what the address was derived for and what has already been quoted; changing them would rewrite the meaning of money that may already be in flight. Close the link and issue a new one.
7. Sweep what you collect
Each link owns an address. Fifty invoices is fifty addresses holding fifty balances, and on an account-model chain you cannot spend them together.
Configure a treasury wallet and a sweep policy for each (chain, asset) you invoice in — see Customer wallets §8. Sweeps are never billed, so do it as often as the network fee justifies.
In the portal
Payment links in the sidebar: mode, raised against target, what will close it and when, and copy-URL, pause and close on each row. Closing sits behind a confirmation because it cannot be undone.
Next
- Payment links — the reference page, including donation mode.
- Donation pages — many payers, accumulating, and the printed-QR case.
- Verifying payments — how to be sure an invoice is really paid.