Subscribe to booking events, verify the HMAC signature in Node and Python, handle retries, and rotate secrets without downtime.
Webhook setup and verification
Subscribe to booking events, verify the HMAC signature in Node and Python, handle retries, and rotate secrets without downtime.
AGNT fires webhooks whenever something interesting happens on the network — a booking is confirmed, cancelled, or refunded. This guide covers the full lifecycle: subscribing, verifying signatures in the two most common runtimes, handling retries, and rotating secrets.
Prerequisites
- A publicly reachable HTTPS endpoint (a tunnel like ngrok is fine for testing).
- Node.js 18+ or Python 3.10+ on the receiver side.
In the developer dashboard, create a new webhook endpoint and paste your URL. Pick the events you care about — booking.confirmed, booking.cancelled, booking.completed, booking.no_show. Each subscription gets its own signing secret, shown exactly once on creation. Store it in your secret manager.
Every delivery carries two headers: X-Agnt-Timestamp (Unix seconds at the time of signing) and X-Agnt-Signature (an HMAC-SHA256 of the exact raw request body, hex-encoded). Verification is a three-step check: compute the HMAC, compare in constant time, and reject anything more than five minutes old to prevent replay.
javascript
import crypto from "node:crypto";
export function verify(rawBody, timestamp, signature, secret) {
const age = Math.floor(Date.now() / 1000) - Number(timestamp);
if (Number.isNaN(age) || age > 300) return false;
const mac = crypto
.createHmac("sha256", secret)
.update(timestamp + "." + rawBody)
.digest("hex");
const a = Buffer.from(mac);
const b = Buffer.from(signature);
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}The two lessons the signature protects: (1) the body was not tampered with in transit, and (2) the request is fresh. Constant-time comparison prevents a timing side channel that would otherwise leak bytes of the HMAC.
python
import hmac, hashlib, time
def verify(raw_body: bytes, timestamp: str, signature: str, secret: str) -> bool:
try:
age = int(time.time()) - int(timestamp)
except ValueError:
return False
if age > 300:
return False
mac = hmac.new(
secret.encode(),
(timestamp + ".").encode() + raw_body,
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(mac, signature)Keep the body as bytes on the way in — decoding to a string will change whitespace or line endings on some frameworks and invalidate the signature. Read the raw body once, verify, then parse JSON.
AGNT retries failed deliveries with exponential backoff: 1s, 5s, 30s, 2m, 10m, 1h, 6h, 24h. Your endpoint should return any 2xx status on success. A 4xx is treated as a permanent failure and moves the delivery to the dead letter queue; a 5xx or timeout is retryable.
Critically, your handler must be idempotent. The same event_id can be delivered more than once if a retry lands at an unlucky moment. Use it as a unique key in your storage and de-duplicate at the application layer.
In the dashboard, click 'Rotate secret' and a new one is generated. AGNT accepts both the old and the new secret for a ten-minute overlap, so you can deploy the new secret to your infra without losing deliveries. After ten minutes the old secret is revoked.
If a secret is leaked, do the rotation immediately and then hit the 'Revoke old' button to kill the overlap window early.
Key terms
Next steps