When Schedo sends a webhook, it includes a signature in the x-schedo-signature header. This signature allows you to verify that the request really came from Schedo and not from a malicious third party.

Why Verify the Signature?

Anyone can send a POST request to your webhook URL. By verifying the signature, you ensure that only requests genuinely sent by Schedo (using your secret) are accepted and processed.

How the Signature is Generated

  1. Schedo takes the raw request body (the JSON payload).
  2. Schedo computes an HMAC-SHA256 hash of this payload, using your unique signing secret as the key.
  3. Schedo sends the resulting hash (as a hex string) in the x-schedo-signature header.

How to Verify the Signature

To verify the request:

  1. Read the raw request body (exactly as received).
  2. Compute your own HMAC-SHA256 hash of the body, using your signing secret.
  3. Compare your computed hash to the value in the x-schedo-signature header.
  4. If they match, the request is authentic.

Example: Manual Signature Verification in TypeScript/Node.js

import { createHmac, timingSafeEqual } from "crypto";
import { NextRequest, NextResponse } from "next/server";

// Your secret from Schedo settings (keep this safe!)
const SCHEDO_SIGNING_SECRET = process.env.SCHEDO_SIGNING_SECRET!;

function computeSignature(payload: Buffer | string, secret: string): string {
  return createHmac("sha256", secret).update(payload).digest("hex");
}

function isValidSignature(payload: Buffer | string, signature: string, secret: string): boolean {
  const expected = computeSignature(payload, secret);
  // Use timingSafeEqual to prevent timing attacks
  try {
    return timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
  } catch {
    return false;
  }
}

export async function POST(req: NextRequest) {
  // Read the raw body as a Buffer
  const body = await req.text();
  const signature = req.headers.get("x-schedo-signature") ?? "";

  if (!isValidSignature(body, signature, SCHEDO_SIGNING_SECRET)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 400 });
  }

  // If you get here, the request is authentic!
  const data = JSON.parse(body);
  // ... handle your logic

  return NextResponse.json({ success: true });
}

Key Points

  • Always use the raw body (not a parsed object) for signature calculation.
  • Use timingSafeEqual for comparison to prevent timing attacks.
  • Never share your signing secret.

By following these steps, you can ensure that only trusted requests from Schedo are processed by your webhook endpoint.