Open this on a computer

The Sample Bot API site is made for a computer, where you write code. Visit api.samplebot.app on your computer.

Back to samplebot.app

Sample Bot API

Webhooks

Instead of asking the API again and again whether something happened, let Sample Bot tell you.

Setting one up

A server admin adds webhooks at config.samplebot.app, under API: a name, an address, and the events to send. Up to 5 per server. Webhooks do not need an application or a key: a server can send its events to its own website or straight to a Discord channel.

What arrives

A POST with a JSON body:

{
  "id": "evt_8Qm2c1XbV9...",
  "event": "cad.panic",
  "guildId": "1345183757107200000",
  "createdAt": "2026-11-11T18:00:00.000Z",
  "data": { "unitId": 31, "callsign": "1A-12", "location": "Postal 207" }
}
HeaderValue
X-Sample-EventThe event name
X-Sample-DeliveryThe event ID. The same ID on a retry, so you can ignore repeats.
X-Sample-Signaturet=<unix seconds>,v1=<hex>

Answer with any 2xx within 8 seconds. Do the slow work afterwards.

Checking the signature

Each webhook has a secret (starting whsec_) shown in server setup. v1 is HMAC-SHA256 of t + "." + the raw body with that secret. Check it, and check that t is within 5 minutes, so nobody can send you fake or replayed events.

Node.js

const crypto = require("crypto");

function fromSampleBot(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(parts.t + "." + rawBody).digest("hex");
  const given = Buffer.from(parts.v1 || "");
  return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(expected), given);
}

// Express: read the raw body, not the parsed one.
app.post("/sample", express.raw({ type: "application/json" }), (req, res) => {
  if (!fromSampleBot(req.body.toString(), req.get("X-Sample-Signature"), process.env.SAMPLE_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(req.body);
  res.sendStatus(200);
  if (event.event === "cad.panic") alertSupervisors(event.data);
});

Python

import hmac, hashlib, time

def from_sample_bot(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(secret.encode(), (parts["t"] + ".").encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Discord channels

Paste a Discord webhook link (https://discord.com/api/webhooks/...) and each event arrives as a readable message instead of JSON. Discord messages are not signed, because Discord does not check signatures.

Retries and switching off

If your address is down or answers 5xx, 408 or 429, the event is sent again after about 15 seconds and again after about 2 minutes. Other 4xx answers are not retried. After 20 failed deliveries in a row the webhook switches itself off and server setup says so; switching it back on resets the count. The last 30 deliveries and how each went are listed there too.

Addresses we will not send to

Only https:// on the normal port, and never to private or local networks.