Webhooks

Checking a Webhook Signature

Swaps.xyz signs the webhook events and requests we send to your endpoints. We do so by including a signature in each event’s X-Webhook-Signature header. This allows you to validate that the events and requests were sent by Swaps.xyz, not by a third party.

Before you can verify X-Webhook-Signature signatures for webhook events, you need to retrieve your webhook API key from us.

The X-Webhook-Signature header contains a timestamp and one signature. The timestamp is prefixed by t=, and the signature is prefixed by s=.

X-Webhook-Signature: t=1492774577,s=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Swaps.xyz generates signatures using a hash-based message authentication code (HMAC) with SHA-256.

Step 1

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp, and s corresponds to the signature.

Step 2

You achieve this by concatenating:

  • The timestamp (as a string)
  • The character . and
  • For a POST request, the actual JSON payload (i.e., the request's body).

Step 3

Compute a HMAC with the SHA-256 hash function. Use your account's webhook API key as the key.

Compare the signature in the header to the expected signature.

Code example

const crypto = require('crypto');

function verifyWebhookSignature(
  payload: any,
  signatureHeader: string,
  webhookSecret: string,
) {
  // Parse signature header
  const parts = signatureHeader.split(',');
  const timestamp = parts[0].substring(2);
  const receivedSignature = parts[1].substring(2);

  // Check timestamp tolerance (5 minutes)
  const now = Date.now();
  const timeDiff = Math.abs(now - Number(timestamp));
  if (timeDiff > 300000) {
    throw new Error('Webhook timestamp too old');
  }

  // Generate expected signature
  const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
  const hmac = crypto.createHmac('sha256', webhookSecret);
  hmac.update(signedPayload);
  const expectedSignature = hmac.digest('hex');

  // Compare signatures (use timing-safe comparison)
  return crypto.timingSafeEqual(
    Buffer.from(receivedSignature, 'hex'),
    Buffer.from(expectedSignature, 'hex'),
  );
}
import hmac
import hashlib
import time
import json

class WebhookVerifier:
    def __init__(self, webhook_secret):
        self.webhook_secret = webhook_secret.encode('utf-8')
    
    def verify(self, request_body, signature_header, tolerance_seconds=300):
        try:
            # Parse signature header
            parts = signature_header.split(',')
            timestamp = int(parts[0].split('=')[1])
            received_signature = parts[1].split('=')[1]
            
            # Check timestamp tolerance
            now = int(time.time() * 1000)  # Convert to milliseconds
            time_diff = abs(now - timestamp)
            if time_diff > tolerance_seconds * 1000:
                raise ValueError(f"Webhook timestamp too old: {time_diff}ms")
            
            # Generate expected signature
            signed_payload = f"{timestamp}.{request_body}"
            expected_signature = hmac.new(
                self.webhook_secret,
                signed_payload.encode('utf-8'),
                hashlib.sha256
            ).hexdigest()
            
            # Compare signatures (timing-safe comparison)
            return hmac.compare_digest(received_signature, expected_signature)
            
        except Exception as e:
            print(f"Webhook verification failed: {e}")
            return False