Body Verification (validate signature)

Signature Headers

Every signed webhook request includes two headers:

Verification Steps

  1. Extract the X-Signature-Ed25519 and X-Signature-Timestamp headers from the request.
  2. Concatenate the timestamp, a period (.), and the raw request body to form the signed message: {timestamp}.{raw_body}
  3. Verify the signature against Routefusion's public key using Ed25519.
  4. (Recommended) Reject requests where the timestamp is more than 5 minutes old to prevent replay attacks.

Public Key

Use the following public key to verify webhook signatures:

Production

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAbKnJoUaBDFT7ZsM3Df/tO6YDL5dmp+aBEstnMg7J+O0=
-----END PUBLIC KEY-----

Sandbox

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAgzAetDWJ6EzDjuq+zfOvotNlmBQhvgxwXxykb+xYdas=
-----END PUBLIC KEY-----

Examples

JavaScript

import crypto from 'crypto';

const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrRp4HERTD+4o9jD8DZfxdGosOMhfdL/afGgLsZf2Ik=\n-----END PUBLIC KEY-----`;

const publicKey = crypto.createPublicKey(PUBLIC_KEY_PEM);

function verifyWebhook(rawBody, signature, timestamp) {
  // Reject requests older than 5 minutes
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 300) {
    return false;
  }

const message = Buffer.from(`${timestamp}.${rawBody}`);
  const sig = Buffer.from(signature, 'hex');
  return crypto.verify(null, message, publicKey, sig);
}

// Express middleware example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-signature-ed25519'];
  const timestamp = req.headers['x-signature-timestamp'];

if (!verifyWebhook(req.body, signature, timestamp)) {
    return res.status(401).send('Invalid signature');
  }

const event = JSON.parse(req.body);
  // Process webhook event...
  res.sendStatus(200);
});

Python

from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
import time

PUBLIC_KEY_HEX = "1eb469e074c74c3fb8a3d8c3f0365fc5d1a8b0e3217dd2ff69f1a02ec65fd889"

def verify_webhook(raw_body: bytes, signature: str, timestamp: str) -> bool:
    # Reject requests older than 5 minutes
    if abs(time.time() - int(timestamp)) > 300:
        return False

verify_key = VerifyKey(bytes.fromhex(PUBLIC_KEY_HEX))
    message = f"{timestamp}.{raw_body.decode()}".encode()

try:
        verify_key.verify(message, bytes.fromhex(signature))
        return True
    except BadSignatureError:
        return False

Go

package main

import (
    "crypto/ed25519"
    "crypto/x509"
    "encoding/hex"
    "encoding/pem"
    "fmt"
    "math"
    "strconv"
    "time"
)

const publicKeyPEM = `-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAHrRp4HERTD+4o9jD8DZfxdGosOMhfdL/afGgLsZf2Ik=\n-----END PUBLIC KEY-----`

func VerifyWebhook(rawBody []byte, signature, timestamp string) bool {
    // Reject requests older than 5 minutes
    ts, err := strconv.ParseInt(timestamp, 10, 64)
    if err != nil || math.Abs(float64(time.Now().Unix()-ts)) > 300 {
        return false
    }

block, _ := pem.Decode([]byte(publicKeyPEM))
    pub, err := x509.ParsePKIXPublicKey(block.Bytes)
    if err != nil {
        return false
    }

sig, err := hex.DecodeString(signature)
    if err != nil {
        return false
    }

message := fmt.Sprintf("%s.%s", timestamp, string(rawBody))
    return ed25519.Verify(pub.(ed25519.PublicKey), []byte(message), sig)
}

Ruby

require 'ed25519'

PUBLIC_KEY_HEX = "1eb469e074c74c3fb8a3d8c3f0365fc5d1a8b0e3217dd2ff69f1a02ec65fd889"

def verify_webhook(raw_body, signature, timestamp)
  # Reject requests older than 5 minutes
  return false if (Time.now.to_i - timestamp.to_i).abs > 300

verify_key = Ed25519::VerifyKey.new([PUBLIC_KEY_HEX].pack('H*'))
  message = "#{timestamp}.#{raw_body}"

begin
    verify_key.verify([signature].pack('H*'), message)
    true
  rescue Ed25519::VerifyError
    false
  end
end