Webhooks
Deal Engine Webhook Integration Guide
This guide explains how to receive and verify webhook events from Deal Engine in your application.
Overview
Deal Engine uses webhooks to notify your application in real-time when events occur. Instead of polling our API for updates, you can configure webhook endpoints to receive automatic notifications.
Key Features:
- ✅ Real-time event notifications
- ✅ Two authentication methods: HMAC Signature or API Key
- ✅ Configurable via UI or API
- ✅ Support for multiple event types
- ✅ Retry mechanism for failed deliveries
Authentication Options
Deal Engine supports two authentication methods for securing webhook deliveries. You can configure your preferred method through the Deal Engine Dashboard or via the API.
Option 1: HMAC Signature (Recommended)
With HMAC authentication, each webhook request is cryptographically signed using your secret key. This provides:
- Per-request verification: Each request has a unique signature
- Timestamp protection: Prevents replay attacks
- Payload integrity: Ensures the data hasn't been tampered with
Header format:
X-DE-WEBHOOK-SIGNATURE: t=1623436092,s=7e526f3c14539d4d...
Option 2: API Key
With API Key authentication, a static key is sent with every request in the header you specify. This is simpler to implement but provides less security than HMAC.
Header format (example):
X-API-KEY: your-api-key-here
Configuring via the Dashboard
- Navigate to Settings → Webhooks in the Deal Engine Dashboard
- Under Authentication, select your preferred method:
- HMAC Signature: For cryptographic request signing
- API Key: For static key authentication
- Configure your secret/key:
- Enter your own: Provide a custom secret or API key
- Generate for me: Let Deal Engine create a secure random key
- (Optional) Customize the header name: Use your own header name or keep the defaults:
- HMAC default:
X-DE-WEBHOOK-SIGNATURE - API Key default:
X-API-KEY
- HMAC default:
- Click Save to apply your configuration
Tip: You can view your secret at any time in the Dashboard, and use the "Roll Secret" button to generate a new one if needed.
Available Event Types
| Event Type | Display Name | Description |
|---|---|---|
refund_request.status_updated | Quote updated | Triggered when a refund quote status changes |
refund_candidate.status_updated | Request updated | Triggered when a refund request status changes |
Webhook Payload Structure
Every webhook event follows this structure:
{
"version": "1.0",
"type": "refund_request.status_updated",
"eventId": "evt_abc123xyz",
"created": 1623436092,
"data": {
// Event-specific data
}
}| Field | Type | Description |
|---|---|---|
version | string | API version of the webhook payload |
type | string | The event type identifier |
eventId | string | Unique identifier for this event |
created | number | Unix timestamp (seconds) when the event was created |
data | object | Event-specific payload data |
Security: Verifying Webhook Requests
Always verify webhook requests before processing them to ensure they originate from Deal Engine.
Verifying API Key Authentication
If you configured API Key authentication, verification is straightforward:
- Extract the API key from the header (default:
X-API-KEYor your custom header name) - Compare it with your stored secret
function verifyApiKey(req) {
const headerName = 'x-api-key'; // Use your configured header name
const receivedKey = req.headers[headerName];
if (!receivedKey) return false;
// Use constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedKey),
Buffer.from(process.env.WEBHOOK_API_KEY)
);
}Verifying HMAC Signature Authentication
If you configured HMAC Signature authentication, follow these steps:
Why Verify HMAC Signatures?
- Authenticity: Confirm requests originate from Deal Engine
- Integrity: Ensure the payload hasn't been tampered with
- Replay Protection: The timestamp prevents replay attacks
The Signature Header
Every webhook request includes a signature header (default: X-DE-WEBHOOK-SIGNATURE or your custom header name):
X-DE-WEBHOOK-SIGNATURE: t=1623436092,s=7e526f3c14539d4d2856a1a2e8b1112c944cd466670041fe758fcc930d8cdf23
| Component | Description |
|---|---|
t | Unix timestamp (seconds) when the request was signed |
s | HMAC SHA-256 signature in hexadecimal format |
Step 1: Extract Timestamp and Signature
Parse the signature header to extract the t and s values:
const header = req.headers['x-de-webhook-signature']; // Use your configured header name
const [tPart, sPart] = header.split(',');
const timestamp = tPart.split('=')[1];
const signature = sPart.split('=')[1];Step 2: Construct the Signed Payload
Concatenate the timestamp and raw request body with a period (.) separator:
const signedPayload = `${timestamp}.${rawRequestBody}`;
Important: Use the raw, unparsed request body exactly as received. Any formatting changes (whitespace, key ordering) will cause verification to fail.
Step 3: Calculate Expected Signature
Compute an HMAC SHA-256 hash using your webhook secret:
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');Step 4: Compare Signatures
Use a constant-time comparison to prevent timing attacks:
const isValid = crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);Step 5: Verify Timestamp (Recommended)
Check that the timestamp is recent (within 5 minutes) to prevent replay attacks:
const currentTime = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 minutes
if (Math.abs(currentTime - parseInt(timestamp)) > tolerance) {
throw new Error('Timestamp too old');
}Code Examples
Choose the example that matches your configured authentication method.
API Key Authentication Examples
Node.js / Express (API Key)
const crypto = require('crypto');
const express = require('express');
const app = express();
const API_KEY = process.env.WEBHOOK_API_KEY;
const HEADER_NAME = 'x-api-key'; // Use your configured header name
app.post('/webhooks/deal-engine',
express.json(),
(req, res) => {
try {
// Verify API key
if (!verifyApiKey(req.headers)) {
return res.status(401).json({ error: 'Invalid API key' });
}
const event = req.body;
switch (event.type) {
case 'refund_request.status_updated':
handleQuoteUpdate(event.data);
break;
case 'refund_candidate.status_updated':
handleRequestUpdate(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(400).json({ error: 'Webhook processing failed' });
}
}
);
function verifyApiKey(headers) {
const receivedKey = headers[HEADER_NAME];
if (!receivedKey) return false;
// Use constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(receivedKey),
Buffer.from(API_KEY)
);
}Python / Flask (API Key)
import hmac
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
API_KEY = os.environ.get('WEBHOOK_API_KEY')
HEADER_NAME = 'X-API-KEY' # Use your configured header name
@app.route('/webhooks/deal-engine', methods=['POST'])
def handle_webhook():
try:
if not verify_api_key(request.headers):
return jsonify({'error': 'Invalid API key'}), 401
event = request.get_json()
if event['type'] == 'refund_request.status_updated':
handle_quote_update(event['data'])
elif event['type'] == 'refund_candidate.status_updated':
handle_request_update(event['data'])
return jsonify({'received': True}), 200
except Exception as e:
return jsonify({'error': str(e)}), 400
def verify_api_key(headers):
received_key = headers.get(HEADER_NAME)
if not received_key:
return False
return hmac.compare_digest(received_key, API_KEY)HMAC Signature Authentication Examples
Node.js / Express (HMAC)
const crypto = require('crypto');
const express = require('express');
const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
const HEADER_NAME = 'x-de-webhook-signature'; // Use your configured header name
// Important: Use raw body parser for HMAC verification
app.post('/webhooks/deal-engine',
express.raw({ type: 'application/json' }),
(req, res) => {
try {
const rawBody = req.body.toString('utf8');
if (!verifyHmacSignature(req.headers, rawBody)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(rawBody);
switch (event.type) {
case 'refund_request.status_updated':
handleQuoteUpdate(event.data);
break;
case 'refund_candidate.status_updated':
handleRequestUpdate(event.data);
break;
default:
console.log(`Unhandled event type: ${event.type}`);
}
res.status(200).json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
res.status(400).json({ error: 'Webhook processing failed' });
}
}
);
function verifyHmacSignature(headers, rawBody) {
const header = headers[HEADER_NAME];
if (!header) return false;
const [tPart, sPart] = header.split(',');
const timestamp = tPart.split('=')[1];
const receivedSignature = sPart.split('=')[1];
// Check timestamp freshness (5 minute tolerance)
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
return false;
}
// Calculate expected signature
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(expectedSignature)
);
}Python / Flask (HMAC)
import hmac
import hashlib
import time
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
HEADER_NAME = 'X-DE-WEBHOOK-SIGNATURE' # Use your configured header name
@app.route('/webhooks/deal-engine', methods=['POST'])
def handle_webhook():
try:
raw_body = request.get_data(as_text=True)
if not verify_hmac_signature(request.headers, raw_body):
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json()
if event['type'] == 'refund_request.status_updated':
handle_quote_update(event['data'])
elif event['type'] == 'refund_candidate.status_updated':
handle_request_update(event['data'])
return jsonify({'received': True}), 200
except Exception as e:
return jsonify({'error': str(e)}), 400
def verify_hmac_signature(headers, raw_body):
signature_header = headers.get(HEADER_NAME)
if not signature_header:
return False
parts = signature_header.split(',')
timestamp = parts[0].split('=')[1]
received_signature = parts[1].split('=')[1]
# Check timestamp freshness
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300:
return False
# Calculate expected signature
signed_payload = f"{timestamp}.{raw_body}"
expected_signature = hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
signed_payload.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(received_signature, expected_signature)C# / ASP.NET Core (HMAC)
using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("webhooks")]
public class WebhookController : ControllerBase
{
private readonly string _webhookSecret;
private readonly string _headerName = "X-DE-WEBHOOK-SIGNATURE"; // Use your configured header name
public WebhookController(IConfiguration config)
{
_webhookSecret = config["WebhookSecret"];
}
[HttpPost("deal-engine")]
public async Task<IActionResult> HandleWebhook()
{
using var reader = new StreamReader(Request.Body);
var rawBody = await reader.ReadToEndAsync();
if (!VerifyHmacSignature(Request.Headers, rawBody))
{
return Unauthorized(new { error = "Invalid signature" });
}
var webhookEvent = JsonSerializer.Deserialize<WebhookEvent>(rawBody);
switch (webhookEvent.Type)
{
case "refund_request.status_updated":
await HandleQuoteUpdate(webhookEvent.Data);
break;
case "refund_candidate.status_updated":
await HandleRequestUpdate(webhookEvent.Data);
break;
}
return Ok(new { received = true });
}
private bool VerifyHmacSignature(IHeaderDictionary headers, string rawBody)
{
if (!headers.TryGetValue(_headerName, out var signatureHeader))
return false;
var parts = signatureHeader.ToString().Split(',');
var timestamp = parts[0].Split('=')[1];
var receivedSignature = parts[1].Split('=')[1];
// Check timestamp freshness
var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(currentTime - long.Parse(timestamp)) > 300)
return false;
// Calculate expected signature
var signedPayload = $"{timestamp}.{rawBody}";
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(_webhookSecret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(signedPayload));
var expectedSignature = BitConverter.ToString(hash).Replace("-", "").ToLower();
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(receivedSignature),
Encoding.UTF8.GetBytes(expectedSignature)
);
}
}Best Practices
1. Return 2xx Quickly
Return a 200 or 202 response as soon as you receive the webhook. Process heavy operations asynchronously:
app.post('/webhooks/deal-engine', (req, res) => {
// Verify signature first
if (!verifySignature(req)) {
return res.status(401).send();
}
// Acknowledge immediately
res.status(202).json({ received: true });
// Process asynchronously
processWebhookAsync(req.body).catch(console.error);
});2. Handle Duplicates
Webhook events may be delivered more than once. Use the eventId field to implement idempotency:
async function handleWebhook(event) {
// Check if already processed
if (await isEventProcessed(event.eventId)) {
return; // Skip duplicate
}
// Process the event
await processEvent(event);
// Mark as processed
await markEventProcessed(event.eventId);
}3. Implement Retry Logic
If your endpoint fails, Deal Engine will retry delivery. Configure your endpoint to handle retries gracefully.
4. Secure Your Endpoint
- Use HTTPS for your webhook endpoint
- Always verify signatures before processing
- Keep your webhook secret secure (use environment variables)
- Implement rate limiting if needed
5. Log Webhook Events
Maintain logs for debugging and auditing:
function logWebhookEvent(event, status) {
console.log({
timestamp: new Date().toISOString(),
eventId: event.eventId,
eventType: event.type,
status: status
});
}Troubleshooting
Common Issues
| Issue | Possible Cause | Solution |
|---|---|---|
| Signature verification fails | Using parsed body instead of raw | Ensure you're using the raw request body |
| Signature verification fails | Wrong secret | Verify your webhook secret matches |
| Signature verification fails | Body modified by middleware | Configure middleware to preserve raw body |
| Timestamp too old | Clock drift | Sync your server's clock with NTP |
| Events not received | Firewall blocking | Allow incoming requests from Deal Engine IPs |
| Duplicate events received | Normal behavior | Implement idempotency using eventId |
Testing Your Integration
- Use the webhook events log: View sent webhooks in the Deal Engine dashboard
- Check response codes: Ensure your endpoint returns 2xx for successful processing
- Verify signature locally: Test your signature verification with known values
Support
If you encounter issues with webhook integration:
- Check the webhook event logs in the Deal Engine dashboard
- Verify your endpoint is accessible from the internet
- Review your signature verification implementation
- Contact Deal Engine support with your
eventIdfor debugging
Updated 22 days ago