Most crypto payment write-ups assume you run WooCommerce. Paste a plugin, flip a switch, done. That works when the cart is WooCommerce. It falls apart when the cart is a Next.js storefront, a SaaS billing page, a custom invoice tool, or a stack you wrote yourself five years ago and are not rewriting for a payment vendor.
Those shops still need a checkout. They just need it over HTTP.
This post is the REST path into Coldtill: create a payment, send the customer to a checkout link, get a signed webhook when it fills, and keep your own order state machine. No plugin required. The same API is what every Coldtill plugin (WooCommerce, PrestaShop, Magento, WHMCS, EmDash, DashCommerce) speaks under the hood — you are just calling it from your own code.
What you are integrating with
Coldtill is a self-hosted crypto payment gateway. You install it on a Linux box with Docker; the stack lands under /opt/coldtill with PostgreSQL, Caddy, and four processes: api, watcher, sweeper, payouts. Free edition install:
curl -fsSL https://coldtill.com/install.sh | sudo bash -s -- --edition free
There is no licence server and no merchant account at Coldtill. The money goes to your cold wallet. Deposit contracts on Ethereum-style chains, Tron and Solana can only send to that cold wallet; Bitcoin deposit addresses come from your own wallet pubkey. The API process holds no keys. Source and releases are on the self-hosted crypto payment gateway repo if you want to read the Compose file before you run anything.
Direct networks cover Ethereum-style (Ethereum, Base, Polygon, BNB), Tron, Solana, Bitcoin, plus USDC/USDT on those rails. Across the product that expands to 200+ coins on 35+ chains when optional auto-convert is on — that path is a separate topic; the REST flow below is the same either way.
The integration shape
Strip away the plugins and you are left with four moving parts:
-
Create a payment —
POST /api/v1/paymentwith amount (USD), customer id, and a return URL. - Redirect the customer — the response includes a checkout link on your Coldtill host (or a custom checkout domain on the project).
- Receive a signed webhook — when the payment fills, partially fills, or cancels, Coldtill POSTs to your endpoint with a signature and replay protection.
- Optionally recheck on return — when the customer hits your return page, ask the API for the payment status again in case the webhook raced the browser.
That is the whole contract. Everything else (which coin they pick, confirmations, sweeps to cold wallet) is Coldtill's problem on your server.

Payments created over the API show up in the dashboard like any other: amount, status, network, and whether they have been swept yet.
Creating a payment
You need a project API key from the Coldtill dashboard. Keys are scoped to one project (one shop surface: wallets, branding, webhooks). Create the payment against your Coldtill base URL:
POST /api/v1/payment
Authorization: Bearer <project-api-key>
Content-Type: application/json
{
"amount": "49.00",
"currency": "USD",
"customer_id": "usr_1842",
"order_id": "inv_9911",
"return_url": "https://app.example.com/billing/crypto/return"
}
Field names in your client may follow the OpenAPI schema exactly — Coldtill ships OpenAPI 3.1 and a TypeScript SDK generated from it, so you do not have to hand-roll types. The important bits are:
- Amount in USD. Coldtill prices payments in dollars. Your storefront can display whatever you like; the invoice Coldtill creates is USD.
- Customer id. Something stable in your system (user id, account id). Coldtill uses it to keep deposit addresses consistent per customer where that model applies.
- Order / reference id. Whatever you need to map the webhook back to an invoice, subscription period, or cart.
- Return URL. Where the checkout "done" button sends the browser. Do not treat arrival here as paid — treat it as a prompt to recheck.
The response gives you a payment id and a checkout URL. Send the customer there. They pick a network and coin, see the exact amount and deposit address, and pay.

