Webhooks
Configure endpoints in the dashboard (per environment, subscribed to specific events or *).
Events
invoice.created, invoice.expired, invoice.cancelled, payment.detected, payment.confirming, payment.confirmed, payment.paid, payment.underpaid, payment.overpaid, payment.failed.
Mark orders paid on payment.paid (invoice settled). payment.confirmed fires for every confirmed transfer, including under/over payments.
Payload
{
"id": "evt_01j…",
"type": "payment.paid",
"created_at": "2026-01-01T12:05:00+00:00",
"environment": "live",
"data": {
"invoice_id": "inv_01j…",
"external_id": "ORDER-123",
"status": "paid",
"amount": "19.99",
"currency": "USD",
"crypto_amount": "19.99",
"received_amount": "19.99",
"asset": "USDT",
"network": "TRON",
"payment_address": "T…",
"payment_id": "pay_01j…",
"transaction_hash": "…",
"from_address": "T…",
"confirmations": 19,
"required_confirmations": 19,
"metadata": {"order_id": "123"}
}
}
Payloads never contain API secrets, webhook secrets or private keys.
Signature
Headers: X-Payment-Signature, X-Payment-Timestamp, X-Payment-Event, X-Payment-Event-Id, X-Payment-Delivery-Id.
signature = "v1=" + hex(HMAC_SHA256(secret, timestamp + "." + raw_body))
Verify with the raw request body, compare in constant time, and reject timestamps older than 5 minutes (replay protection).
PHP:
$body = file_get_contents('php://input');
$ts = (int) $_SERVER['HTTP_X_PAYMENT_TIMESTAMP'];
$sig = substr($_SERVER['HTTP_X_PAYMENT_SIGNATURE'], 3); // strip "v1="
if (abs(time() - $ts) > 300 || !hash_equals(hash_hmac('sha256', $ts.'.'.$body, $secret), $sig)) {
http_response_code(400); exit;
}
$event = json_decode($body, true);
Node.js:
const crypto = require('crypto');
const ts = Number(req.headers['x-payment-timestamp']);
const expected = crypto.createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex');
const given = String(req.headers['x-payment-signature']).replace(/^v1=/, '');
const ok = Math.abs(Date.now() / 1000 - ts) < 300 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given));
Python:
import hmac, hashlib, time
ts = int(request.headers["X-Payment-Timestamp"])
expected = hmac.new(secret.encode(), f"{ts}.".encode() + request.data, hashlib.sha256).hexdigest()
given = request.headers["X-Payment-Signature"].removeprefix("v1=")
ok = abs(time.time() - ts) < 300 and hmac.compare_digest(expected, given)
Delivery guarantees
- At-least-once. Respond with any 2xx within 10 seconds. Anything else is retried after 1m, 5m, 15m, 1h, 6h and 24h, then marked failed (retry manually from the dashboard).
- Stable ids. Retries reuse the same
id(evt_…). De-duplicate on it — processingpayment.paidtwice must be harmless. - Ordering is not guaranteed; use the
statusindatarather than assuming sequence.