Skip to content

Webhooks

PayWarden notifies your backend via HTTPS webhooks when payment events occur.

Events

EventWhen
payment.detectedTransfer detected on-chain, awaiting confirmations
payment.confirmedRequired confirmations reached
payment.expiredOrder expired without payment
payment.underpaidPayment received but amount was insufficient

Payload format

json
{
  "event": "payment.confirmed",
  "order_id": "ord_abc123",
  "external_id": "your-order-123",
  "amount": "99.00",
  "amount_received": "99.00",
  "currency": "USDT",
  "address": "TXxx...",
  "tx_hash": "abc123...",
  "confirmations": 19,
  "created_at": "2025-01-01T00:00:00Z",
  "confirmed_at": "2025-01-01T00:05:00Z"
}

Signature verification

Every webhook includes an X-PayWarden-Signature header:

X-PayWarden-Signature: sha256=<hex>
X-Idempotency-Key: <uuid>

Always verify the signature before processing. This prevents replay attacks and spoofed events.

typescript
import crypto from 'crypto'

function verifyWebhook(
  rawBody: string,
  signature: string,
  secret: string
): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex')
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  )
}
python
import hmac
import hashlib

def verify_webhook(raw_body: str, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(),
        raw_body.encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)
php
function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signature);
}

Retry logic

If your endpoint returns a non-2xx status, PayWarden retries with exponential backoff:

AttemptDelay
12s
24s
38s
416s
532s
664s
7128s (~2 min)
8256s (~4 min)
9512s (~9 min)
101024s (~17 min)

After 10 failed attempts, the webhook is marked as failed. The order status remains confirmed — the payment was received, only the notification failed.

Idempotency

Each webhook delivery has a unique X-Idempotency-Key. If your endpoint receives the same key twice (due to network issues), it's safe to process only once.

typescript
// Store processed keys in Redis or your DB
const key = req.headers['x-idempotency-key']
if (await redis.sismember('processed_webhooks', key)) {
  return res.status(200).send('already processed')
}
await redis.sadd('processed_webhooks', key)
// ... process the event

Webhook endpoint requirements

  • Must be HTTPS in production
  • Must respond within 5 seconds (configurable via WEBHOOK_TIMEOUT_MS)
  • Must return HTTP 2xx to acknowledge receipt
  • Should be idempotent (safe to call multiple times)

Released under the BSL 1.1 License.