Webhooks
Overview
DCE sends HTTPS POST requests to your configured webhookUrl when a deposit or withdrawal status changes.
Each request includes:
Content-Type: application/jsonX-Webhook-Event: event nameX-Webhook-Signature: lowercase hex HMAC-SHA256 signature of the exact raw request body, using yourwebhookSecret
Webhook setup
Configure these fields on your merchant profile:
webhookUrlwebhookSecretwebhookEnabled- Optional:
webhookEvents,webhookTimeout,webhookRetryCount
Events
Deposit events
| Event | Description |
|---|---|
deposit.pending | Deposit is created or awaiting final confirmation |
deposit.confirmed | Deposit is confirmed |
deposit.failed | Deposit failed |
Withdrawal events
| Event | Description |
|---|---|
withdrawal.pending | Withdrawal is queued or in progress |
withdrawal.confirmed | Withdrawal is confirmed |
withdrawal.failed | Withdrawal failed |
Signature verification
Verify X-Webhook-Signature against the raw body exactly as received before parsing JSON.
const crypto = require('crypto');
function verifyDceSignature(rawBody, signatureHeader, webhookSecret) {
const expected = crypto
.createHmac('sha256', webhookSecret)
.update(rawBody, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signatureHeader, 'hex'),
Buffer.from(expected, 'hex')
);
}Delivery and retries
- DCE retries failed deliveries (non-2xx, timeout, or network error) using exponential backoff.
- Default retry count is
3unless configured otherwise. - Return a
2xxresponse after safely receiving the webhook. - Implement idempotency because retries can happen.
Sample payloads
deposit.confirmed
deposit.confirmed{
"event": "deposit.confirmed",
"txHash": "tx_1752630134805_ti0a34wjs",
"toAddress": "0x1234567890123456789012345678901234567890",
"fromAddress": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"amount": "0.08",
"coinSymbol": "ETH",
"tokenSymbol": "ETH",
"confirmedAt": "2024-12-19T10:30:00Z",
"layer": "L1Transaction",
"feeCharges": {
"amount": "0.004",
"percentage": "0.05",
"type": "PERCENTAGE"
},
"receivableAmount": "0.076"
}withdrawal.confirmed
withdrawal.confirmed{
"event": "withdrawal.confirmed",
"txHash": "tx_1752630134805_ti0a34wjs",
"toAddress": "0x1234567890123456789012345678901234567890",
"fromAddress": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"amount": "0.08",
"coinSymbol": "ETH",
"tokenSymbol": "ETH",
"confirmedAt": "2024-12-19T10:30:00Z",
"layer": "L1Transaction",
"feeCharges": {
"amount": "0.004",
"percentage": "0.05",
"type": "PERCENTAGE"
},
"receivableAmount": "0.076"
}For event-specific business behavior, refer to the Deposits and Withdrawals guides.
Webhooks
The webhooks API allows you to receive real-time notifications about payment events, transaction status changes, and system updates. This guide covers webhook setup, event handling, signature verification, and best practices for reliable webhook processing.
How the system delivers webhooks
There are three different paths; only the first one is the merchant integration surface.
1. Outbound webhooks to your URL (merchant integration)
- You configure
webhookUrl,webhookSecret,webhookEnabled, and optionallywebhookEvents,webhookTimeout, andwebhookRetryCounton the merchant profile (for example via user/merchant APIs). - When a deposit, withdrawal, or underlying transaction changes state,
WebhookServicemay queue a delivery: it creates awebhookLogrow, then POSTs JSON to yourwebhookUrl. - The HTTP request includes:
Content-Type: application/jsonX-Webhook-Event— the event name (for exampledeposit.confirmed,withdrawal.failed)X-Webhook-Signature— lowercase hex HMAC-SHA256 of the exact raw body bytes, keyed bywebhookSecret(same string asJSON.stringify(payload)on the server). Verify using the raw body, not a re-serializedJSON.stringifyof a parsed object, so key order cannot break verification.
- Subscription filter: if
webhookEventsis a non-empty string array, only listed events are sent. If the field is missing or not parseable as a string array, there is no filter. An empty array means nothing is sent. - Retries: failed HTTP status, timeouts, or network errors schedule retries with exponential backoff (capped at 30s) up to
webhookRetryCount(default 3). If the response body is JSON and contains"ok": true, retries are not scheduled for that attempt. Operators can run thewebhook-retriesbackground job to process due retries.
Event names in code (not payout.*): deposit.confirmed | deposit.failed | deposit.pending; withdrawal.confirmed | withdrawal.failed | withdrawal.pending; transaction.confirmed | transaction.failed.
2. Internal ingestion — POST /api/webhook/event
POST /api/webhook/eventThis endpoint is admin-only (admin:write). It accepts callbacks from trusted upstream systems using WEBHOOK_SECRET, x-signature (hex HMAC-SHA256 of the JSON body after recursive alphabetical key sorting), and x-timestamp (Unix ms, ±5 minutes). It is not the URL you publish as a merchant. Successful processing updates core records via WebhookHandler and may trigger outbound deliveries in (1).
3. Chain-provider callbacks
Additional routes under /api/webhook/* (for example chain adapters) handle provider-specific payloads and signatures. They are internal plumbing, not the merchant webhook contract.
Overview
Outbound webhooks (merchant integration): DCE POSTs JSON to your configured webhookUrl when deposits, withdrawals, or ledger transactions change state. The canonical description (headers, X-Webhook-Signature, retries, subscription filters) is in How the system delivers webhooks at the top of this page.
Inbound operator route: POST /api/webhook/event is for admin/system ingestion only (signed with WEBHOOK_SECRET). It is not the URL you publish as a merchant.
Settlement notifications: Outbound webhook event names in code are deposit.*, withdrawal.*, and transaction.*. Do not assume settlement.* events are emitted on the same merchant webhook channel unless your account team explicitly confirms it—use the Settlements API for settlement state when in doubt.
Webhook setup (merchant)
Configure webhookUrl, webhookSecret, webhookEnabled, and optionally webhookEvents, webhookTimeout, and webhookRetryCount on the merchant profile (via user/merchant APIs).
Your HTTPS endpoint should:
- Accept POST requests with
Content-Type: application/json - Return
2xxafter the event is safely accepted (move heavy work async if needed) - Verify
X-Webhook-Signature(hex HMAC-SHA256 of the raw body withwebhookSecret) - Implement idempotency (retries are expected on failures/timeouts)
Outbound event names (current code)
| Event type | Description |
|---|---|
deposit.confirmed | Deposit confirmed |
deposit.failed | Deposit failed |
deposit.pending | Deposit pending |
withdrawal.confirmed | Withdrawal confirmed |
withdrawal.failed | Withdrawal failed |
withdrawal.pending | Withdrawal pending |
transaction.confirmed | Ledger transaction confirmed |
transaction.failed | Ledger transaction failed |
Payloads include an event field plus chain and business fields (see repository src/types/webhook.ts for internal TypeScript shapes). Do not rely on a generic { data, metadata } envelope unless you normalize it yourself—integrate against event and the documented fields.
Internal: POST /api/webhook/event
POST /api/webhook/eventPurpose: ingest upstream/system events for processing by WebhookHandler (operator tooling; not the merchant callback URL).
Auth: API key with admin:write, plus server WEBHOOK_SECRET.
Headers:
Content-Type: application/jsonx-signature— hex HMAC-SHA256 of the canonical JSON (parsed then recursively key-sorted—see server verification)x-timestamp— Unix time in milliseconds (±5 minutes)
Success response (typical):
{
"success": true,
"message": "Webhook processed successfully"
}Signature Verification
Merchant outbound deliveries (X-Webhook-Signature)
X-Webhook-Signature)DCE signs the exact raw JSON body bytes sent in the webhook POST using your webhookSecret. The signature is lowercase hex HMAC-SHA256 and is sent in the X-Webhook-Event / X-Webhook-Signature headers (see How the system delivers webhooks).
Important: verify the signature against the raw HTTP body string captured before JSON parsing. Re-stringifying JSON.parse output can break verification due to key ordering differences.
JavaScript (Express + raw body):
const crypto = require('crypto');
function verifyDceSignature(rawBodyString, signatureHeader, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBodyString, 'utf8').digest('hex');
return crypto.timingSafeEqual(Buffer.from(signatureHeader, 'hex'), Buffer.from(expected, 'hex'));
}
// Capture raw JSON body, then parse JSON only after verification
app.post(
'/webhooks/dce',
express.raw({ type: 'application/json' }),
(req, res) => {
const raw = req.body.toString('utf8');
const signature = req.headers['x-webhook-signature'];
if (!signature || !verifyDceSignature(raw, signature, process.env.DCE_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(raw);
processWebhook(event);
return res.status(200).json({ ok: true });
}
);Python:
import hmac
import hashlib
import json
def verify_webhook_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
# Flask webhook handler
@app.route('/webhooks/dce', methods=['POST'])
def webhook_handler():
signature = request.headers.get('Signature')
payload = request.get_data(as_text=True)
if not verify_webhook_signature(payload, signature, os.environ['WEBHOOK_SECRET']):
return jsonify({'error': 'Invalid signature'}), 401
process_webhook(request.json)
return jsonify({'status': 'received'})PHP:
<?php
function verifyWebhookSignature($payload, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', $payload, $secret);
return hash_equals($signature, $expectedSignature);
}
// Slim/Laravel webhook handler
$app->post('/webhooks/dce', function (Request $request, Response $response) {
$signature = $request->getHeaderLine('signature');
$payload = $request->getBody()->getContents();
if (!verifyWebhookSignature($payload, $signature, $_ENV['WEBHOOK_SECRET'])) {
$response->getBody()->write(json_encode(['error' => 'Invalid signature']));
return $response->withStatus(401);
}
// Process webhook
$event = json_decode($payload, true);
processWebhook($event);
$response->getBody()->write(json_encode(['status' => 'received']));
return $response;
});
?>Java:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
public class WebhookVerifier {
public static boolean verifyWebhookSignature(String payload, String signature, String secret) {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] hash = sha256Hmac.doFinal(payload.getBytes());
StringBuilder expectedSignature = new StringBuilder();
for (byte b : hash) {
expectedSignature.append(String.format("%02x", b));
}
return signature.equals(expectedSignature.toString());
} catch (Exception e) {
return false;
}
}
}
// Spring Boot webhook handler
@PostMapping("/webhooks/dce")
public ResponseEntity<Map<String, String>> webhook(
@RequestBody Map<String, Object> event,
@RequestHeader("signature") String signature,
@RequestBody String rawPayload) {
if (!WebhookVerifier.verifyWebhookSignature(rawPayload, signature, System.getenv("WEBHOOK_SECRET"))) {
return ResponseEntity.status(401)
.body(Map.of("error", "Invalid signature"));
}
// Process webhook
processWebhook(event);
return ResponseEntity.ok(Map.of("status", "received"));
}C#:
using System.Security.Cryptography;
using System.Text;
public class WebhookVerifier
{
public static bool VerifyWebhookSignature(string payload, string signature, string secret)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedSignature = Convert.ToHexString(hash).ToLower();
return signature.Equals(expectedSignature, StringComparison.OrdinalIgnoreCase);
}
}
}
// ASP.NET Core webhook handler
[HttpPost("dce")]
public IActionResult Webhook()
{
var signature = Request.Headers["signature"].FirstOrDefault();
var payload = Request.Body.ToString();
if (!WebhookVerifier.VerifyWebhookSignature(payload, signature,
Environment.GetEnvironmentVariable("WEBHOOK_SECRET")))
{
return Unauthorized(new { error = "Invalid signature" });
}
// Process webhook
var eventData = JsonConvert.DeserializeObject<dynamic>(payload);
ProcessWebhook(eventData);
return Ok(new { status = "received" });
}Event Processing
Fee Charges in Webhook Payloads
For deposit.confirmed and withdrawal.confirmed events, the webhook payload includes a feeCharges object that provides detailed information about the fees charged to the merchant:
{
"feeCharges": {
"amount": "0.004",
"percentage": "0.05",
"type": "PERCENTAGE"
},
"receivableAmount": "0.076"
}Fee Charges Object Properties
| Property | Type | Description |
|---|---|---|
amount | string | The actual fee amount charged (in the transaction currency) |
percentage | string | The percentage rate used for calculation (e.g., "0.05" for 5%) |
type | string | The charge type: PERCENTAGE, FIXED_AMOUNT, or HYBRID |
Additional Fields
| Property | Type | Description |
|---|---|---|
receivableAmount | string | The final amount after fee deduction (transaction amount - fee charges) |
Charge Types
- PERCENTAGE: Fee calculated as a percentage of the transaction amount
- FIXED_AMOUNT: Fixed fee amount regardless of transaction size
- HYBRID: Combination of percentage and fixed amount
Example Fee Calculations
Percentage-based (5%):
{
"amount": "100.00",
"feeCharges": {
"amount": "5.00",
"percentage": "0.05",
"type": "PERCENTAGE"
},
"receivableAmount": "95.00"
}Fixed amount ($2.50):
{
"amount": "100.00",
"feeCharges": {
"amount": "2.50",
"percentage": "0.00",
"type": "FIXED_AMOUNT"
},
"receivableAmount": "97.50"
}Hybrid (2% + $1.00):
{
"amount": "100.00",
"feeCharges": {
"amount": "3.00",
"percentage": "0.02",
"type": "HYBRID"
},
"receivableAmount": "97.00"
}Deposit Events
deposit.confirmed
{
"event": "deposit.confirmed",
"txHash": "tx_1752630134805_ti0a34wjs",
"toAddress": "0x1234567890123456789012345678901234567890",
"fromAddress": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"amount": "0.08",
"coinSymbol": "ETH",
"tokenSymbol": "ETH",
"confirmedAt": "2024-12-19T10:30:00Z",
"layer": "L1Transaction",
"internalFee": {
"deposit": "0.002"
},
"feeCharges": {
"amount": "0.004",
"percentage": "0.05",
"type": "PERCENTAGE"
},
"receivableAmount": "0.076",
"receiverInfo": {
"identity": "AS188689e48494c8a452683587138f209d673aada204cb23393140e7f40280e0c5"
},
"identifier": "user123",
"depositRequest": {
"exchangeRate": "7.182",
"requestedValue": {
"amount": "1000",
"currency": "USD"
}
}
}deposit.failed
{
"event": "deposit.failed",
"txHash": "tx_1752630135962_4vp6wefk2",
"toAddress": "0x1234567890123456789012345678901234567890",
"fromAddress": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"amount": "0.1",
"coinSymbol": "ETH",
"tokenSymbol": "ETH",
"failedAt": "2024-12-19T10:30:00Z",
"layer": "L1Transaction",
"reason": "Invalid destination address"
}Payout Events
payout.confirmed
{
"event": "payout.confirmed",
"txHash": "tx_1752630134805_ti0a34wjs",
"toAddress": "0x1234567890123456789012345678901234567890",
"fromAddress": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"amount": "0.08",
"coinSymbol": "ETH",
"tokenSymbol": "ETH",
"confirmedAt": "2024-12-19T10:30:00Z",
"layer": "L1Transaction",
"internalFee": {
"withdraw": "0.002"
},
"feeCharges": {
"amount": "0.004",
"percentage": "0.05",
"type": "PERCENTAGE"
},
"receivableAmount": "0.076"
}payout.failed
{
"event": "payout.failed",
"txHash": "tx_1752630135962_4vp6wefk2",
"toAddress": "0x1234567890123456789012345678901234567890",
"fromAddress": "0x742d35Cc6634C0532925a3b8D4C9db96C4b4d8b6",
"amount": "0.1",
"coinSymbol": "ETH",
"tokenSymbol": "ETH",
"failedAt": "2024-12-19T10:30:00Z",
"layer": "L1Transaction",
"reason": "Invalid destination address"
}Webhook Processing
Idempotency
Implement idempotency to prevent duplicate processing:
class WebhookProcessor {
constructor() {
this.processedEvents = new Set();
}
async processWebhook(event) {
const eventId = `${event.txHash}-${event.event}`;
// Check if already processed
if (this.processedEvents.has(eventId)) {
console.log(`Event already processed: ${eventId}`);
return { status: 'already_processed' };
}
// Process the event
await this.handleEvent(event);
// Mark as processed
this.processedEvents.add(eventId);
return { status: 'processed' };
}
async handleEvent(event) {
switch (event.event) {
case 'deposit.confirmed':
await this.handleDepositConfirmed(event);
break;
case 'deposit.failed':
await this.handleDepositFailed(event);
break;
case 'payout.confirmed':
await this.handlePayoutConfirmed(event);
break;
case 'payout.failed':
await this.handlePayoutFailed(event);
break;
default:
console.log(`Unknown event type: ${event.event}`);
}
}
}Database Integration
class WebhookDatabase {
async logWebhookEvent(event, status) {
return await prisma.webhookLog.create({
data: {
eventType: event.event,
payload: event,
status: status,
merchantId: event.metadata?.merchantId || 'system',
transactionId: event.metadata?.transactionId
}
});
}
async updateTransactionStatus(transactionId, status, metadata = {}) {
return await prisma.transaction.update({
where: { id: transactionId },
data: {
status,
updatedAt: new Date(),
metadata: {
...metadata,
webhookProcessedAt: new Date().toISOString()
}
}
});
}
}Error Handling
Retry Logic
Implement exponential backoff for failed webhook processing:
class WebhookRetryHandler {
async processWithRetry(event, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await this.processWebhook(event);
} catch (error) {
attempt++;
if (attempt >= maxRetries) {
console.error(`Webhook processing failed after ${maxRetries} attempts:`, error);
throw error;
}
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
}Error Response Handling
app.post('/webhooks/dce', async (req, res) => {
try {
// Verify signature
if (!verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
await webhookProcessor.processWebhook(req.body);
res.json({ status: 'received' });
} catch (error) {
console.error('Webhook processing error:', error);
// Return 200 to prevent retries for processing errors
res.status(200).json({
status: 'error',
error: error.message
});
}
});Webhook Retry Strategies and Failure Handling
Understanding Webhook Delivery Failures
Webhook delivery can fail for various reasons:
- Network Issues: Temporary connectivity problems, DNS resolution failures
- Server Errors: 5xx HTTP status codes from your endpoint
- Timeout Issues: Slow processing causing connection timeouts
- Rate Limiting: Too many requests hitting your endpoint
- Authentication Failures: Invalid signatures or missing headers
- Payload Issues: Malformed JSON or oversized payloads
Exponential Backoff Implementation
1. Basic Exponential Backoff
class ExponentialBackoffRetry {
constructor(options = {}) {
this.maxRetries = options.maxRetries || 5;
this.baseDelay = options.baseDelay || 1000; // 1 second
this.maxDelay = options.maxDelay || 30000; // 30 seconds
this.jitter = options.jitter || 0.1; // 10% jitter
}
async execute(operation) {
let lastError;
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === this.maxRetries) {
throw new Error(`Operation failed after ${this.maxRetries} attempts: ${error.message}`);
}
const delay = this.calculateDelay(attempt);
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms`);
await this.sleep(delay);
}
}
throw lastError;
}
calculateDelay(attempt) {
// Exponential backoff with jitter
const exponentialDelay = Math.min(
this.baseDelay * Math.pow(2, attempt),
this.maxDelay
);
const jitterAmount = exponentialDelay * this.jitter;
const jitter = (Math.random() - 0.5) * jitterAmount;
return Math.floor(exponentialDelay + jitter);
}
sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
// Usage
const retryHandler = new ExponentialBackoffRetry({
maxRetries: 5,
baseDelay: 1000,
maxDelay: 30000
});
try {
const result = await retryHandler.execute(async () => {
return await processWebhook(event);
});
console.log('Webhook processed successfully');
} catch (error) {
console.error('Webhook processing failed:', error);
}2. Advanced Retry with Circuit Breaker
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.recoveryTimeout = options.recoveryTimeout || 60000; // 1 minute
this.failures = 0;
this.lastFailureTime = null;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
}
async execute(operation) {
if (this.state === 'OPEN') {
if (Date.now() - this.lastFailureTime > this.recoveryTimeout) {
this.state = 'HALF_OPEN';
} else {
throw new Error('Circuit breaker is OPEN');
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
this.lastFailureTime = Date.now();
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
}
}
}
class AdvancedWebhookRetry {
constructor() {
this.retryHandler = new ExponentialBackoffRetry();
this.circuitBreaker = new CircuitBreaker();
}
async processWebhook(event) {
return await this.circuitBreaker.execute(async () => {
return await this.retryHandler.execute(async () => {
return await this.actualWebhookProcessing(event);
});
});
}
async actualWebhookProcessing(event) {
// Your actual webhook processing logic
console.log('Processing webhook:', event.event);
// ... processing logic
}
}Dead Letter Queue Implementation
1. Database-Based Dead Letter Queue
class DeadLetterQueue {
constructor(prisma) {
this.prisma = prisma;
}
async addToDeadLetter(event, error, attemptCount) {
return await this.prisma.deadLetterQueue.create({
data: {
eventType: event.event,
payload: event,
error: error.message,
stackTrace: error.stack,
attemptCount,
correlationId: event.correlationId,
createdAt: new Date(),
status: 'pending'
}
});
}
async processDeadLetterQueue() {
const failedEvents = await this.prisma.deadLetterQueue.findMany({
where: {
status: 'pending',
createdAt: {
gte: new Date(Date.now() - 24 * 60 * 60 * 1000) // Last 24 hours
}
},
orderBy: {
createdAt: 'asc'
}
});
for (const failedEvent of failedEvents) {
try {
await this.retryFailedEvent(failedEvent);
// Mark as processed
await this.prisma.deadLetterQueue.update({
where: { id: failedEvent.id },
data: { status: 'processed' }
});
} catch (error) {
console.error('Failed to process dead letter:', error);
// Mark as permanently failed after max attempts
if (failedEvent.attemptCount >= 10) {
await this.prisma.deadLetterQueue.update({
where: { id: failedEvent.id },
data: { status: 'permanently_failed' }
});
}
}
}
}
async retryFailedEvent(failedEvent) {
const retryHandler = new ExponentialBackoffRetry({
maxRetries: 3,
baseDelay: 5000 // 5 seconds
});
return await retryHandler.execute(async () => {
return await this.processWebhook(failedEvent.payload);
});
}
}2. Redis-Based Dead Letter Queue
class RedisDeadLetterQueue {
constructor(redis) {
this.redis = redis;
this.queueKey = 'webhook:dead_letter_queue';
this.processingKey = 'webhook:processing_queue';
}
async addToDeadLetter(event, error, attemptCount) {
const deadLetterItem = {
id: `dlq_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
event,
error: error.message,
attemptCount,
timestamp: Date.now(),
correlationId: event.correlationId
};
await this.redis.lpush(this.queueKey, JSON.stringify(deadLetterItem));
// Set TTL for automatic cleanup (7 days)
await this.redis.expire(this.queueKey, 7 * 24 * 60 * 60);
console.log(`Added to dead letter queue: ${deadLetterItem.id}`);
}
async processDeadLetterQueue() {
while (true) {
const item = await this.redis.brpop(this.queueKey, 1);
if (!item) continue;
const deadLetterItem = JSON.parse(item[1]);
try {
await this.retryFailedEvent(deadLetterItem);
console.log(`Successfully processed dead letter: ${deadLetterItem.id}`);
} catch (error) {
console.error(`Failed to process dead letter ${deadLetterItem.id}:`, error);
// Re-add to queue if under max attempts
if (deadLetterItem.attemptCount < 10) {
deadLetterItem.attemptCount++;
await this.redis.lpush(this.queueKey, JSON.stringify(deadLetterItem));
} else {
console.log(`Permanently failed dead letter: ${deadLetterItem.id}`);
}
}
}
}
async retryFailedEvent(deadLetterItem) {
const retryHandler = new ExponentialBackoffRetry({
maxRetries: 3,
baseDelay: 5000
});
return await retryHandler.execute(async () => {
return await this.processWebhook(deadLetterItem.event);
});
}
}Comprehensive Webhook Handler with Retry and DLQ
class ComprehensiveWebhookHandler {
constructor(options = {}) {
this.retryHandler = new ExponentialBackoffRetry(options.retry);
this.deadLetterQueue = new DeadLetterQueue(options.prisma);
this.metrics = {
totalEvents: 0,
successfulEvents: 0,
failedEvents: 0,
deadLetterEvents: 0
};
}
async handleWebhook(event) {
this.metrics.totalEvents++;
try {
// Attempt to process with retry
const result = await this.retryHandler.execute(async () => {
return await this.processWebhook(event);
});
this.metrics.successfulEvents++;
return result;
} catch (error) {
this.metrics.failedEvents++;
// Add to dead letter queue
await this.deadLetterQueue.addToDeadLetter(
event,
error,
this.retryHandler.maxRetries
);
this.metrics.deadLetterEvents++;
// Log for monitoring
await this.logFailure(event, error);
throw error;
}
}
async processWebhook(event) {
// Your webhook processing logic
console.log(`Processing webhook: ${event.event}`);
// Simulate processing time
await new Promise(resolve => setTimeout(resolve, 100));
// Simulate potential failure
if (Math.random() < 0.1) { // 10% failure rate for testing
throw new Error('Simulated processing failure');
}
return { status: 'processed', event: event.event };
}
async logFailure(event, error) {
const logEntry = {
timestamp: new Date().toISOString(),
event: event.event,
correlationId: event.correlationId,
error: error.message,
stackTrace: error.stack
};
// Log to monitoring system
console.error('Webhook failure:', logEntry);
// Send to monitoring service
await this.sendToMonitoring(logEntry);
}
async sendToMonitoring(logEntry) {
// Send to your monitoring service (DataDog, New Relic, etc.)
console.log('Sending to monitoring:', logEntry);
}
getMetrics() {
return {
...this.metrics,
successRate: this.metrics.totalEvents > 0
? (this.metrics.successfulEvents / this.metrics.totalEvents) * 100
: 0
};
}
}
// Usage
const handler = new ComprehensiveWebhookHandler({
retry: {
maxRetries: 5,
baseDelay: 1000,
maxDelay: 30000
},
prisma: prisma
});
// Process webhook
try {
const result = await handler.handleWebhook(webhookEvent);
console.log('Webhook processed:', result);
} catch (error) {
console.error('Webhook failed:', error);
}
// Monitor metrics
setInterval(() => {
const metrics = handler.getMetrics();
console.log('Webhook metrics:', metrics);
}, 60000); // Log metrics every minuteMonitoring and Alerting
1. Webhook Health Monitoring
class WebhookHealthMonitor {
constructor() {
this.alerts = [];
this.thresholds = {
failureRate: 0.05, // 5% failure rate
avgProcessingTime: 5000, // 5 seconds
deadLetterQueueSize: 100
};
}
async checkHealth(metrics) {
const alerts = [];
// Check failure rate
if (metrics.successRate < (1 - this.thresholds.failureRate) * 100) {
alerts.push({
severity: 'critical',
message: `High webhook failure rate: ${(100 - metrics.successRate).toFixed(2)}%`,
metric: 'failure_rate',
value: 100 - metrics.successRate
});
}
// Check dead letter queue size
if (metrics.deadLetterEvents > this.thresholds.deadLetterQueueSize) {
alerts.push({
severity: 'warning',
message: `Large dead letter queue: ${metrics.deadLetterEvents} events`,
metric: 'dead_letter_queue_size',
value: metrics.deadLetterEvents
});
}
return alerts;
}
async sendAlert(alert) {
// Send to your alerting system (PagerDuty, Slack, etc.)
console.log(`ALERT [${alert.severity.toUpperCase()}]: ${alert.message}`);
// Store alert
this.alerts.push({
...alert,
timestamp: new Date().toISOString()
});
}
}2. Real-time Metrics Dashboard
class WebhookMetricsDashboard {
constructor() {
this.metrics = {
hourly: new Map(),
daily: new Map()
};
}
recordEvent(event, processingTime, success) {
const hour = new Date().toISOString().slice(0, 13) + ':00:00.000Z';
const day = new Date().toISOString().slice(0, 10);
// Update hourly metrics
if (!this.metrics.hourly.has(hour)) {
this.metrics.hourly.set(hour, {
total: 0,
successful: 0,
failed: 0,
avgProcessingTime: 0,
events: []
});
}
const hourlyMetric = this.metrics.hourly.get(hour);
hourlyMetric.total++;
hourlyMetric.avgProcessingTime =
(hourlyMetric.avgProcessingTime + processingTime) / 2;
if (success) {
hourlyMetric.successful++;
} else {
hourlyMetric.failed++;
}
hourlyMetric.events.push({
event: event.event,
processingTime,
success,
timestamp: new Date().toISOString()
});
// Keep only last 100 events per hour
if (hourlyMetric.events.length > 100) {
hourlyMetric.events = hourlyMetric.events.slice(-100);
}
}
getMetrics(timeframe = 'hourly') {
const metrics = timeframe === 'hourly' ? this.metrics.hourly : this.metrics.daily;
const result = [];
for (const [timestamp, data] of metrics) {
result.push({
timestamp,
total: data.total,
successful: data.successful,
failed: data.failed,
successRate: data.total > 0 ? (data.successful / data.total) * 100 : 0,
avgProcessingTime: data.avgProcessingTime
});
}
return result.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
}
}Best Practices for Webhook Reliability
1. Idempotency Implementation
class IdempotentWebhookProcessor {
constructor(prisma) {
this.prisma = prisma;
}
async processWebhook(event) {
const eventId = this.generateEventId(event);
// Check if already processed
const existing = await this.prisma.webhookLog.findFirst({
where: { eventId }
});
if (existing) {
console.log(`Event already processed: ${eventId}`);
return { status: 'already_processed', eventId };
}
// Process webhook
const result = await this.actualProcessing(event);
// Log successful processing
await this.prisma.webhookLog.create({
data: {
eventId,
eventType: event.event,
payload: event,
status: 'processed',
processedAt: new Date()
}
});
return { status: 'processed', eventId, result };
}
generateEventId(event) {
// Create unique ID based on event characteristics
const components = [
event.event,
event.txHash || event.correlationId,
event.timestamp
];
return require('crypto')
.createHash('sha256')
.update(components.join('|'))
.digest('hex');
}
}2. Rate Limiting and Throttling
class WebhookRateLimiter {
constructor(options = {}) {
this.maxRequestsPerSecond = options.maxRequestsPerSecond || 10;
this.maxRequestsPerMinute = options.maxRequestsPerMinute || 100;
this.requests = [];
}
async throttle(operation) {
await this.waitForRateLimit();
this.requests.push(Date.now());
// Clean old requests
this.requests = this.requests.filter(
time => Date.now() - time < 60000 // Keep last minute
);
return await operation();
}
async waitForRateLimit() {
const now = Date.now();
const requestsLastSecond = this.requests.filter(
time => now - time < 1000
).length;
const requestsLastMinute = this.requests.length;
if (requestsLastSecond >= this.maxRequestsPerSecond) {
const delay = 1000 - (now - this.requests[0]);
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
if (requestsLastMinute >= this.maxRequestsPerMinute) {
const delay = 60000 - (now - this.requests[0]);
if (delay > 0) {
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
}Configuration Examples
1. Environment-Specific Configuration
// config/webhook-config.js
const webhookConfig = {
development: {
retry: {
maxRetries: 3,
baseDelay: 1000,
maxDelay: 10000
},
deadLetterQueue: {
maxRetries: 5,
cleanupAfterDays: 1
},
monitoring: {
enabled: false
}
},
staging: {
retry: {
maxRetries: 5,
baseDelay: 2000,
maxDelay: 30000
},
deadLetterQueue: {
maxRetries: 10,
cleanupAfterDays: 3
},
monitoring: {
enabled: true,
alertThreshold: 0.1 // 10% failure rate
}
},
production: {
retry: {
maxRetries: 7,
baseDelay: 3000,
maxDelay: 60000
},
deadLetterQueue: {
maxRetries: 15,
cleanupAfterDays: 7
},
monitoring: {
enabled: true,
alertThreshold: 0.05 // 5% failure rate
}
}
};
module.exports = webhookConfig[process.env.NODE_ENV || 'development'];2. Docker Compose for Webhook Testing
# docker-compose.webhook-test.yml
version: '3.8'
services:
webhook-server:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- WEBHOOK_SECRET=test_secret
- DATABASE_URL=postgresql://user:pass@db:5432/webhook_test
depends_on:
- db
- redis
db:
image: postgres:13
environment:
POSTGRES_DB: webhook_test
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:6-alpine
ports:
- "6379:6379"
webhook-tester:
build: .
command: npm run test:webhooks
environment:
- WEBHOOK_URL=http://webhook-server:3000/webhooks/dce
- WEBHOOK_SECRET=test_secret
depends_on:
- webhook-server
volumes:
postgres_data:This comprehensive webhook retry and failure handling system provides:
- Exponential backoff with jitter for retry attempts
- Circuit breaker pattern to prevent cascading failures
- Dead letter queue for permanently failed events
- Comprehensive monitoring and alerting
- Idempotency to prevent duplicate processing
- Rate limiting to prevent overwhelming your endpoint
- Environment-specific configuration for different deployment stages
The system ensures your webhook processing is robust, reliable, and maintainable across all environments.
Testing Webhooks
Local Testing
Use ngrok to test webhooks locally:
# Install ngrok
npm install -g ngrok
# Start your local server
npm start
# Expose local server
ngrok http 3000
# Use the ngrok URL as your webhook endpoint
# https://abc123.ngrok.io/webhooks/dceWebhook Testing Tools
class WebhookTester {
async testWebhookEndpoint(url, event) {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Signature': this.generateSignature(event)
},
body: JSON.stringify(event)
});
return {
status: response.status,
body: await response.json()
};
}
generateSignature(payload) {
const crypto = require('crypto');
return crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(JSON.stringify(payload), 'utf8')
.digest('hex');
}
createTestEvent(type, data = {}) {
return {
event: type,
timestamp: new Date().toISOString(),
correlationId: `test-${Date.now()}`,
...data
};
}
}
// Usage
const tester = new WebhookTester();
const testEvent = tester.createTestEvent('deposit.confirmed', {
txHash: 'test_tx_hash',
amount: '0.1',
toAddress: '0x1234567890123456789012345678901234567890'
});
const result = await tester.testWebhookEndpoint('https://your-domain.com/webhooks/dce', testEvent);
console.log(result);Best Practices
1. Security
- Always verify signatures before processing webhooks
- Use HTTPS for webhook endpoints
- Implement rate limiting to prevent abuse
- Validate payload structure before processing
2. Reliability
class ReliableWebhookHandler {
constructor() {
this.queue = [];
this.processing = false;
}
async addToQueue(event) {
this.queue.push(event);
if (!this.processing) {
this.processing = true;
await this.processQueue();
}
}
async processQueue() {
while (this.queue.length > 0) {
const event = this.queue.shift();
try {
await this.processWebhook(event);
} catch (error) {
console.error('Failed to process webhook:', error);
// Add back to queue for retry
this.queue.unshift(event);
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
this.processing = false;
}
}3. Monitoring
class WebhookMonitor {
constructor() {
this.metrics = {
totalEvents: 0,
successfulEvents: 0,
failedEvents: 0,
averageProcessingTime: 0
};
}
async trackEvent(event, processingTime) {
this.metrics.totalEvents++;
this.metrics.averageProcessingTime =
(this.metrics.averageProcessingTime + processingTime) / 2;
// Log to monitoring service
await this.logToMonitoring({
event: event.event,
processingTime,
timestamp: new Date().toISOString()
});
}
async logToMonitoring(data) {
// Send to monitoring service (e.g., DataDog, New Relic)
console.log('Webhook metric:', data);
}
}4. Logging
class WebhookLogger {
async logWebhook(event, result) {
const logEntry = {
timestamp: new Date().toISOString(),
event: event.event,
correlationId: event.correlationId,
status: result.status,
processingTime: result.processingTime,
error: result.error
};
// Log to database
await this.saveToDatabase(logEntry);
// Log to external service
await this.sendToLogService(logEntry);
}
}Integration Examples
Express.js Webhook Handler
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
class WebhookHandler {
constructor() {
this.processor = new WebhookProcessor();
this.logger = new WebhookLogger();
}
async handleWebhook(req, res) {
const startTime = Date.now();
try {
// Verify signature
if (!this.verifySignature(req)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook
const result = await this.processor.processWebhook(req.body);
// Log success
await this.logger.logWebhook(req.body, {
status: 'success',
processingTime: Date.now() - startTime
});
res.json({ status: 'received' });
} catch (error) {
// Log error
await this.logger.logWebhook(req.body, {
status: 'error',
processingTime: Date.now() - startTime,
error: error.message
});
res.status(200).json({
status: 'error',
error: error.message
});
}
}
verifySignature(req) {
const signature = req.headers['signature'];
const payload = JSON.stringify(req.body);
const expectedSignature = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
}
const handler = new WebhookHandler();
app.post('/webhooks/dce', (req, res) => handler.handleWebhook(req, res));Python Flask Webhook Handler
from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
from datetime import datetime
app = Flask(__name__)
class WebhookHandler:
def __init__(self):
self.secret = os.environ['WEBHOOK_SECRET']
def verify_signature(self, payload, signature):
expected_signature = hmac.new(
self.secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
def process_webhook(self, event):
event_type = event.get('event')
if event_type == 'deposit.confirmed':
return self.handle_deposit_confirmed(event)
elif event_type == 'payout.confirmed':
return self.handle_payout_confirmed(event)
else:
print(f"Unknown event type: {event_type}")
return {'status': 'ignored'}
def handle_deposit_confirmed(self, event):
# Update user balance
# Update transaction status
# Send notification
return {'status': 'processed'}
def handle_payout_confirmed(self, event):
# Update withdrawal status
# Send confirmation email
return {'status': 'processed'}
handler = WebhookHandler()
@app.route('/webhooks/dce', methods=['POST'])
def webhook_endpoint():
try:
# Verify signature
signature = request.headers.get('Signature')
payload = request.get_data(as_text=True)
if not handler.verify_signature(payload, signature):
return jsonify({'error': 'Invalid signature'}), 401
# Process webhook
event = request.json
result = handler.process_webhook(event)
return jsonify({'status': 'received'})
except Exception as e:
print(f"Webhook processing error: {e}")
return jsonify({'status': 'error', 'error': str(e)}), 200
if __name__ == '__main__':
app.run(debug=True)Node.js (Express):
const express = require('express');
const crypto = require('crypto');
require('dotenv').config();
const app = express();
app.use(express.json());
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expectedSignature, 'hex')
);
}
function processWebhook(event) {
console.log('Processing webhook:', event.event);
switch (event.event) {
case 'deposit.confirmed':
console.log('Deposit confirmed:', event.txHash);
// Update database, send confirmation email, etc.
break;
case 'deposit.failed':
console.log('Deposit failed:', event.reason);
// Handle failed deposit
break;
case 'payout.confirmed':
console.log('Payout confirmed:', event.txHash);
// Update withdrawal status
break;
case 'payout.failed':
console.log('Payout failed:', event.reason);
// Handle failed payout
break;
default:
console.log('Unknown event type:', event.event);
}
}
app.post('/webhooks/dce', (req, res) => {
try {
const signature = req.headers['signature'];
const payload = JSON.stringify(req.body);
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
processWebhook(req.body);
res.json({ status: 'received' });
} catch (error) {
console.error('Webhook error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});Python (Flask):
from flask import Flask, request, jsonify
import hmac
import hashlib
import os
from dotenv import load_dotenv
load_dotenv()
app = Flask(__name__)
def verify_webhook_signature(payload, signature, secret):
expected_signature = hmac.new(
secret.encode('utf-8'),
payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected_signature)
def process_webhook(event):
print(f"Processing webhook: {event['event']}")
if event['event'] == 'deposit.confirmed':
print(f"Deposit confirmed: {event['txHash']}")
# Update database, send confirmation email, etc.
elif event['event'] == 'deposit.failed':
print(f"Deposit failed: {event['reason']}")
# Handle failed deposit
elif event['event'] == 'payout.confirmed':
print(f"Payout confirmed: {event['txHash']}")
# Update withdrawal status
elif event['event'] == 'payout.failed':
print(f"Payout failed: {event['reason']}")
# Handle failed payout
else:
print(f"Unknown event type: {event['event']}")
@app.route('/webhooks/dce', methods=['POST'])
def webhook():
try:
signature = request.headers.get('signature')
payload = request.get_data(as_text=True)
if not verify_webhook_signature(payload, signature, os.getenv('WEBHOOK_SECRET')):
return jsonify({'error': 'Invalid signature'}), 401
process_webhook(request.json)
return jsonify({'status': 'received'})
except Exception as e:
print('Webhook error:', e)
return jsonify({'error': 'Internal server error'}), 500
if __name__ == '__main__':
app.run(port=3000)PHP (Laravel/Slim):
<?php
require 'vendor/autoload.php';
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
$app = AppFactory::create();
$app->addBodyParsingMiddleware();
function verifyWebhookSignature($payload, $signature, $secret) {
$expectedSignature = hash_hmac('sha256', $payload, $secret);
return hash_equals($signature, $expectedSignature);
}
function processWebhook($event) {
echo "Processing webhook: " . $event['event'] . "\n";
switch ($event['event']) {
case 'deposit.confirmed':
echo "Deposit confirmed: " . $event['txHash'] . "\n";
// Update database, send confirmation email, etc.
break;
case 'deposit.failed':
echo "Deposit failed: " . $event['reason'] . "\n";
// Handle failed deposit
break;
case 'payout.confirmed':
echo "Payout confirmed: " . $event['txHash'] . "\n";
// Update withdrawal status
break;
case 'payout.failed':
echo "Payout failed: " . $event['reason'] . "\n";
// Handle failed payout
break;
default:
echo "Unknown event type: " . $event['event'] . "\n";
}
}
$app->post('/webhooks/dce', function (Request $request, Response $response) {
try {
$signature = $request->getHeaderLine('signature');
$payload = $request->getBody()->getContents();
if (!verifyWebhookSignature($payload, $signature, $_ENV['WEBHOOK_SECRET'])) {
$response->getBody()->write(json_encode(['error' => 'Invalid signature']));
return $response->withStatus(401);
}
$event = json_decode($payload, true);
processWebhook($event);
$response->getBody()->write(json_encode(['status' => 'received']));
return $response;
} catch (Exception $e) {
echo "Webhook error: " . $e->getMessage() . "\n";
$response->getBody()->write(json_encode(['error' => 'Internal server error']));
return $response->withStatus(500);
}
});
$app->run();
?>Java (Spring Boot):
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Map;
@SpringBootApplication
@RestController
public class WebhookController {
private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET");
private boolean verifyWebhookSignature(String payload, String signature) {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(WEBHOOK_SECRET.getBytes(), "HmacSHA256");
sha256Hmac.init(secretKey);
byte[] hash = sha256Hmac.doFinal(payload.getBytes());
StringBuilder expectedSignature = new StringBuilder();
for (byte b : hash) {
expectedSignature.append(String.format("%02x", b));
}
return signature.equals(expectedSignature.toString());
} catch (Exception e) {
return false;
}
}
private void processWebhook(Map<String, Object> event) {
String eventType = (String) event.get("event");
System.out.println("Processing webhook: " + eventType);
switch (eventType) {
case "deposit.confirmed":
System.out.println("Deposit confirmed: " + event.get("txHash"));
// Update database, send confirmation email, etc.
break;
case "deposit.failed":
System.out.println("Deposit failed: " + event.get("reason"));
// Handle failed deposit
break;
case "payout.confirmed":
System.out.println("Payout confirmed: " + event.get("txHash"));
// Update withdrawal status
break;
case "payout.failed":
System.out.println("Payout failed: " + event.get("reason"));
// Handle failed payout
break;
default:
System.out.println("Unknown event type: " + eventType);
}
}
@PostMapping("/webhooks/dce")
public ResponseEntity<Map<String, String>> webhook(
@RequestBody Map<String, Object> event,
@RequestHeader("signature") String signature,
@RequestBody String rawPayload) {
try {
if (!verifyWebhookSignature(rawPayload, signature)) {
return ResponseEntity.status(401)
.body(Map.of("error", "Invalid signature"));
}
processWebhook(event);
return ResponseEntity.ok(Map.of("status", "received"));
} catch (Exception e) {
System.err.println("Webhook error: " + e.getMessage());
return ResponseEntity.status(500)
.body(Map.of("error", "Internal server error"));
}
}
public static void main(String[] args) {
SpringApplication.run(WebhookController.class, args);
}
}C# (.NET):
using Microsoft.AspNetCore.Mvc;
using System.Security.Cryptography;
using System.Text;
using Newtonsoft.Json;
[ApiController]
[Route("[controller]")]
public class WebhookController : ControllerBase
{
private static readonly string webhookSecret = Environment.GetEnvironmentVariable("WEBHOOK_SECRET");
private bool VerifyWebhookSignature(string payload, string signature)
{
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(webhookSecret)))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedSignature = Convert.ToHexString(hash).ToLower();
return signature.Equals(expectedSignature, StringComparison.OrdinalIgnoreCase);
}
}
private void ProcessWebhook(dynamic eventData)
{
string eventType = eventData.event;
Console.WriteLine($"Processing webhook: {eventType}");
switch (eventType)
{
case "deposit.confirmed":
Console.WriteLine($"Deposit confirmed: {eventData.txHash}");
// Update database, send confirmation email, etc.
break;
case "deposit.failed":
Console.WriteLine($"Deposit failed: {eventData.reason}");
// Handle failed deposit
break;
case "payout.confirmed":
Console.WriteLine($"Payout confirmed: {eventData.txHash}");
// Update withdrawal status
break;
case "payout.failed":
Console.WriteLine($"Payout failed: {eventData.reason}");
// Handle failed payout
break;
default:
Console.WriteLine($"Unknown event type: {eventType}");
break;
}
}
[HttpPost("dce")]
public IActionResult Webhook()
{
try
{
var signature = Request.Headers["signature"].FirstOrDefault();
var payload = Request.Body.ToString();
if (!VerifyWebhookSignature(payload, signature))
{
return Unauthorized(new { error = "Invalid signature" });
}
var eventData = JsonConvert.DeserializeObject<dynamic>(payload);
ProcessWebhook(eventData);
return Ok(new { status = "received" });
}
catch (Exception ex)
{
Console.WriteLine($"Webhook error: {ex.Message}");
return StatusCode(500, new { error = "Internal server error" });
}
}
}For more information about specific event types, see the Deposits and Withdrawals documentation.
Updated about 1 month ago
