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.
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_hereWe 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
| Method | Endpoint | Purpose | Rate limit | Auth |
|---|---|---|---|---|
| GET | /v1/bots/:id | Public bot info | 60 / min per IP | None |
| POST | /v1/bots/:id/stats | Post server and shard count | 1 / 5 min per bot | Token |
| POST | /v1/bots/:id/commands | Replace the bot's command list | 1 / min per bot | Token |
| POST | /v1/bots/:id/heartbeat | Say the bot is online, every 5 minutes | 1 / min per bot | Token |
| GET | /v1/bots/:id/votes/check?userId= | Has this user voted in the last 12 hours? | 120 / min per token | Token |
| GET | /v1/bots/:id/votes | Last 1,000 voters | 10 / min per token | Token |
| GET | /v1/bots/search?q=&tag=&sort= | Search, for integrations | 30 / min per IP | None |
| GET | /v1/users/:id | Public profile | 60 / min per IP | None |
| GET | /v1/widget/:id.svg | Embeddable badge (votes, servers) | Cached at the edge for 10 min | None |
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:
[](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
}bot | Your bot's client ID |
user | The voter's Discord ID |
type | upvote, or test from the dashboard's test button |
isWeekend | Whether it's Saturday or Sunday (UTC) |
streak | How many days in a row this person has voted for your bot. Missing on test deliveries |
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 try | Immediately |
| Retry 1 | 10 seconds later |
| Retry 2 | 1 minute later |
| Retry 3 | 6 minutes later |
| Retry 4 | 30 minutes later |
| Retry 5 | 3 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."
}400 | The input isn't valid |
401 | Token missing or not valid |
403 | Token belongs to a different bot |
404 | Not found, or the bot isn't publicly listed |
429 | Rate limited. Wait and try again. |