The hosted checkout is yours: project branding, optional custom domain, coin and network picker. Your app only created the payment and redirected.
You do not poll the blockchain from the storefront. The watcher on your Coldtill box does that. Your app waits for the webhook (and optionally rechecks on return).
Signed webhooks without getting burned
Webhooks are where custom integrations usually get clever and then get hurt. Coldtill signs each delivery and includes replay protection. Practical rules that have held up:
- Verify the signature with the project's webhook secret before you touch order state. Reject anything that fails.
-
Respect replay protection. A replayed
payment.filledshould be a no-op, not a second credit. Keep an idempotency key or event id and short-circuit duplicates. - Map statuses, don't invent them. Filled means enough confirmed value to cover the invoice under your rules. Partial and cancelled are first-class; handle them explicitly so support is not inventing policy in Slack.
- Ship or unlock from the webhook, not from the return page. The return page is a UX nicety. Networks are slow; browsers are impatient. The webhook is the source of truth; a return-page GET that rechecks the payment is a backup when the user stares at a spinner.
A minimal receiver sketch (shape only — use the SDK or OpenAPI types in real code):
app.post("/webhooks/coldtill", async (req, res) => {
if (!verifyColdtillSignature(req, process.env.COLDTILL_WEBHOOK_SECRET)) {
return res.status(401).end();
}
const event = req.body;
if (await alreadyProcessed(event.id)) {
return res.status(200).end();
}
switch (event.type) {
case "payment.filled":
await markInvoicePaid(event.data.order_id, event.data.payment_id);
break;
case "payment.partial":
await flagPartial(event.data.order_id, event.data);
break;
case "payment.cancelled":
await releaseReservation(event.data.order_id);
break;
}
await rememberEvent(event.id);
res.status(200).end();
});
Point the project's webhook URL at that endpoint in the Coldtill dashboard. Each project has its own URL and signing secret, so a SaaS billing surface and a side store can land on different receivers without sharing secrets.
OpenAPI and the TypeScript SDK
Hand-writing fetch calls is fine for a spike. For anything that lives more than a weekend, use the OpenAPI 3.1 document Coldtill exposes and the TypeScript SDK built from it. You get typed request/response shapes for creating payments, fetching a payment by id (the return-page recheck), listing, and the webhook payload shapes your verifier should expect.
That also keeps you honest when you upgrade the Coldtill box: regenerate or bump the SDK, fix the type errors, ship. Plugins do the same dance behind a nicer UI; custom stacks get the raw contract.
Roles on the Coldtill side matter here. Give the engineer who owns the integration a developer role (API keys, webhooks) and keep admin for people who can change wallets and approve payouts. Read-only is for whoever watches the dashboard without touching keys.
Fitting it into a real stack
Custom storefront / headless cart
On "Pay with crypto", create the payment server-side with the project API key (never from the browser), redirect to the checkout URL, and let the webhook flip the order to paid. Your cart never sees a private key. Your cart never talks to a chain RPC.
SaaS billing / invoices
Same pattern, different nouns. Invoice id becomes order_id. When payment.filled lands, extend the subscription period or mark the invoice settled. Partials stay open with a clear "underpaid — send the remainder to the same address" state if that matches how you operate; otherwise cancel and issue a fresh invoice.
Mixed fleet
One Coldtill install can host many projects. WooCommerce shops use the WordPress plugin; the weird internal billing tool uses REST. Same server under /opt/coldtill, different project API keys and webhook secrets. The GitHub repo is the place to confirm process layout and Compose services if you are sizing the box.
What the API deliberately cannot do
A few boundaries are worth knowing before you design around the wrong assumption:
- The API cannot move customer deposits to an arbitrary address. On EVM-style, Tron and Solana, deposit contracts are locked to your cold wallet. Sweeps only go there.
-
The API holds no keys. Gas for sweeps and the hot wallet used for payouts/refunds are unlocked by the
sweeperandpayoutsprocesses behind your passphrase — not by a REST call with an API key. - Payouts are a person-shaped flow. Address book, 2FA, and for amounts over your limits an admin plus a fresh 2FA code plus an emailed OTP. Do not plan an unattended "refund any address the customer pastes" endpoint on top of Coldtill; that is not what the till is for.
- Editions change sweep fee, not API surface. Free is 2%, Standard 1%, Pro 0% — one-time price, no licence check on the box. Your integration code does not change between editions.
A short checklist before you go live
- Coldtill installed, project created, cold wallets set, hot wallet topped up for gas.
- Project API key created; webhook URL and secret configured; signature verification tested with a deliberate bad signature.
- Create-payment → checkout → testnet (or small mainnet) payment → webhook received once → return-page recheck agrees.
- Partial and cancel paths exercised so support has a runbook.
- Idempotency proven: replay the same webhook, confirm no double credit.
- API key stored as a server secret; browser never sees it.
The short version
If you do not have a plugin for your stack, you do not need one. POST /api/v1/payment, redirect to the checkout link, verify signed webhooks with replay protection, recheck on return. OpenAPI 3.1 and the TypeScript SDK keep the contract typed. The gateway still settles to your cold wallet on your server; your app just drives the payment lifecycle over HTTP.
Docs, pricing and the install one-liner are on coldtill.com. The self-hosted crypto payment gateway repo has the source if you want to read before you curl.
Top comments (0)