> ## Documentation Index
> Fetch the complete documentation index at: https://docs.discordbotlist.lol/llms.txt
> Use this file to discover all available pages before exploring further.

# Official libraries

> A JavaScript and a Python client, so the whole integration is four lines instead of forty.

Everything on this site is plain HTTP and you never have to install anything. The
libraries exist because four things — the autoposter's interval, the retry on a
429, the constant-time signature check, and hashing the raw body rather than a
re-serialised one — are the four things hand-written integrations get wrong, and
each of them costs something: a rate limit, a missed vote, or a stranger claiming
rewards.

<CardGroup cols={2}>
  <Card title="JavaScript" icon="js" href="https://www.npmjs.com/package/discordbotlist-sdk">
    `npm install discordbotlist-sdk` — no dependencies, Node 18+.
  </Card>

  <Card title="Python" icon="python" href="https://pypi.org/project/discordbotlist/">
    `pip install discordbotlist` — async, no hard dependencies.
  </Card>
</CardGroup>

<Warning>
  On npm the package is **`discordbotlist-sdk`**, not `discordbotlist`. That name
  belongs to a different site (discordbotlist\*\*.com\*\*) and installing it here
  gets you a client for somebody else's API.
</Warning>

## The whole integration

<CodeGroup>
  ```js discord.js theme={null}
  import { Client, Events } from 'discord.js';
  import { AutoPoster, DiscordBotListClient } from 'discordbotlist-sdk';

  const client = new Client({ intents: [] });

  const dbl = new DiscordBotListClient({
    token: process.env.DBL_TOKEN,
    botId: process.env.BOT_ID,
  });

  client.once(Events.ClientReady, () => {
    new AutoPoster(dbl, { serverCount: () => client.guilds.cache.size }).start();
  });
  ```

  ```python discord.py theme={null}
  import os
  from discord.ext import commands
  from discordbotlist import AutoPoster, DiscordBotListClient

  bot = commands.Bot(command_prefix="!")
  dbl = DiscordBotListClient(os.environ["DBL_TOKEN"], os.environ["BOT_ID"])

  @bot.event
  async def on_ready():
      AutoPoster(dbl, lambda: len(bot.guilds)).start()
  ```
</CodeGroup>

The poster posts once on start and every 30 minutes after that. The floor is five
minutes: the limit is 60 requests a minute and a server count does not move
sixty times a minute.

<Note>
  **Sharded bots post the total**, not one shard's slice. A shard posting its own
  count makes the listing show whichever shard checked in last — the number then
  jumps around and means nothing.
</Note>

## Vote rewards

<CodeGroup>
  ```js JavaScript theme={null}
  const { hasVoted, nextVoteAt } = await dbl.hasVoted(userId);
  ```

  ```python Python theme={null}
  vote = await dbl.has_voted(str(user_id))
  # vote.has_voted, vote.next_vote_at
  ```
</CodeGroup>

`nextVoteAt` / `next_vote_at` is the cooldown's end, so your bot can say when to
come back rather than "you have already voted".

## Verifying a webhook

Both libraries verify and parse in one call, and return nothing at all when the
signature does not match — so there is no boolean to forget.

<CodeGroup>
  ```js Express theme={null}
  import { parseWebhook } from 'discordbotlist-sdk';

  app.post('/webhooks/votes', express.raw({ type: 'application/json' }), (req, res) => {
    const vote = parseWebhook(
      req.body,
      req.header('X-Webhook-Signature-256'),
      process.env.DBL_WEBHOOK_SECRET
    );
    if (!vote) return res.status(401).end();

    grantReward(vote.user_id);
    res.status(204).end();
  });
  ```

  ```python FastAPI theme={null}
  from discordbotlist import parse_webhook

  @app.post("/webhooks/votes")
  async def votes(request: Request):
      vote = parse_webhook(
          await request.body(),
          request.headers.get("x-webhook-signature-256"),
          os.environ["DBL_WEBHOOK_SECRET"],
      )
      if vote is None:
          return Response(status_code=401)

      await grant_reward(vote.user_id)
      return Response(status_code=204)
  ```
</CodeGroup>

Both take the **raw** body. Parsing first and hashing the result is the common
mistake: `JSON.parse` then `JSON.stringify` — or `json.loads` then `json.dumps` —
produces different bytes, and the HMAC of different bytes never matches.

## Errors you can branch on

One class per status, so nothing has to read a message to decide what to do.

| Class             | Status | What to do                                       |
| ----------------- | ------ | ------------------------------------------------ |
| `BadRequestError` | 400    | Fix the body. Retrying sends the same bytes.     |
| `AuthError`       | 401    | Fix the token.                                   |
| `NotFoundError`   | 404    | Wrong bot id, or the token is another listing's. |
| `RateLimitError`  | 429    | Wait `retryAfter` / `retry_after` seconds.       |
| `ServerError`     | 5xx    | Retry with backoff.                              |

Both clients already retry a 429 or a 5xx **once**, waiting the interval the API
asked for.

## Using another library

Nothing here is required. The
[API reference](/api-reference/bot/post-the-server-count) is the contract, and the
field names match what every other bot list uses — so a poster library you
already run (blapi and friends) works by adding one host. The
[autoposting guide](/guides/autoposting) shows that form.
