Overview
Every webhook delivery from Anton includes a signature header that you must verify before processing the event. This confirms the request genuinely came from Anton and has not been tampered with in transit.
The Signature Header
Each webhook request includes an Anton-Signature header with a timestamp and HMAC-SHA256 signature:
Anton-Signature: t=1708000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
The header contains two components:
t-- Unix timestamp (seconds) of when the webhook was sentv1-- HMAC-SHA256 signature of the signed payload
Verification Steps
Extract the timestamp (
t) and signature (v1) from theAnton-SignatureheaderConstruct the signed payload by concatenating the timestamp, a period, and the raw request body:
{timestamp}.{raw_body}Compute the HMAC-SHA256 of the signed payload using your webhook signing secret
Compare the computed signature with the received
v1value using constant-time comparisonReject the request if the timestamp is more than 5 minutes old (replay protection)
Obtaining Your Signing Secret
The signing secret is returned when you create a webhook subscription. It is only shown once. If you lose it, you can retrieve it via the API:
curl https://api.antonpayments.dev/v1/webhooks/wh_abc123/secret \ -H "Authorization: Bearer ak_test_..."
Store the secret securely in your environment variables or secrets manager.
Code Examples
Node.js (Express)
const crypto = require("crypto");function verifySignature(payload, header, secret) {
const parts = Object.fromEntries(
header.split(",").map(p => p.split("="))
);
const timestamp = parts["t"];
const signature = parts["v1"]; // Reject if older than 5 minutes
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) throw new Error("Timestamp too old"); const signedPayload = timestamp + "." + payload;
const expected = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex"); if (!crypto.timingSafeEqual(
Buffer.from(signature), Buffer.from(expected)
)) {
throw new Error("Invalid signature");
}
}Python
import hmac
import hashlib
import timedef verify_signature(payload: bytes, header: str, secret: str):
parts = dict(p.split("=", 1) for p in header.split(","))
timestamp = parts["t"]
signature = parts["v1"] # Reject if older than 5 minutes
if int(time.time()) - int(timestamp) > 300:
raise ValueError("Timestamp too old") signed_payload = f"{timestamp}.{payload.decode()}"
expected = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest() if not hmac.compare_digest(signature, expected):
raise ValueError("Invalid signature")Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
)func verifySignature(payload []byte, header, secret string) error {
parts := make(map[string]string)
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(p, "=", 2)
if len(kv) == 2 { parts[kv[0]] = kv[1] }
} ts, _ := strconv.ParseInt(parts["t"], 10, 64)
if time.Now().Unix()-ts > 300 {
return fmt.Errorf("timestamp too old")
} signedPayload := fmt.Sprintf("%s.%s", parts["t"], string(payload))
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(signedPayload))
expected := hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(parts["v1"]), []byte(expected)) {
return fmt.Errorf("invalid signature")
}
return nil
}Important Security Notes
Always use constant-time comparison -- Functions like
crypto.timingSafeEqual(Node.js),hmac.compare_digest(Python), andhmac.Equal(Go) prevent timing attacks. Never use==or===.Use the raw request body -- Verify against the raw bytes, not a parsed-and-re-serialized version. JSON re-serialization can change formatting and break the signature.
Enforce timestamp checks -- Rejecting events older than 5 minutes prevents replay attacks.
