Webhook RelayQuickstart

Receiver patterns

A receiver is a publicly-reachable HTTPS endpoint that accepts a signed POST, verifies the signature, returns 2xx fast, and does any slow work asynchronously. The shape is identical across languages; the snippets below are deliberately complete — copy them.

Next.js (App Router)

typescript
// app/api/webhooks/route.ts
import { verifySignature } from "@philiprehberger/webhook-relay-client";

export async function POST(request: Request) {
  const body = await request.text();
  const ok = verifySignature(
    process.env.WEBHOOK_SECRET!,
    body,
    request.headers.get("x-webhook-signature"),
  );
  if (!ok) return new Response("Bad signature", { status: 400 });

  // Acknowledge fast, do work in the background.
  enqueue(JSON.parse(body));
  return new Response("ok", { status: 200 });
}

Laravel

php
// routes/api.php
use PhilipRehberger\WebhookRelayClient\Signer;

Route::post('/webhooks/relay', function (Request $request) {
    $body = $request->getContent();
    if (! Signer::verify(
        secret: env('WEBHOOK_SECRET'),
        body: $body,
        header: $request->header('X-Webhook-Signature', ''),
    )) {
        return response()->json(['error' => 'bad_signature'], 400);
    }
    HandleEventJob::dispatch(json_decode($body, true));
    return response()->noContent();
});

FastAPI

python
from fastapi import FastAPI, Request, Response
from philiprehberger_webhook_relay_client import verify_signature

app = FastAPI()

@app.post("/webhooks/relay")
async def webhook(request: Request):
    body = await request.body()                            # raw bytes
    if not verify_signature(
        secret=os.environ["WEBHOOK_SECRET"],
        body=body,
        header=request.headers.get("x-webhook-signature"),
    ):
        return Response("Bad signature", status_code=400)
    queue.enqueue(json.loads(body))
    return Response(status_code=204)

Go (net/http)

go
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    if !webhookrelay.VerifySignature(
        os.Getenv("WEBHOOK_SECRET"),
        string(body),
        r.Header.Get("X-Webhook-Signature"),
        0,
    ) {
        http.Error(w, "bad signature", http.StatusBadRequest)
        return
    }
    queue.Enqueue(body)
    w.WriteHeader(http.StatusNoContent)
}

Five rules

  1. Verify against the raw body. Never JSON-parse and re-stringify before verification — the bytes change.
  2. Return fast. Acknowledge with 2xx the moment the signature checks out. Push slow work to a queue. The relay retries on timeouts.
  3. Be idempotent. Use the event ID as your dedup key downstream. Retries happen; design for them.
  4. Don't throw on 4xx-shaped input. Return 400 and let the relay dead-letter it. Throwing turns into a 500 → retry storm.
  5. Log the signature header. When a receiver rejects a webhook, the signature header in your logs is what lets the operator reproduce the verification locally.