Webhooks
Verify signatures
Deliveries follow the Standard Webhooks spec. Verify the raw body before parsing it.
Library
js
import { Webhook } from "standardwebhooks";
const event = new Webhook(process.env.THECARGO_WEBHOOK_SECRET).verify(rawBody, headers);python
from standardwebhooks.webhooks import Webhook
event = Webhook(os.environ["THECARGO_WEBHOOK_SECRET"]).verify(raw_body, headers)verify throws on a bad signature or a timestamp older than 5 minutes.
Manual
webhook-signature is v1, + base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}. The key is the
secret after whsec_, base64-decoded; a custom secret without that prefix is used as is.
python
import base64, hashlib, hmac, time
def verify(secret: str, headers: dict, body: bytes) -> bool:
msg_id, ts = headers["webhook-id"], headers["webhook-timestamp"]
if abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret[6:]) if secret.startswith("whsec_") else secret.encode()
expected = base64.b64encode(hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()).decode()
return any(sig.startswith("v1,") and hmac.compare_digest(sig[3:], expected)
for sig in headers["webhook-signature"].split(" "))php
function verify(string $secret, array $headers, string $body): bool
{
$id = $headers['webhook-id'];
$ts = $headers['webhook-timestamp'];
if (abs(time() - (int) $ts) > 300) return false;
$key = str_starts_with($secret, 'whsec_') ? base64_decode(substr($secret, 6)) : $secret;
$expected = base64_encode(hash_hmac('sha256', "$id.$ts.$body", $key, true));
foreach (explode(' ', $headers['webhook-signature']) as $sig) {
if (str_starts_with($sig, 'v1,') && hash_equals($expected, substr($sig, 3))) return true;
}
return false;
}