Skip to content

API v1

Read bot data, read the votes for your own bot, and report your server count. REST over HTTPS, JSON both ways.

The full specification as OpenAPI 3.1: /api/v1/openapi.json

Authentication

Create keys in your bot dashboard; you see them exactly once. They belong in an environment variable, not in your source. The two read endpoints also work without a key, at a lower limit.

const response = await fetch('https://topbot.gg/api/v1/bots/155149108183695360', {
  headers: { Authorization: `Bearer ${process.env.TOPBOT_TOKEN}` },
});

if (!response.ok) {
  const { error } = await response.json();
  throw new Error(`${error.code}: ${error.message}`);
}

const { data } = await response.json();
console.log(data.name, data.votesTotal);

Limits

Counted per minute and key, or per minute and IP when anonymous. Every response carries the numbers; a 429 adds a Retry-After header.

TierRequests per minute
Without a key20
Tier 1: standard60
Tier 2: raised240
Tier 3: partner1200
  • x-ratelimit-limit
  • x-ratelimit-remaining
  • x-ratelimit-reset

Every key starts on tier 1. If you need more, tell us what for and how much traffic you expect. We will raise the tier for your key. Ask in the support server

Endpoints

GET/api/v1/botsno key needed

List published bots

Parameters

pageintegerquery · optional · default 1
Page number, starting at 1.
limitintegerquery · optional · default 50
How many entries one response returns.Between 1 and 100
sortstringquery · optional · default votes_month
Order of the page. The directory itself defaults to votes this month.Allowed: votes_month, votes_total, servers, new, name
searchstringquery · optional
Substring match on the bot name. For a query a human typed, use `GET /search`.At most 100 characters

Responses

200
A page of bots.
405
This path exists, but not for that method. See `Allow`.
422
Invalid query parameters.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": [
    {
      "id": "155149108183695360",
      "name": "Dyno",
      "slugs": {
        "de": "dyno",
        "en": "dyno"
      },
      "shortDescription": {
        "de": "Automod gegen Spam und Linkwerbung, dazu Warnungen, Timeouts und ein Protokoll.",
        "en": "Automod against spam and link ads, plus warnings, timeouts and an audit log."
      },
      "prefix": "?",
      "pricing": "freemium",
      "iconUrl": "https://bilder.topbot.gg/bots/155149108183695360/icon.webp",
      "bannerUrl": null,
      "inviteUrl": "https://discord.com/oauth2/authorize?client_id=155149108183695360&scope=bot",
      "supportServerUrl": "https://discord.gg/dyno",
      "websiteUrl": "https://dyno.gg",
      "categories": [
        "moderation",
        "security"
      ],
      "tags": [
        "automod",
        "logging"
      ],
      "owners": [
        "155149108183695360"
      ],
      "serverCount": 11768028,
      "shardCount": 512,
      "votesTotal": 905,
      "votesMonth": 84,
      "rating": {
        "average": 3.5,
        "count": 12
      },
      "isVerified": true,
      "publishedAt": "2026-08-10T19:14:15.268Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 39,
    "pages": 1
  }
}
GET/api/v1/bots/{id}no key needed

Get one bot

Parameters

idstringpath · required
Discord application ID of the bot, the number from its invite URL.

Responses

200
The bot.
404
No published bot with that application ID.
405
This path exists, but not for that method. See `Allow`.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": {
    "id": "155149108183695360",
    "name": "Dyno",
    "slugs": {
      "de": "dyno",
      "en": "dyno"
    },
    "shortDescription": {
      "de": "Automod gegen Spam und Linkwerbung, dazu Warnungen, Timeouts und ein Protokoll.",
      "en": "Automod against spam and link ads, plus warnings, timeouts and an audit log."
    },
    "prefix": "?",
    "pricing": "freemium",
    "iconUrl": "https://bilder.topbot.gg/bots/155149108183695360/icon.webp",
    "bannerUrl": null,
    "inviteUrl": "https://discord.com/oauth2/authorize?client_id=155149108183695360&scope=bot",
    "supportServerUrl": "https://discord.gg/dyno",
    "websiteUrl": "https://dyno.gg",
    "categories": [
      "moderation",
      "security"
    ],
    "tags": [
      "automod",
      "logging"
    ],
    "owners": [
      "155149108183695360"
    ],
    "serverCount": 11768028,
    "shardCount": 512,
    "votesTotal": 905,
    "votesMonth": 84,
    "rating": {
      "average": 3.5,
      "count": 12
    },
    "isVerified": true,
    "publishedAt": "2026-08-10T19:14:15.268Z"
  }
}
GET/api/v1/bots/{id}/reviewsno key needed

Published reviews for a bot

The same reviews shown on the public bot page, most helpful first. No key required.

Parameters

