SDKs / Verify webhooks
Verify webhooks with the SDKs
Check a webhook delivery's Pantech-Signature in one call, in Go, Python or JavaScript.
Every webhook delivery carries a Pantech-Signature header. Each SDK checks it for you in one call. Check it before you act on the event, and answer 400 if it fails.
Pantech-Signature: t=1792687211,v1=1a5d101f814e8d6654baea274139a16ec4eea3a5f7619322edbfabf799cabb0eWhat the SDKs check:
v1is the HMAC-SHA256 of<t>.<raw body>, keyed with your webhook's secret (whsec_…, the whole string).- The comparison takes the same time whatever the input, so it leaks nothing.
tmust be within 5 minutes of your clock, so an old delivery cannot be replayed.- Any of several
v1values may match. After you rotate the secret, deliveries are signed with both secrets for 24 hours.
How it works underneath, and examples with no SDK: Verify webhook signatures.
Go
VerifyWebhook returns an error when the header is missing or malformed (pantech.ErrWebhookHeader), too old (pantech.ErrWebhookTimestamp) or does not match (pantech.ErrWebhookSignature). A tolerance of 0 means the default, 5 minutes. ParseWebhook verifies and decodes the event in one call.
package main
import (
"io"
"log"
"net/http"
"os"
pantech "github.com/Pantech-Dynamics/pantech-go"
)
var secret = os.Getenv("PANTECH_WEBHOOK_SECRET")
func handle(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "bad body", http.StatusBadRequest)
return
}
event, err := pantech.ParseWebhook(secret, r.Header.Get("Pantech-Signature"), body, 0)
if err != nil {
http.Error(w, "invalid signature", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusNoContent)
// Handle the event after answering. Skip an event.ID you have seen before.
log.Println(event.Type, event.ID)
}
func main() {
http.HandleFunc("POST /hooks/pantech", handle)
log.Fatal(http.ListenAndServe(":3000", nil))
}To test your receiver, pantech.SignWebhook(secret, time.Now(), body) returns a valid header.
Python
pantech.verify_webhook(secret, header, body, tolerance=300) returns None when the delivery verifies, and raises pantech.WebhookVerificationError when the header is missing or malformed, the signature is wrong, or the timestamp is more than tolerance seconds from now (tolerance=None skips that check). With Flask:
import json
import os
import pantech
from flask import Flask, abort, request
SECRET = os.environ["PANTECH_WEBHOOK_SECRET"]
app = Flask(__name__)
@app.post("/hooks/pantech")
def pantech_webhook():
body = request.get_data() # the raw bytes
try:
pantech.verify_webhook(SECRET, request.headers.get("Pantech-Signature"), body)
except pantech.WebhookVerificationError:
abort(400)
event = json.loads(body)
# Queue the event. Skip an event["id"] you have seen before.
return "", 204JavaScript
verifyWebhook takes { secret, header, body } and is async: it resolves when the signature is valid, and rejects with WebhookVerificationError when the header is missing or malformed, the signature is wrong, or the timestamp is too old. toleranceSeconds changes the 300-second window. It uses Web Crypto, so the same code works on Node, Bun, Deno and edge runtimes (on Node 18, start node with --experimental-global-webcrypto).
import { verifyWebhook, WebhookVerificationError } from "@pantechdynamics/sdk";
const SECRET = process.env.PANTECH_WEBHOOK_SECRET!;
export async function POST(request: Request) {
const body = await request.text(); // the raw body
try {
await verifyWebhook({ secret: SECRET, header: request.headers.get("Pantech-Signature"), body });
} catch (err) {
if (err instanceof WebhookVerificationError) {
return new Response("invalid signature", { status: 400 });
}
throw err;
}
const event = JSON.parse(body);
// Queue the event. Skip an event.id you have seen before.
return new Response(null, { status: 204 });
}