Coldtill Get Coldtill

Guide: accept a payment from your shop

Paste into Claude Code, Codex, Cursor or your favourite coding agent. View Markdown

This walks through the whole flow once: create an API key, create a payment, send the customer to checkout, and mark the order paid from a webhook. It takes about fifteen minutes on a server that has finished Get started in the dashboard (a cold wallet and at least one deposit wallet).

The full reference is openapi.yaml, rendered at coldtill.com/docs/api.

1. Create an API key and a webhook

  1. In the dashboard, open API keys → New key. Copy the key (ct_…); it is shown once. Store it as COLDTILL_API_KEY in your shop's environment, never in the browser.
  2. Open Webhooks → Add endpoint with your shop's URL, for example https://shop.example.com/coldtill/webhook. Copy the signing secret as COLDTILL_WEBHOOK_SECRET.

2. Create a payment when the customer checks out

Use your own order number as invoiceID and a stable customer ID: the same customer always gets the same deposit addresses.

curl -X POST https://pay.example.com/api/v1/payment \
  -H "API-Key: $COLDTILL_API_KEY" -H 'Content-Type: application/json' \
  -d '{"customerID":"42","customerEmail":"jane@example.com","amountInUSD":25.00,"invoiceID":"INV-1001"}'
// Node 18+, with the SDK: npm install @coldtill/sdk
import { Coldtill } from '@coldtill/sdk'

const ct = new Coldtill({ baseUrl: 'https://pay.example.com', apiKey: process.env.COLDTILL_API_KEY! })
const { url, reference_id } = await ct.createPayment({ customerID: '42', amountInUSD: 25, invoiceID: 'INV-1001' })
await saveReference('INV-1001', reference_id)
return redirect(url)
$res = wp_remote_post('https://pay.example.com/api/v1/payment', [
    'headers' => ['API-Key' => getenv('COLDTILL_API_KEY'), 'Content-Type' => 'application/json'],
    'body'    => wp_json_encode(['customerID' => '42', 'amountInUSD' => 25.00, 'invoiceID' => 'INV-1001']),
    'timeout' => 20,
]);
$payment = json_decode(wp_remote_retrieve_body($res), true);
wp_redirect($payment['url']);
import os, requests

r = requests.post(
    "https://pay.example.com/api/v1/payment",
    headers={"API-Key": os.environ["COLDTILL_API_KEY"]},
    json={"customerID": "42", "amountInUSD": "25.00", "invoiceID": "INV-1001"},
    timeout=20,
)
r.raise_for_status()
payment = r.json()  # {"host": ..., "reference_id": ..., "url": ...}

Save reference_id with the order, then redirect the customer to url. If the project has a checkout domain, the link uses it.

A 409 means the customer's previous payment still has a deposit confirming; show "try again in a few minutes".

3. Mark the order paid from the webhook

Coldtill posts to your endpoint as deposits arrive and confirm. Verify the signature on the raw body, then act on the signed status:

import express from 'express'
import { verifyWebhookRequest, isPaid } from '@coldtill/sdk'

app.post('/coldtill/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const ev = await verifyWebhookRequest(req.body, req.headers, process.env.COLDTILL_WEBHOOK_SECRET!)
    if (ev.event.startsWith('payment.') && isPaid(ev.data.status)) {
      await markOrderPaid(ev.data.invoice_id, ev.data.reference_id) // must be idempotent
    }
    res.sendStatus(200)
  } catch {
    res.sendStatus(401)
  }
})

PHP, Go and Python versions are in Webhooks.

Rules that avoid the usual mistakes:

  • Ship on FILLED or OVER_FILLED only. PARTIALLY_FILLED means the customer sent too little; the checkout page asks them for the rest.
  • Check that reference_id matches the one you saved for invoice_id, so an old payment can't mark a newer order.
  • Make markOrderPaid idempotent: the final event can be retried for up to two days.
  • Respond within 60 seconds. Do slow work (emails, fulfilment) after responding.

4. Handle the edges

What happened Webhook status What to do
Customer never paid CANCELLED after the expiry (24 h by default) Cancel the order or offer a new link
Customer started again REPLACED Ignore; the newer payment carries on
Paid 10% too much OVER_FILLED Ship; refund the difference with a payout if you like
Paid late, after expiry progress event with a new filled_amount Decide by hand; the payment stays closed

5. Test it

On a testnet server (COLDTILL_MODE=testnet), pay with Base Sepolia USDC or Sepolia ETH from a faucet, and point the webhook at http://localhost. Webhooks → Send test sends a signed test event at any time. Then switch to a mainnet server and pay yourself $1 before going live.

Refunds and supplier payments

Payouts send from the hot wallet. Through the API they can only go to addresses in the project's address book, and amounts over the project's limits wait for an admin. See Payouts and the createPayout operation in the reference.