idstringpath · required
Discord application ID of the bot, the number from its invite URL.
pageintegerquery · optional · default 1
Page number, starting at 1.
limitintegerquery · optional · default 25
How many reviews one page returns.Between 1 and 100

Responses

200
A page of reviews.
404
No published bot with that application ID.
405
This path exists, but not for that method. See `Allow`.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": [
    {
      "id": "4f6d9a2c-1f0e-4a5e-9b3d-2c8a7e1b5d40",
      "rating": 5,
      "body": "Läuft seit Monaten ohne Ausfall, die Einrichtung war in wenigen Minuten erledigt.",
      "user": "624317230955626507",
      "username": "thespen",
      "helpfulCount": 3,
      "developerReply": {
        "body": "Danke! Die nächste Version bringt Vorlagen für Automod-Regeln.",
        "repliedAt": "2026-09-02T10:12:00.000Z"
      },
      "createdAt": "2026-08-30T18:22:41.000Z",
      "updatedAt": "2026-08-30T18:22:41.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 25,
    "total": 12,
    "pages": 1
  }
}
GET/api/v1/bots/{id}/historyno key needed

Server count history for a bot

Daily server counts, from the same source as the `serverCount` field on the bot: the numbers you report through `POST /bots/{id}/stats` if you report any, otherwise Discord's own count from the App Directory. `source` says which one it is. A bot with neither gets an empty list. One point per day, oldest first, collected nightly. No key required.

Parameters

idstringpath · required
Discord application ID of the bot, the number from its invite URL.
daysintegerquery · optional · default 30
Window in days. Only these four values exist; each one is its own cache entry.Allowed: 7, 30, 90, 365

Responses

200
The history, oldest first.
404
No published bot with that application ID.
405
This path exists, but not for that method. See `Allow`.
422
Invalid query parameters.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": [
    {
      "date": "2026-09-14",
      "servers": 11762004
    },
    {
      "date": "2026-09-15",
      "servers": 11768028
    }
  ],
  "days": 30,
  "source": "self"
}
GET/api/v1/bots/{id}/votes

Recent votes for your bot

Requires the `votes:read` scope. The key must belong to this bot.

Parameters

idstringpath · required
Discord application ID of the bot, the number from its invite URL.
limitintegerquery · optional · default 50
How many entries one response returns.Between 1 and 100
hoursintegerquery · optional
Only votes from the last n hours (max 720).Between 1 and 720

Responses

200
The most recent votes, newest first.
401
Missing or invalid API key.
403
The key lacks the required scope, or belongs to a different bot.
405
This path exists, but not for that method. See `Allow`.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": [
    {
      "user": "624317230955626507",
      "username": "thespen",
      "votedAt": "2026-09-16T07:41:02.104Z",
      "flagged": false
    }
  ]
}
GET/api/v1/bots/{id}/check/{userId}

Has this user voted?

Requires the `votes:read` scope. Use this before handing out a reward. Returns 200 with `voted: false` when they have not, and that is an answer, not an error.

Parameters

idstringpath · required
Discord application ID of the bot, the number from its invite URL.
userIdstringpath · required
Discord user ID.

Responses

200
Vote status within the 12-hour cooldown window.
401
Missing or invalid API key.
403
The key lacks the required scope, or belongs to a different bot.
405
This path exists, but not for that method. See `Allow`.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": {
    "voted": true,
    "votedAt": "2026-09-16T07:41:02.104Z",
    "expiresAt": "2026-09-16T19:41:02.104Z"
  }
}
GET/api/v1/bots/{id}/premium/{userId}

Does this user have premium?

Requires the `premium:read` scope. Use this before unlocking a premium feature. Returns 200 with `active: false` when they do not subscribe, and that is an answer, not an error.

Parameters

idstringpath · required
Discord application ID of the bot, the number from its invite URL.
userIdstringpath · required
Discord user ID.

Responses

200
Premium status, including the tier and the paid-until date.
401
Missing or invalid API key.
403
The key lacks the required scope, or belongs to a different bot.
405
This path exists, but not for that method. See `Allow`.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": {
    "active": true,
    "tierId": "e0b4a1c8-3f6d-4f0a-9f2e-7c1b5d8a4e32",
    "tierName": "Pro",
    "interval": "month",
    "guildId": null,
    "expiresAt": "2026-10-14T09:00:00.000Z",
    "cancelAtPeriodEnd": false
  }
}
GET/api/v1/bots/{id}/premium/guild/{guildId}

Does this server have premium?

Requires the `premium:read` scope. The check for guild-scoped tiers: ask with the server ID instead of the user ID. Returns 200 with `active: false` when it does not subscribe.

Parameters

idstringpath · required
Discord application ID of the bot, the number from its invite URL.
guildIdstringpath · required
Discord server (guild) ID.

Responses

