Webhook signature verification
Webhook signature verification
Actual sender protocol
| Header | Value |
|---|---|
X-Webhook-Signature | 64 lowercase hex characters of HMAC-SHA256, without a prefix |
X-Webhook-Event | Event name; not covered by the signature |
Content-Type | application/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.