Coldtill Get Coldtill

Webhooks

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

Coldtill sends an HTTP POST to your endpoints whenever a payment or payout changes. Add endpoints under Webhooks in the dashboard; each one gets its own signing secret, shown once.

Delivery

  • Method and body: POST with a JSON body.
  • Success: any 2xx response within 60 seconds. Redirects are not followed.
  • Destination: on mainnet, endpoints must be public https URLs. Private, loopback, link-local, carrier-grade NAT and other reserved addresses are refused when connecting, so DNS tricks don't help.
  • Ordering: not guaranteed. Use the payload's status and timestamp, and treat events as idempotent.

Headers:

Header Value
X-Coldtill-Event Event name, for example payment.filled
X-Coldtill-Delivery Unique delivery ID. Retries of the same delivery reuse it
X-Coldtill-Timestamp Unix time in seconds when this attempt was signed
X-Coldtill-Signature-V2 sha256= + hex HMAC-SHA256 of <timestamp>.<raw body> with the endpoint secret
X-Coldtill-Signature sha256= + hex HMAC-SHA256 of the raw body (legacy, no replay protection)
User-Agent Coldtill-Webhook/1

Coldtill was called Keyhold before. Every header above is also sent under its old X-Keyhold- name with the same value, so receivers written before the rename keep working. New code should read X-Coldtill-.

Retries

Kind Schedule after the first attempt
Progress events 2 s, 4 s. A later change sends a fresh event anyway
Final events 2 s, 4 s, 30 min, 1 h, 2 h, 4 h, 8 h, 24 h, 48 h, then marked failed

You can retry any delivery by hand under Webhooks → Deliveries.

Verifying requests

Always verify before trusting the body:

  1. Read the raw body bytes, before any JSON parsing.
  2. Check that X-Coldtill-Timestamp is within five minutes of your clock.
  3. Compute HMAC-SHA256(secret, timestamp + "." + rawBody), hex-encode it, prefix sha256=.
  4. Compare with X-Coldtill-Signature-V2 in constant time.
  5. Optionally, store X-Coldtill-Delivery IDs you've processed and ignore repeats.

X-Coldtill-Event and X-Coldtill-Delivery are not signed. Decide what to do from the signed body (its status, and reference_id for payments), not from the event header. The SDK derives the event from the body and rejects a request whose header disagrees.

Node.js (SDK)

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

const app = express()
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)
    }
    res.sendStatus(200)
  } catch {
    res.sendStatus(401)
  }
})

PHP

$raw = file_get_contents('php://input');
$ts  = $_SERVER['HTTP_X_COLDTILL_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_COLDTILL_SIGNATURE_V2'] ?? '';
$ok  = ctype_digit($ts) && abs(time() - (int) $ts) <= 300
    && hash_equals('sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret), strtolower($sig));
if (!$ok) { http_response_code(401); exit; }
$event = json_decode($raw, true);

Go

func verify(r *http.Request, body, secret []byte) bool {
	ts := r.Header.Get("X-Coldtill-Timestamp")
	n, err := strconv.ParseInt(ts, 10, 64)
	if err != nil || math.Abs(float64(time.Now().Unix()-n)) > 300 {
		return false
	}
	m := hmac.New(sha256.New, secret)
	m.Write([]byte(ts + "."))
	m.Write(body)
	want := "sha256=" + hex.EncodeToString(m.Sum(nil))
	return hmac.Equal([]byte(want), []byte(strings.ToLower(r.Header.Get("X-Coldtill-Signature-V2"))))
}

Python

import hashlib, hmac, time

def verify(headers, raw: bytes, secret: bytes) -> bool:
    ts = headers.get("X-Coldtill-Timestamp", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    want = "sha256=" + hmac.new(secret, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(want, headers.get("X-Coldtill-Signature-V2", "").lower())

Payment events

Event names are payment. plus the lower-cased state: payment.open, payment.partially_filled, payment.filled, payment.over_filled, payment.cancelled, payment.replaced.

{
  "customer_id": "42",
  "invoice_id": "INV-1001",
  "reference_id": "fd09c7e5-d661-4355-9b04-7b3749b41e2e",
  "status": "FILLED",
  "amount": "25",
  "currency": "USD",
  "filled_amount": "25.00000000",
  "filled_amount_in_usd": "25.00000000",
  "timestamp": 1791647937,
  "payment_info": [
    {
      "source_address": "0x…", "transaction_hash": "0x…", "destination_address": "0x…",
      "block_number": 21234567, "network": "BASE", "currency": "USDC", "amount": "25"
    }
  ],
  "confirmation_current": 0,
  "confirmation_required": 0
}
  • Progress events (a deposit seen or gaining confirmations) include confirmation_current and confirmation_required for the deposit that triggered them.

  • The final event (filled, over_filled, cancelled, replaced) is sent once, when the payment closes. A late deposit on a closed payment sends a progress event with the new filled_amount.

  • Auto-convert: payments paid through a swap include a conversion array:

    "conversion": [{
      "provider": "near-intents", "from_currency": "BTC", "from_network": "BTC", "from_amount": "0.000253",
      "from_amount_in_usd": "15.18", "network": "BASE", "currency": "USDC", "amount": "15",
      "transaction_hashes": ["0x…"]
    }]
    

Payout events

Event names are payout. plus the status: payout.pending-approval, payout.pending, payout.initiated, payout.sent, payout.failed, payout.rejected, payout.cancelled, payout.needs-review. The body is the payout object.

payout.needs-review means Coldtill could not tell whether the transaction landed. Don't retry the payout until an admin has checked the hot wallet's history and resolved it. See Payouts.

Testing

Webhooks → Send test posts a signed test event to an endpoint. On testnet installs you can point endpoints at http://localhost receivers; mainnet requires public https URLs.