Webhook Signature Verification

Apideck signs all webhook events that are sent to your endpoints. This allows you to verify that the webhooks were sent by Apideck, not by a third party.

How It Works

When Apideck sends a webhook, it includes a

x-apideck-signature
header. This signature is created by:

  1. Taking the entire request body (with the
    payload
    envelope structure)
  2. Recursively sorting all object keys alphabetically (maintaining array order)
  3. Creating a hash using HMAC SHA-256, with your API key as the secret key

Verifying the Signature

To verify the signature, you need to:

  1. Get the
    x-apideck-signature
    header from the request
  2. Get the raw request body (as a string)
  3. Recreate the same signature on your server using your API key
  4. Compare the signatures using a secure constant-time comparison function

If the signatures match, you can be sure the webhook was sent by Apideck and the request body hasn't been tampered with.

Implementation Examples

Node.js

import crypto from 'crypto'
import express from 'express'
const app = express()

// Use express.json() middleware AFTER verification
app.use(express.raw({ type: 'application/json' }))

// Your webhook endpoint
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-apideck-signature']
  const payload = req.body
  const apiKey = 'your_api_key' // Store this securely

  // Verify the signature
  const isValid = verifySignature(payload, signature, apiKey)

  if (!isValid) {
    return res.status(401).send('Invalid signature')
  }

  // Process the webhook payload
  const parsedPayload = JSON.parse(payload)
  console.log('Verified webhook:', parsedPayload)

  res.status(200).send('Webhook received')
})

function verifySignature(payload, signature, apiKey) {
  // Sort object keys recursively to match Apideck's signature algorithm
  const sortObjectKeys = (obj) => {
    if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
      return obj
    }

    return Object.keys(obj)
      .sort()
      .reduce((result, key) => {
        result[key] = sortObjectKeys(obj[key])
        return result
      }, {})
  }

  // Create sorted payload JSON string
  const sortedPayload = JSON.stringify(sortObjectKeys(JSON.parse(payload)))

  // Create HMAC using your API key
  const calculatedSignature = crypto
    .createHmac('sha256', apiKey)
    .update(sortedPayload)
    .digest('hex')

  // Use constant-time comparison
  return crypto.timingSafeEqual(Buffer.from(calculatedSignature), Buffer.from(signature))
}

app.listen(3000, () => {
  console.log('Server listening on port 3000')
})

Python

import json
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)

# Your API key
API_KEY = 'your_api_key'  # Store this securely

def sort_object_keys(obj):
    """Recursively sort object keys to match Apideck's signature algorithm"""
    if obj is None or not isinstance(obj, dict):
        return obj

    return {
        k: sort_object_keys(v) if isinstance(v, dict) else v
        for k, v in sorted(obj.items())
    }

def verify_signature(payload, signature, api_key):
    """Verify webhook signature"""
    # Parse and sort the payload
    parsed_payload = json.loads(payload)
    sorted_payload = sort_object_keys(parsed_payload)

    # Create JSON string with sorted keys
    payload_string = json.dumps(sorted_payload, separators=(',', ':'))

    # Calculate HMAC
    calculated_signature = hmac.new(
        api_key.encode('utf-8'),
        payload_string.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # Compare signatures using constant-time comparison
    return hmac.compare_digest(calculated_signature, signature)

@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.get_data().decode('utf-8')
    signature = request.headers.get('x-apideck-signature')

    if not signature:
        return jsonify({'error': 'No signature provided'}), 401

    if verify_signature(payload, signature, API_KEY):
        # Process the webhook
        webhook_data = json.loads(payload)
        print(f"Verified webhook: {webhook_data}")
        return jsonify({'status': 'success'}), 200
    else:
        return jsonify({'error': 'Invalid signature'}), 401

if __name__ == '__main__':
    app.run(port=3000)

PHP

<?php

function sortObjectKeys($obj) {
    if (!is_array($obj) || !count($obj)) {
        return $obj;
    }

    // Check if array is associative or sequential
    if (array_keys($obj) !== range(0, count($obj) - 1)) {
        // Sort associative array keys
        ksort($obj);
        foreach ($obj as $key => $value) {
            if (is_array($value)) {
                $obj[$key] = sortObjectKeys($value);
            }
        }
    } else {
        // For sequential arrays, maintain order but sort contents if they're objects
        foreach ($obj as $key => $value) {
            if (is_array($value)) {
                $obj[$key] = sortObjectKeys($value);
            }
        }
    }

    return $obj;
}

function verifySignature($payload, $signature, $apiKey) {
    // Parse and sort payload
    $parsedPayload = json_decode($payload, true);
    $sortedPayload = sortObjectKeys($parsedPayload);

    // Create JSON string with sorted keys
    $payloadString = json_encode($sortedPayload, JSON_UNESCAPED_SLASHES);

    // Calculate HMAC
    $calculatedSignature = hash_hmac('sha256', $payloadString, $apiKey);

    // Compare signatures using constant-time comparison
    return hash_equals($calculatedSignature, $signature);
}

// Get request data
$payload = file_get_contents('php://input');
$headers = getallheaders();
$signature = isset($headers['X-Apideck-Signature']) ? $headers['X-Apideck-Signature'] : '';
$apiKey = 'your_api_key'; // Store this securely

if (empty($signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'No signature provided']);
    exit;
}

if (verifySignature($payload, $signature, $apiKey)) {
    // Process the webhook
    $webhookData = json_decode($payload, true);
    error_log('Verified webhook: ' . print_r($webhookData, true));

    http_response_code(200);
    echo json_encode(['status' => 'success']);
} else {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
}

Important Notes

  1. Key Security: Store your API key securely and never expose it in client-side code.

  2. Raw Body: You must use the raw request body for verification before parsing it. Many frameworks automatically parse JSON bodies, but you need the exact string that Apideck signed.

  3. Sorting Algorithm: Make sure your sorting algorithm matches Apideck's exactly:

    • Sort object keys alphabetically
    • Process objects recursively to sort nested objects
    • Preserve array element order (don't sort arrays)
  4. Constant-Time Comparison: Always use a constant-time string comparison function to prevent timing attacks.

  5. Content Type: Apideck sends webhook payloads as JSON with

    Content-Type: application/json
    .

Testing Your Implementation

We recommend testing your signature verification with a known payload and signature. You can:

  1. Use a webhook testing tool to capture a real Apideck webhook
  2. Log the raw payload and signature to verify your implementation
  3. Create a test endpoint that prints both the calculated and received signatures for debugging

If you have any implementation questions, contact Apideck support for assistance.