← Back to docs
Recipe

Webhook signature verification

Meridian signs every outbound webhook with an HMAC-SHA256 digest so you can prove a payload originated from us and was not tampered with in transit. This recipe walks through the three-step verification flow you should run before trusting any incoming event.

1. Capture the raw body

HMAC is computed over the exact bytes we sent. Read the request body as text before any JSON parsing or middleware mutates it. Reading as a parsed object first will silently re-serialize the payload and break the signature match.

2. Check the timestamp

The x-meridian-timestamp header carries a Unix epoch second. Reject anything older than five minutes — it stops attackers from replaying captured webhooks against your endpoint hours or days later.

3. Constant-time compare

Recompute the HMAC over ${timestamp}.${rawBody} and compare against x-meridian-signature using timingSafeEqual. A naive string equality leaks bytes through timing side channels.

Drop-in handler

import crypto from 'crypto';
import { NextRequest, NextResponse } from 'next/server';

const SECRET = process.env.MERIDIAN_WEBHOOK_SECRET!;

export async function POST(req: NextRequest) {
  const raw = await req.text();
  const sig = req.headers.get('x-meridian-signature') ?? '';
  const ts = req.headers.get('x-meridian-timestamp') ?? '';

  // 1. Reject stale events (>5 min skew)
  const age = Math.abs(Date.now() / 1000 - Number(ts));
  if (!ts || age > 300) {
    return NextResponse.json({ error: 'stale' }, { status: 400 });
  }

  // 2. Recompute HMAC over "timestamp.body"
  const expected = crypto
    .createHmac('sha256', SECRET)
    .update(`${ts}.${raw}`)
    .digest('hex');

  // 3. Constant-time compare
  const ok =
    sig.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));

  if (!ok) {
    return NextResponse.json({ error: 'bad signature' }, { status: 401 });
  }

  const event = JSON.parse(raw);
  // ... handle event ...
  return NextResponse.json({ received: true });
}

Store MERIDIAN_WEBHOOK_SECRET in your environment, never in source. Rotate it from the Meridian dashboard if you suspect leakage.