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 — processing payment.paid twice must be harmless.
  • Ordering is not guaranteed; use the status in data rather than assuming sequence.