Authenticating Routific Events

Verify that events are originating from Routific

Signature Validation

Routific signs every webhook request with x-routific-signatureheader so you can verify that the payload came from Routific and has not been tampered with. Signature validation is optional on your end — but strongly recommended for production integrations.

Every webhook request includes a signature header. Use the signing secret from your webhook settings to validate it against the raw request body before processing the payload.

If a signature does not match, discard the request.

How it works

The signature is an HMAC-SHA256 hash of the raw request body, signed with your webhook's signing secret.

HMAC-SHA256(secret, "${body}")

The x-routific-signature header value follows the format:

v0=${signature}

To verify:

  1. Extract the signature from the header
  2. Compute the expected signature using your secret
  3. Compare signatures using a constant-time comparison
  4. Optionally, reject requests with old timestamps to prevent replay attacks

📘

Use raw request body

Always verify against the raw, unparsed request body. Parsing the JSON and re-serializing it before signing will produce a different hash and fail verification.

🚧

Rotating Secret

During a secret rotation process, two comma-separated signatures may be present — your verification should accept either:

x-routific-signature: v0=<previous_signature>,<new_signature>

Code Samples for Verifying Events

const crypto = require("crypto");

function verifySignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader.startsWith("v0=")) return false;

  const signatures = signatureHeader.slice("v0=".length).split(",").map(s => s.trim());
  const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
  const expectedBuf = Buffer.from(expected, "utf8");

  return signatures.some(sig => {
    const sigBuf = Buffer.from(sig, "utf8");
    return sigBuf.length === expectedBuf.length && crypto.timingSafeEqual(sigBuf, expectedBuf);
  });
}

// Express.js example
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
  const isValid = verifySignature(
    req.body,                              // raw Buffer
    req.headers["x-routific-signature"],
    process.env.ROUTIFIC_WEBHOOK_SECRET
  );

  if (!isValid) return res.sendStatus(401);
  
  // Replay check
  const timestamp = new Date(req.headers["x-routific-timestamp"]);
  if (Date.now() - timestamp.getTime() > 5 * 60 * 1000) return res.sendStatus(401);

  res.sendStatus(200);
  // process req.body ...
});
import hmac
import hashlib
from datetime import datetime, timezone, timedelta

def verify_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    if not signature_header.startswith("v0="):
        return False

    signatures = signature_header[len("v0="):].split(",")
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()

    return any(hmac.compare_digest(expected, sig.strip()) for sig in signatures)

# Flask example
from flask import Flask, request, abort

app = Flask(__name__)

@app.route("/webhooks", methods=["POST"])
def webhook():
    valid = verify_signature(
        request.get_data(),                          # raw bytes
        request.headers.get("x-routific-signature", ""),
        os.environ["ROUTIFIC_WEBHOOK_SECRET"]
    )
    if not valid:
        abort(401)

    # Replay check
    timestamp = datetime.fromisoformat(
        request.headers["x-routific-timestamp"].replace("Z", "+00:00")
    )
    if datetime.now(timezone.utc) - timestamp > timedelta(minutes=5):
        abort(401)

    return "", 200
    # process request.json ...
function verifySignature(string $rawBody, string $signatureHeader, string $secret): bool {
    if (!str_starts_with($signatureHeader, 'v0=')) return false;

    $signatures = explode(',', substr($signatureHeader, strlen('v0=')));
    $expected   = hash_hmac('sha256', $rawBody, $secret);

    foreach ($signatures as $sig) {
        if (hash_equals($expected, trim($sig))) return true;
    }
    return false;
}

// Usage
$rawBody         = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_ROUTIFIC_SIGNATURE'] ?? '';
$secret          = getenv('ROUTIFIC_WEBHOOK_SECRET');

if (!verifySignature($rawBody, $signatureHeader, $secret)) {
    http_response_code(401);
    exit;
}

// Replay check
$timestamp = new DateTime($_SERVER['HTTP_X_ROUTIFIC_TIMESTAMP']);
$age       = (new DateTime('now', new DateTimeZone('UTC')))->getTimestamp() - $timestamp->getTimestamp();
if ($age > 300) {
    http_response_code(401);
    exit;
}

// process json_decode($rawBody) ...
require "openssl"

def verify_signature(raw_body, signature_header, secret)
  return false unless signature_header.start_with?("v0=")

  signatures = signature_header.delete_prefix("v0=").split(",").map(&:strip)
  expected   = OpenSSL::HMAC.hexdigest("SHA256", secret, raw_body)

  signatures.any? { |sig| ActiveSupport::SecurityUtils.secure_compare(expected, sig) }
end

# Rails example
def webhook
  raw_body = request.raw_post

  unless verify_signature(raw_body, request.headers["x-routific-signature"], ENV["ROUTIFIC_WEBHOOK_SECRET"])
    return head :unauthorized
  end

  # Replay check
  timestamp = Time.parse(request.headers["x-routific-timestamp"]).utc
  return head :unauthorized if Time.now.utc - timestamp > 300

  head :ok
  # process JSON.parse(raw_body) ...
end
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "strings"
    "time"
)

func verifySignature(rawBody []byte, signatureHeader, secret string) bool {
    if !strings.HasPrefix(signatureHeader, "v0=") {
        return false
    }

    signatures := strings.Split(strings.TrimPrefix(signatureHeader, "v0="), ",")

    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))

    for _, sig := range signatures {
        if hmac.Equal([]byte(strings.TrimSpace(sig)), []byte(expected)) {
            return true
        }
    }
    return false
}

// Usage in an http.Handler
func webhookHandler(w http.ResponseWriter, r *http.Request) {
    rawBody, _ := io.ReadAll(r.Body)

    if !verifySignature(rawBody, r.Header.Get("x-routific-signature"), os.Getenv("ROUTIFIC_WEBHOOK_SECRET")) {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }

    // Replay check
    ts, _ := time.Parse(time.RFC3339, r.Header.Get("x-routific-timestamp"))
    if time.Since(ts) > 5*time.Minute {
        w.WriteHeader(http.StatusUnauthorized)
        return
    }

    w.WriteHeader(http.StatusOK)
    // process rawBody ...
}