200
Premium status of the server, including the paid-until date.
401
Missing or invalid API key.
403
The key lacks the required scope, or belongs to a different bot.
405
This path exists, but not for that method. See `Allow`.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": {
    "active": true,
    "tierId": "a1c2e3f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
    "tierName": "Server Pro",
    "interval": "month",
    "guildId": "1538175049536901261",
    "expiresAt": "2026-10-14T09:00:00.000Z",
    "cancelAtPeriodEnd": false
  }
}
POST/api/v1/bots/{id}/stats

Report your server count

Requires the `stats:write` scope. Call this when your shard counts change; once every few minutes is plenty.

Parameters

idstringpath · required
Discord application ID of the bot, the number from its invite URL.

Body

serverCount
integer
required · ≥ 0
shardCount
integer
optional · ≥ 1
{
  "serverCount": 6520,
  "shardCount": 4
}

Responses

200
The stored values.
401
Missing or invalid API key.
403
The key lacks the required scope, or belongs to a different bot.
405
This path exists, but not for that method. See `Allow`.
422
Invalid body.
429
Rate limit exceeded. Wait the seconds given in `Retry-After`.
500
Something broke on our side. Retrying later is reasonable.
Example response
{
  "data": {
    "serverCount": 6520,
    "shardCount": 4
  }
}

Errors

Every failure has the same shape. `code` is stable and machine-readable, `message` is English prose for whoever reads the log. Only `validation_failed` adds `details`, one entry per rejected field.

{
  "error": {
    "code": "validation_failed",
    "message": "One or more query parameters are invalid.",
    "details": [
      {
        "field": "limit",
        "message": "Too big: expected number to be <=100"
      }
    ]
  }
}
CodeStatusWhen
unauthorized401The key is missing, invalid or revoked.
forbidden403The key lacks the required scope, or belongs to a different bot.
not_found404No published bot with that application ID.
method_not_allowed405The path exists, the method does not. `Allow` lists the ones it answers.
validation_failed422Parameters or body are invalid. `details` names the field and the reason.
rate_limited429Too many requests in the current window.
internal_error500Something broke on our side. Retrying later is reasonable.

Every endpoint can answer 405, 429 and 500, including the ones that look like they cannot fail. On a 429, `Retry-After` says how many seconds to wait.

Webhooks

We call your URL as soon as someone votes for your bot, a premium subscription starts or ends (event: premium), or someone supports the bot (event: donation). Verify the signature before handing out a reward or unlocking anything; otherwise anyone who knows your URL can reward themselves.

import { createHmac, timingSafeEqual } from 'node:crypto';

app.post('/topbot', express.raw({ type: 'application/json' }), (request, response) => {
  const timestamp = request.header('x-topbot-timestamp');
  const signature = request.header('x-topbot-signature');
  const body = request.body.toString('utf8');

  // Reject replayed calls.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
    return response.sendStatus(400);
  }

  const expected = createHmac('sha256', process.env.TOPBOT_WEBHOOK_SECRET)
    .update(`${timestamp}.${body}`)
    .digest('hex');

  // Compare in constant time.
  if (
    signature.length !== expected.length ||
    !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  ) {
    return response.sendStatus(401);
  }

  const payload = JSON.parse(body);
  if (payload.event === 'vote' && !payload.flagged) {
    grantReward(payload.user);
  }

  response.sendStatus(200);
});

Answer with any 2xx. On 5xx and 429 we retry six times with growing delays (10s, 40s, 90s, 160s, 250s); on other 4xx we do not retry. After 20 finally failed deliveries we switch the webhook off and let you know.

Badges

An SVG you embed in your README or on your site. It follows the viewer’s system setting and is cached for five minutes. Use theme=dark or theme=light to pin one version, compact=1 to drop the label.

votesserversrating
[![Topbot.gg](https://topbot.gg/api/badge/155149108183695360?variant=votes)](https://topbot.gg/en/bots/YOUR-SLUG)

Widget

The embeddable bot card: icon, numbers and a vote button, live from the directory. Parameters: theme=dark|light and lang=de|en. Ready-made code with your application ID is in your dashboard.

<iframe src="https://topbot.gg/api/widget/155149108183695360?theme=dark&lang=en"
  width="420" height="140" style="border:0"
  title="Topbot.gg" loading="lazy"></iframe>

Server history

Your bot's server count over time as an embeddable image, captured daily. It comes from the same source as the serverCount field: your own reports, otherwise Discord's count from the App Directory. Parameters: range=7|30|90|365 and theme=auto|dark|light. The raw data comes from GET /bots/{id}/history, where the source field names the origin.

Server history
[![Topbot.gg](https://topbot.gg/api/chart/155149108183695360?range=30)](https://topbot.gg/en/bots/YOUR-SLUG)