Webhooks

Receive real-time notifications when payment events occur.

Why webhooks?

Webhooks push data to your server as soon as a payment event happens. This is more reliable and faster than polling GET /v1/payment/:id in a loop.

Recommendation

Use the checkout.settled event to trigger fulfillment. Only query payment status directly when you need the current state for a UI display.

Setup

1. Create your endpoint

// Express.js — use raw body for signature verification
app.post('/webhooks/cloakd',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    // verify signature, process event, return 200
    res.status(200).json({ received: true });
  }
);

2. Configure your webhook URL

curl -X PUT https://api.cloakd.ai/v1/webhooks/config \
  -H "X-API-Key: sk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{"webhookUrl": "https://yoursite.com/webhooks/cloakd", "enabled": true}'

3. Get your webhook secret

curl -X GET https://api.cloakd.ai/v1/webhooks/secrets \
  -H "X-API-Key: sk_live_your_api_key"

Store the secret securely — you need it to verify webhook signatures.

Event types

EventWhen it fires
checkout.createdA new payment checkout was created
checkout.pendingPayment detected on-chain, waiting for confirmations
checkout.processingPayment being processed and converted
checkout.settledPayment fully confirmed and settled — safe to fulfill
checkout.failedPayment failed or checkout expired
checkout.refundedA refund was issued

Payload structure

All events share this envelope:

{
  "id": "evt_1234567890",
  "type": "checkout.settled",
  "timestamp": "2026-01-03T14:30:00.000Z",
  "data": {
    "checkout": {
      "id": "chk_abc123",
      "status": "SETTLED",
      "amount": "100.00",
      "currency": "USD",
      "cryptoAmount": "0.0025",
      "cryptoCurrency": "BTC",
      "settledAt": "2026-01-03T14:30:00.000Z"
    },
    "payment": {
      "txHash": "abc123...",
      "confirmations": 3,
      "network": "bitcoin"
    }
  },
  "metadata": {
    "orderId": "order_12345"
  }
}

Signature verification

Every webhook includes two headers:

  • X-Webhook-SignatureHMAC-SHA256 signature of the payload
  • X-Webhook-TimestampUnix timestamp when the webhook was sent

The signature is computed as: HMAC-SHA256(timestamp + "." + raw_payload, your_secret)

Reject webhooks with timestamps older than 5 minutes to prevent replay attacks.

// Node.js verification
const crypto = require('crypto');

function verifySignature(rawBody, signature, timestamp, secret) {
  const signedPayload = `${timestamp}.${rawBody.toString()}`;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  // Use timing-safe comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

Handling webhooks

app.post('/webhooks/cloakd',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.headers['x-webhook-signature'];
    const timestamp = req.headers['x-webhook-timestamp'];

    if (!verifySignature(req.body, signature, timestamp, WEBHOOK_SECRET)) {
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Reject stale webhooks (> 5 min)
    if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
      return res.status(401).json({ error: 'Timestamp expired' });
    }

    const event = JSON.parse(req.body.toString());

    switch (event.type) {
      case 'checkout.settled':
        fulfillOrder(event.data.checkout, event.metadata);
        break;
      case 'checkout.failed':
        handleFailure(event.data.checkout, event.metadata);
        break;
      // Handle other events as needed
    }

    // Always return 200 to acknowledge receipt
    res.status(200).json({ received: true });
  }
);

Always return 200

Return a 200 response as quickly as possible, even if your fulfillment logic is asynchronous. Process the event in a background job rather than making Cloakd wait for it.