Webhooks
PayWarden notifies your backend via HTTPS webhooks when payment events occur.
Events
| Event | When |
|---|---|
payment.detected | Transfer detected on-chain, awaiting confirmations |
payment.confirmed | Required confirmations reached |
payment.expired | Order expired without payment |
payment.underpaid | Payment 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:
| Attempt | Delay |
|---|---|
| 1 | 2s |
| 2 | 4s |
| 3 | 8s |
| 4 | 16s |
| 5 | 32s |
| 6 | 64s |
| 7 | 128s (~2 min) |
| 8 | 256s (~4 min) |
| 9 | 512s (~9 min) |
| 10 | 1024s (~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 eventWebhook 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)