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

  1. Navigate to SettingsWebhooks in the Deal Engine Dashboard
  2. Under Authentication, select your preferred method:
    • HMAC Signature: For cryptographic request signing
    • API Key: For static key authentication
  3. 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
  4. (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
  5. 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 TypeDisplay NameDescription
refund_request.status_updatedQuote updatedTriggered when a refund quote status changes
refund_candidate.status_updatedRequest updatedTriggered 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
  }
}
FieldTypeDescription
versionstringAPI version of the webhook payload
typestringThe event type identifier
eventIdstringUnique identifier for this event
creatednumberUnix timestamp (seconds) when the event was created
dataobjectEvent-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:

  1. Extract the API key from the header (default: X-API-KEY or your custom header name)
  2. 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
ComponentDescription
tUnix timestamp (seconds) when the request was signed
sHMAC 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

IssuePossible CauseSolution
Signature verification failsUsing parsed body instead of rawEnsure you're using the raw request body
Signature verification failsWrong secretVerify your webhook secret matches
Signature verification failsBody modified by middlewareConfigure middleware to preserve raw body
Timestamp too oldClock driftSync your server's clock with NTP
Events not receivedFirewall blockingAllow incoming requests from Deal Engine IPs
Duplicate events receivedNormal behaviorImplement idempotency using eventId

Testing Your Integration

  1. Use the webhook events log: View sent webhooks in the Deal Engine dashboard
  2. Check response codes: Ensure your endpoint returns 2xx for successful processing
  3. Verify signature locally: Test your signature verification with known values

Support

If you encounter issues with webhook integration:

  1. Check the webhook event logs in the Deal Engine dashboard
  2. Verify your endpoint is accessible from the internet
  3. Review your signature verification implementation
  4. Contact Deal Engine support with your eventId for debugging