Skip to content

Signature verification

Every delivery includes a Nativpost-Signature header. Verify it before acting on the payload. Requests that fail verification should be discarded, not merely logged.

Nativpost-Signature: t=1747663200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is the Unix timestamp (seconds) when we signed the request.
  • v1 is a hex-encoded HMAC-SHA256 signature.

Multiple v1= pairs may be present during a key rotation. Accept the delivery if any of them verifies.

The value fed into the HMAC is:

<timestamp>.<raw_request_body>

Use the raw bytes of the body exactly as they arrived. Do not decode the JSON and re-serialise. Extra whitespace changes the signature.

import crypto from "node:crypto";
export function verify(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("=", 2)),
);
const timestamp = Number(parts.t);
const signature = parts.v1;
const skewSeconds = Math.abs(Date.now() / 1000 - timestamp);
if (skewSeconds > 300) throw new Error("timestamp outside tolerance");
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const ok = crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(signature, "hex"),
);
if (!ok) throw new Error("bad signature");
}
import hmac, hashlib, time
def verify(raw_body: bytes, signature_header: str, secret: str) -> None:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = int(parts["t"])
signature = parts["v1"]
if abs(time.time() - timestamp) > 300:
raise ValueError("timestamp outside tolerance")
expected = hmac.new(
secret.encode(),
f"{timestamp}.".encode() + raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise ValueError("bad signature")

The recommended tolerance is 5 minutes (300 seconds). This absorbs clock skew while stopping simple replay attacks. Tighten to 60 seconds for higher-stakes integrations.

Call POST /webhooks/{id}/rotate to generate a new secret. Redeploy your verifier with the new value. Old signatures are rejected immediately after rotation, so cut over during a low-traffic window.