discord-botlists.

discord-botlists docs · archived reader

Core Concepts

Webhook security

How the server rejects fake votes, floods and brute force.

Anyone who discovers your webhook URL could POST fake votes. The server defends against that with secure defaults: secrets are required, per-IP rate limiting and brute-force lockout are always on, and HMAC payload signing is supported per list.

security options

const lists = new Botlists({
  webhook: {
    port: 8080,
    secret: {
      'top.gg': 'shared-secret-for-topgg',
      'botlist.me': 'another-secret',
    },
    security: {
      // HMAC-SHA256 signing keys per list. when present, requests from that
      // list must carry a valid signature in x-signature-256 /
      // x-hub-signature-256 / x-signature.
      hmac: { 'top.gg': 'webhook-signing-key' },

      // per ip rate limit (default 30/min). extra requests get 429.
      rateLimit: { max: 30, windowMs: 60_000 },

      // ban an ip after N consecutive auth failures (default 10),
      // for M ms (default 15 minutes). banned ips get 403.
      banAfterFailures: 10,
      banDurationMs: 15 * 60_000,

      // only these lists may POST at all. others get 403.
      allowedLists: ['top.gg', 'botlist.me', 'discordbotlist.com'],

      // set true behind nginx/cloudflare so rate limits use the real ip.
      trustProxy: true,

      // only for local testing: accept unsigned posts.
      requireSecret: false,
    },
  },
});
ThreatDefenseResponse
Fake votes (no secret)Secret required on every POST401
Replayed/forged payloadstop.gg v1 signature or HMAC-SHA256 check401
Captured delivery replayed laterv1 timestamp window (10 min)401
Secret brute forceFailure counter per ip403 ban after 10
Request floodsPer ip token bucket429 + Retry-After
Giant payloads512 KB body limit413
Unlisted sourcesallowedLists check403

top.gg v1 signed deliveries

top.gg migrated its webhooks: instead of the shared password in the Authorization header, every v1 delivery carries `x-topgg-signature: t=<unix seconds>,v1=<hex>` where v1 is HMAC-SHA256 of `<t>.<rawBody>` keyed with your whs_-prefixed webhook secret. The SDK verifies this scheme automatically whenever the `top.gg` secret is configured - keep using the same secret, nothing else changes. The legacy Authorization header (and the other lists’ signature headers) keep working alongside it.

verify a v1 delivery inside your own framework route

// With headers passed, ingest() enforces transport auth itself and returns
// null on a bad signature. Without headers it trusts your framework's auth
// and only parses.
const parsed = lists.webhook.ingest('top.gg', req.body, {
  headers: { 'x-topgg-signature': req.headers['x-topgg-signature'] },
});
if (!parsed) return res.status(401).end();

The v1 payload is wrapped ({"vote":{"userId":..,"botId":..,"type":"vote"|"test"}}). The SDK flattens it transparently: your `vote` event still receives voterId, botId and a `raw` field holding the original enveloped body. Dashboard test deliveries (type "test") arrive on the `test` event with isTest: true.

NoteThe server throws at start() when requireSecret is true (the default) and no secret was configured. This is intentional: an open webhook endpoint will receive fake votes within hours of going public.