Webhook
Receive the same events as SSE via HTTP POST to your server, signed with HMAC-SHA256.
Registering an endpoint
- Developer Portal → Webhooks → Add subscription: a public HTTPS URL (port 443) and the subscribed events.
- The secret is shown once; store it in your server environment.
- A 2xx response = accepted. Failure → retried up to 8 times with backoff; after that the status is
dead. Redirects are not followed.
Verifying the signature
Headers: x-webhook-id (event ID, for deduplication), x-webhook-timestamp (Unix seconds), x-webhook-signature (v1=<hex>). Signature = HMAC-SHA256 with the secret over timestamp + "." + raw body. Compare in constant time and reject timestamps outside ±5 minutes.
ts
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const app = express();
app.post('/banipool-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.header('x-webhook-timestamp') ?? '';
const sig = (req.header('x-webhook-signature') ?? '').replace(/^v1=/, '');
const expected = createHmac('sha256', process.env.BANIPOOL_WEBHOOK_SECRET!)
.update(`${ts}.${req.body.toString('utf8')}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
const ok = fresh && sig.length === expected.length && timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
if (!ok) return res.status(401).end();
const event = JSON.parse(req.body.toString('utf8')); // { id, type, data, createdAt }
// Deduplikasi: simpan event.id yang sudah diproses
console.log(event.type, event.data);
res.status(204).end();
});
app.listen(3000);python
import hmac, hashlib, os, time, json
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["BANIPOOL_WEBHOOK_SECRET"].encode()
@app.post("/banipool-webhook")
def webhook():
ts = request.headers.get("x-webhook-timestamp", "")
sig = request.headers.get("x-webhook-signature", "").removeprefix("v1=")
raw = request.get_data()
expected = hmac.new(SECRET, f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
if abs(time.time() - int(ts or 0)) > 300 or not hmac.compare_digest(sig, expected):
abort(401)
event = json.loads(raw) # {id, type, data, createdAt}
print(event["type"], event["data"])
return "", 204