Webhook signature verification

Webhook signature verification

Actual sender protocol

HeaderValue
X-Webhook-Signature64 lowercase hex characters of HMAC-SHA256, without a prefix
X-Webhook-EventEvent name; not covered by the signature
Content-Typeapplication/json

The original signing_secret is shown once at registration/rotation. The sender derives its key as UTF8(SHA256(secret).hex_lowercase) and signs unparsed raw HTTP body bytes. Official SDK helpers perform this derivation and timing-independent verification. Do not pass a stored server hash as the original secret.

The signature authenticates body bytes. The current protocol has no signed timestamp or delivery ID and provides no replay prevention or event-header authentication. Do not authorize operations using the unsigned event header. Business deduplication requires your own trusted identifiers and storage; payloads are not guaranteed to share a universal event ID.

Verify with an official SDK

These standalone programs accept an unchanged body encoded in APPFORGES_WEBHOOK_BODY_BASE64, the X-Webhook-Signature value in APPFORGES_WEBHOOK_SIGNATURE, and the original secret in APPFORGES_WEBHOOK_SECRET. In an HTTP handler, capture raw bytes before JSON parsing and parse only after verification. Never log secrets or complete bodies.

Install using quickstart. The programs print true/false and perform no business event processing.

TypeScript

import { verifyWebhookSignature } from "@appforges/sdk/runtime/webhook";
 
const body = new Uint8Array(Buffer.from(process.env.APPFORGES_WEBHOOK_BODY_BASE64!, "base64"));
const valid = await verifyWebhookSignature(
  body,
  process.env.APPFORGES_WEBHOOK_SIGNATURE!,
  [process.env.APPFORGES_WEBHOOK_SECRET!],
);
console.log(valid);

Python

import base64
import os
from appforges import verify_webhook_signature
 
body = base64.b64decode(os.environ["APPFORGES_WEBHOOK_BODY_BASE64"], validate=True)
valid = verify_webhook_signature(
    body,
    os.environ["APPFORGES_WEBHOOK_SIGNATURE"],
    [os.environ["APPFORGES_WEBHOOK_SECRET"]],
)
print(valid)

Go

package main
 
import (
    "encoding/base64"
    "fmt"
    "log"
    "os"
    "github.com/appforges/appforges-sdk-go/runtime"
)
 
func main() {
    body, err := base64.StdEncoding.DecodeString(os.Getenv("APPFORGES_WEBHOOK_BODY_BASE64"))
    if err != nil { log.Fatal(err) }
    valid := runtime.VerifyWebhookSignature(body,
        os.Getenv("APPFORGES_WEBHOOK_SIGNATURE"),
        []string{os.Getenv("APPFORGES_WEBHOOK_SECRET")})
    fmt.Println(valid)
}

Rotation and failure handling

Rotation immediately replaces the sender's key; there is no guaranteed 24-hour dual-signing window. Receivers may temporarily keep multiple original secrets for in-flight requests, then remove the old secret after migration. Wrong/empty keys, tampered bodies and malformed signatures must fail. Stop business processing on failure and return an error according to your receiver contract.

See Webhook management and Webhooks.