0Bit public documentation
Verifying signatures
Verify Gate-Signature against the raw webhook request body.
Every delivery includes:
Gate-Signature: t=1716729600,v1=HEX_HMAC_SHA256
X-0bit-Timestamp: 1716729600
X-0bit-Event-Id: EVENT_UUID
X-0bit-Event-Type: gate_session.completed
The signed bytes are:
<timestamp>.<raw request body>
Verification order:
- read the raw body bytes before JSON parsing;
- parse
tandv1fromGate-Signature; - reject malformed values or a timestamp more than 300 seconds from your clock;
- compute
HMAC-SHA256(webhook_secret, timestamp + "." + rawBody)as lowercase hex; - compare the supplied and expected digests in constant time;
- parse JSON and deduplicate by event
id.
Node
import crypto from 'node:crypto';
export function verifyGateWebhook(rawBody, signatureHeader, secret, now = Date.now()) {
const parts = Object.fromEntries(
signatureHeader.split(',').map(part => {
const index = part.indexOf('=');
return [part.slice(0, index).trim(), part.slice(index + 1).trim()];
}),
);
const timestamp = Number(parts.t);
if (!Number.isInteger(timestamp)) return false;
if (Math.abs(Math.floor(now / 1000) - timestamp) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const supplied = Buffer.from(parts.v1 ?? '', 'hex');
const calculated = Buffer.from(expected, 'hex');
return supplied.length === calculated.length && crypto.timingSafeEqual(supplied, calculated);
}
Python
import hashlib
import hmac
import time
def verify_gate_webhook(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
try:
timestamp = int(parts["t"])
supplied = parts["v1"]
except (KeyError, ValueError):
return False
if abs(int(time.time()) - timestamp) > 300:
return False
payload = str(timestamp).encode() + b"." + raw_body
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(supplied, expected)
Do not parse and reserialize JSON before verification. Whitespace and property order are part of the signed body. Rotate the secret through POST /dashboard/webhook-secret/rotate and store the replacement immediately; the response is the only time the raw replacement is returned.
