Skip to content

Webhooks

Configure a callback URL in your checkout config to receive real-time HTTP POST notifications when invoice status changes.

Events

EventTrigger
payment_receivedFull BTC amount detected on-chain (invoice moves to paid)
payment_confirmedPayment reaches 1 block confirmation (invoice moves to confirmed)
invoice_expiredInvoice TTL elapsed with no payment (invoice moves to expired)
invoice_underpaidPartial payment received but grace period elapsed (invoice moves to underpaid)

Payload

All webhook payloads have the same shape:

json
{
  "event": "payment_received",
  "invoice": {
    "id": "a1b2c3d4-...",
    "amountUsd": 49.99,
    "amountBtc": 0.0005,
    "btcAddress": "bc1q...",
    "status": "paid",
    "txHash": "def456...",
    "confirmations": 0,
    "paidAt": "2025-01-15T10:35:00.000Z",
    "confirmedAt": null,
    "metadata": {"orderId": "order_123"}
  }
}

Signature verification

Every webhook request includes an X-Checkout-Signature header containing an HMAC-SHA256 hex digest of the raw JSON body, signed with your checkout config ID as the secret.

To verify:

javascript
import crypto from 'node:crypto';

function verifySignature(body, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected),
  );
}

// In your webhook handler:
const body = req.rawBody; // raw JSON string
const signature = req.headers['x-checkout-signature'];
const configId = 'your-checkout-config-id'; // from GET /api/checkout/config

if (!verifySignature(body, signature, configId)) {
  return res.status(401).send('Invalid signature');
}
python
import hmac
import hashlib

def verify_signature(body: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(), body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

TIP

Your config ID (returned by GET /api/checkout/config) is the signing secret. Keep it private.

Retry policy

If your endpoint returns a non-2xx status or the request times out (10 seconds), the webhook is retried:

AttemptDelay after failure
1st retry1 minute
2nd retry5 minutes
3rd retry15 minutes

After 3 failed attempts, the webhook is permanently marked as failed and not retried again. Each invoice event is delivered independently.

Best practices

  • Respond quickly — return a 2xx status within 10 seconds. Process the event asynchronously if needed.
  • Verify signatures — always validate the X-Checkout-Signature header before processing.
  • Handle duplicates — use the invoice.id + event combination as an idempotency key. Retries may deliver the same event multiple times.
  • Use HTTPS — your callback URL should use HTTPS in production.