Docs academy · API reference

API reference

A REST API at https://api.nexobots.app. Requests and responses use JSON. The machine-readable version is at /v1/openapi.json.

For JavaScript and Python, use the official SDKs: npm install @nexobots/sdk or pip install nexobots. These examples use plain HTTP, which works from any language.

Authentication

Some endpoints need your bot's API token, which you can generate in your dashboard. Send it in the Authorization header, with or without a Bearer prefix:

Authorization: nexo_your_token_here

We show a token once and store only a hash of it. Generating a new one invalidates the old one. A token only works for its own bot: :id in the path must be that bot's client ID, otherwise you get a 403.

Endpoints

MethodEndpointPurposeRate limitAuth
GET/v1/bots/:idPublic bot info60 / min per IPNone
POST/v1/bots/:id/statsPost server and shard count1 / 5 min per botToken
POST/v1/bots/:id/commandsReplace the bot's command list1 / min per botToken
POST/v1/bots/:id/heartbeatSay the bot is online, every 5 minutes1 / min per botToken
GET/v1/bots/:id/votes/check?userId=Has this user voted in the last 12 hours?120 / min per tokenToken
GET/v1/bots/:id/votesLast 1,000 voters10 / min per tokenToken
GET/v1/bots/search?q=&tag=&sort=Search, for integrations30 / min per IPNone
GET/v1/users/:idPublic profile60 / min per IPNone
GET/v1/widget/:id.svgEmbeddable badge (votes, servers)Cached at the edge for 10 minNone

In these paths, :id for a bot is its Discord client ID, and for a user it's their Discord ID.

Post stats

The body takes server_count (required) and shard_count (optional). Unknown fields are rejected. You can post once every 5 minutes, so every 30 minutes is a sensible schedule.

Node.js

const res = await fetch(`https://api.nexobots.app/v1/bots/${CLIENT_ID}/stats`, {
  method: "POST",
  headers: {
    Authorization: process.env.NEXOBOTS_TOKEN,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ server_count: 1200, shard_count: 2 }),
});
console.log(res.status);

Python

import os

import requests

res = requests.post(
    f"https://api.nexobots.app/v1/bots/{CLIENT_ID}/stats",
    headers={"Authorization": os.environ["NEXOBOTS_TOKEN"]},
    json={"server_count": 1200, "shard_count": 2},
    timeout=10,
)
print(res.status_code)

Post commands

Send your commands when your bot starts, and the Commands tab on your page stays up to date. The body is a list of up to 200 commands with name, and optionally description and usage. What Discord's API returns for your application's commands works too, so you can pass that along as it is. It replaces the whole list, and an empty list is refused, so a bot that hasn't loaded its commands yet can't wipe yours.

await nexobots.postCommands(client.application.commands.cache.map((command) => command.toJSON()));

Check a vote

GET /v1/bots/:id/votes/check?userId= answers with voted set to 1 or 0, and voted_at as a time or null:

{ "voted": 1, "voted_at": "2026-11-04T10:15:00.000Z" }

A user can vote for a bot once every 12 hours.

Search

Query parameters for /v1/bots/search are q, tag, sort (votes, servers or new), page and pageSize (up to 100).

Badge

Embed a live badge showing your monthly votes and server count:

[![Nexobots](https://api.nexobots.app/v1/widget/YOUR_CLIENT_ID.svg)](https://nexobots.app/bots/your-bot)

Vote webhooks

When someone votes, we send a POST to the URL you saved in your dashboard. The URL must use https and point to a public address. Votes that aren't counted, such as those flagged by our fraud checks or from very new accounts, don't trigger a webhook.

{
  "bot": "400000000000000001",
  "user": "300000000000000001",
  "type": "upvote",
  "isWeekend": false,
  "streak": 7
}
botYour bot's client ID
userThe voter's Discord ID
typeupvote, or test from the dashboard's test button
isWeekendWhether it's Saturday or Sunday (UTC)
streakHow many days in a row this person has voted for your bot. Missing on test deliveries
Webhooks send you a voter's Discord ID, which is personal data, so they fall under our Data Processing Agreement.

Verify the signature

Every delivery has an X-Nexo-Signature header: the HMAC-SHA256 of the raw request body, as hex, using your webhook secret. We show the secret once, whenever you save a webhook URL. Always check the signature, or anyone could fake a vote by calling your URL.

Node.js

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

// rawBody must be the exact bytes received, before any JSON parsing.
function isValidSignature(rawBody, signatureHeader, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest();
  const given = Buffer.from(String(signatureHeader ?? ""), "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

Python

import hashlib
import hmac


# raw_body must be the exact bytes received, before any JSON parsing.
def is_valid_signature(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, signature_header or "")

Retries

Any 2xx response counts as delivered. Anything else is a failure, including a timeout after 10 seconds or a redirect, which we don't follow. Failed deliveries are retried on this schedule:

First tryImmediately
Retry 110 seconds later
Retry 21 minute later
Retry 36 minutes later
Retry 430 minutes later
Retry 53 hours later

Retries mean you can occasionally receive the same vote twice, so make your handler safe to run twice. After 50 failures in a row we switch the webhook off and send you a DM. Saving it again in your dashboard turns it back on. The dashboard shows a delivery log, and we keep deliveries for 30 days.

Errors

Errors use the matching HTTP status and this shape:

{
  "error": "rate_limited",
  "message": "Too many requests. Try again in 5 minutes."
}
400The input isn't valid
401Token missing or not valid
403Token belongs to a different bot
404Not found, or the bot isn't publicly listed
429Rate limited. Wait and try again.
Still stuck? Open a ticket and a mentor will take a look.