For bot developers

DiscordPlace Bots API

Keep your bot's listing up to date and find out who voted for it. One key per bot.

Start in three calls

  1. 1

    Get a key

    Open your bot's page on DiscordPlace, tab SDK, and generate its key. One key per bot. It is shown once: keep it in an environment variable, never in your repository.

  2. 2

    Check it

    Call GET /bot. It changes nothing and tells you the key works, whether your bot is published, and what we last received from it.

  3. 3

    Send your stats every 5 minutes

    POST /bot/stats on a timer, not once at startup: the "online" dot on your page is the freshness of these calls.

curl "https://discordplace.com/api/v1/bot" \
  -H "Authorization: Bearer $DISCORDPLACE_API_KEY"

Authentication

Calls that act for your bot carry its key in the Authorization header. The bot is always the one the key belongs to: there is no bot ID in the address, so a key can never read or write another bot.

HTTP
Authorization: Bearer <DISCORDPLACE_API_KEY>

The API key of your bot, generated in the SDK tab of its page on discordplace.com.

  • A regenerated key stops working at once. If your bot keeps sending the old one, its SDK tab says so.
  • Reading a public listing needs no key, and works from a browser on any site.
  • No cookie is ever set by this API.

Endpoints

https://discordplace.com/api/v1

GET/bot

Check your key

Returns the bot this key belongs to, whether it is published, and the last sync DiscordPlace received. Changes nothing: call it first to make sure your key works.

Bot key600 per minute, per key

Request

curl "https://discordplace.com/api/v1/bot" \
  -H "Authorization: Bearer $DISCORDPLACE_API_KEY"
Response · 200
{
  "id": "1234567890123456789",
  "name": "Example Bot",
  "status": "published",
  "online": true,
  "lastSyncAt": "2026-10-13T09:55:00.000Z",
  "servers": 1250,
  "votes": {
    "day": 3,
    "week": 12,
    "month": 41,
    "total": 230
  },
  "rating": {
    "average": 4.7,
    "count": 9
  },
  "url": "https://discordplace.com/bots/1234567890123456789",
  "sdk": {
    "latest": "1.9.1"
  }
}
  • status is "pending" while your bot waits for approval: votes, rating and url are then null.
  • online is true while your syncs keep arriving at their usual pace. It is not read from Discord.

POST/bot/stats

Send your stats (heartbeat)

Updates the numbers on your listing and counts as a heartbeat: the "online" dot on your page depends on these calls arriving regularly. Call it every 5 minutes, not once at startup. Every field is optional; a field you leave out keeps its value.

Bot key60 per minute, per key

Body

guildCountinteger
Servers the bot is in.
userCountinteger
Sum of the member counts of those servers. Shown as "members across its servers", never as a user count.
latencynumber
Gateway ping in milliseconds.
shardCountinteger
Number of shards.
uptimeinteger
Process uptime in milliseconds.
versionstring
Your bot's own version string (50 characters at most).

Request

curl -X POST "https://discordplace.com/api/v1/bot/stats" \
  -H "Authorization: Bearer $DISCORDPLACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"guildCount":1250,"userCount":310000,"latency":84,"shardCount":2}'
Response · 200
{
  "success": true,
  "message": "Statistiques mises à jour",
  "data": {
    "id": "1234567890123456789",
    "guildCount": 1250,
    "isOnline": true,
    "lastStatsUpdate": "2026-10-13T10:00:00.000Z"
  }
}

POST/bot/sync

Send your commands and profile

Everything /bot/stats takes, plus your name, avatar and the full list of your commands, which replaces the one on your listing. Send the whole list each time, at most 200 slash and 200 prefix commands, 1 MB per request. Leave out commands only your team can use.

Bot key60 per minute, per key

Body

namestring
Your bot's current username.
avatarUrlstring
Your bot's avatar on cdn.discordapp.com. Any other address is ignored.
slashCommandsarray
Your slash commands and context menus.
prefixCommandsarray
Your prefix commands: name and description only. Do not include the prefix in the name.
guildCountinteger
Servers the bot is in.
userCountinteger
Sum of the member counts of those servers. Shown as "members across its servers", never as a user count.
latencynumber
Gateway ping in milliseconds.
shardCountinteger
Number of shards.
uptimeinteger
Process uptime in milliseconds.
versionstring
Your bot's own version string (50 characters at most).

Request

curl -X POST "https://discordplace.com/api/v1/bot/sync" \
  -H "Authorization: Bearer $DISCORDPLACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"Example Bot","guildCount":1250,"slashCommands":[{"name":"ban","description":"Ban a member","options":[{"name":"member","description":"Who","type":"USER","required":true}]},{"name":"ticket","description":"Open a support ticket"}],"prefixCommands":[{"name":"help","description":"List commands"}]}'
Response · 200
{
  "success": true,
  "message": "Informations synchronisées avec succès",
  "data": {
    "id": "1234567890123456789",
    "name": "Example Bot",
    "guildCount": 1250,
    "slashCommandCount": 2,
    "prefixCommandCount": 1,
    "isOnline": true,
    "lastStatsUpdate": "2026-10-13T10:00:00.000Z"
  }
}

GET/bot/votes/{userId}

Has this user voted for your bot?

True while the user's last vote is less than 12 hours old. Use it to reward voters: check when they run your reward command, no public server needed. A user who never voted returns voted: false, not an error.

Bot key600 per minute, per key

Parameters

userId
Discord ID of the user.

Request

curl "https://discordplace.com/api/v1/bot/votes/9876543210987654321" \
  -H "Authorization: Bearer $DISCORDPLACE_API_KEY"
Response · 200
{
  "success": true,
  "voted": true,
  "votedAt": "2026-10-13T08:12:00.000Z",
  "expiresAt": "2026-10-13T20:12:00.000Z"
}
  • The bot is always the one your key belongs to: you cannot read another bot's votes.
  • expiresAt is when the user can vote again. A pending bot gets 403 bot_not_approved.

POST/bot/usage

Report usage (optional)

Counters accumulated since your last report: commands run, interactions left unanswered, servers gained and lost, daily active users. They feed the "Activity and usage" section of your listing, marked as declared by the bot. Aggregates only: never send a user or server ID. Send a new batchId each time; resending the same one is ignored, so a retry is safe.

Bot key60 per minute, per key

Body

batchIdrequiredstring
Unique ID of this report, 8 to 64 characters (letters, digits, _ and -).
daystring
UTC day the counters belong to, YYYY-MM-DD. Today or yesterday only. Defaults to today.
usageobject
interactions, commands, components, modals, autocompletes, unanswered, guildsAdded, guildsRemoved, activeUsers, and commandCounts (a map of command name to count).
telemetryobject
Optional: library, runtime, guildLocales and guildSizes (maps of counts).

Request

curl -X POST "https://discordplace.com/api/v1/bot/usage" \
  -H "Authorization: Bearer $DISCORDPLACE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"batchId":"a3f9c2e17b6d4f08","usage":{"interactions":420,"commands":310,"unanswered":4,"guildsAdded":3,"guildsRemoved":1,"activeUsers":96,"commandCounts":{"ban":40,"ticket":120}}}'
Response · 200
{
  "success": true,
  "duplicate": false
}

GET/bots/{id}

Public listing of a bot

What anyone can see about a published bot: votes, rating, servers. No key needed, readable from a browser on any site. Use it to show your DiscordPlace numbers on your own site or dashboard.

No key200 per minute, per IP address

Parameters

id
Discord application ID of the bot.

Request

curl "https://discordplace.com/api/v1/bots/1234567890123456789"
Response · 200
{
  "id": "1234567890123456789",
  "name": "Example Bot",
  "summary": "Moderation and tickets for small communities.",
  "avatarUrl": "https://cdn.discordapp.com/avatars/1234567890123456789/a1b2c3.webp?size=256",
  "tags": [
    "MODERATION",
    "TICKETS"
  ],
  "verified": false,
  "online": true,
  "servers": 1250,
  "commands": 42,
  "votes": {
    "day": 3,
    "week": 12,
    "month": 41,
    "total": 230
  },
  "rating": {
    "average": 4.7,
    "count": 9
  },
  "listedAt": "2026-03-23T10:00:00.000Z",
  "url": "https://discordplace.com/bots/1234567890123456789",
  "widgetUrl": "https://discordplace.com/api/widget/bot/1234567890123456789"
}
  • servers, commands and rating are null when they were never measured. Show nothing rather than 0.
  • votes.day, week and month are rolling windows (24 hours, 7 and 30 days). Responses are cached for 60 seconds.

Vote webhook

A user voted for your bot

If you set a webhook address in the SDK tab, DiscordPlace sends this to it at every vote. It needs an address reachable from the internet; if your bot has none, use GET /bot/votes/{userId} instead. Answer 2xx within 5 seconds, otherwise the delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. Each vote has a unique id: ignore one you already handled.

What you receive

POST · application/json
{
  "type": "vote",
  "id": "cmg1x2y3z0001",
  "botId": "1234567890123456789",
  "userId": "9876543210987654321",
  "votedAt": "2026-10-13T08:12:00.000Z",
  "nextVoteAt": "2026-10-13T20:12:00.000Z"
}
X-DiscordPlace-Signature
t is a Unix timestamp, v1 is HMAC-SHA256(secret, "<t>.<raw body>") in hex. Compute it on the raw body, before any JSON parsing, and reject a timestamp older than 5 minutes.

Verify the signature

JavaScript
import crypto from 'node:crypto';

// rawBody: the exact bytes you received, before any JSON.parse
export function verify(secret, rawBody, header) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || '');
  if (!m) return false;
  if (Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false; // replay
  const expected = crypto.createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest();
  const received = Buffer.from(m[2], 'hex');
  return received.length === expected.length && crypto.timingSafeEqual(received, expected);
}
Python
import hashlib, hmac, re, time

# raw_body: the exact bytes you received, before any json.loads
def verify(secret: str, raw_body: bytes, header: str) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
    if not m:
        return False
    if abs(time.time() - int(m.group(1))) > 300:  # replay
        return False
    signed = m.group(1).encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, m.group(2))
  • The "Send a test" button of the SDK tab sends the same body with type: "test".

Errors

Every refusal is JSON with success: false and an error field. Branch on the HTTP status.

400
The body or a parameter is not valid.
401
No key, or a key that is unknown or was regenerated. The SDK tab of your bot tells you if your bot is still sending an old key.
403
Not available to a bot that is still waiting for approval.
404
Unknown bot, or a bot that is not published.
413
Body larger than 1 MB.
429
Too many requests. Wait for the number of seconds in the Retry-After header.
500
Our fault. Retry later; nothing was written.

Rate limits

  • Limits are per minute. Calls made with a key are counted per key, so two bots on the same host do not share a budget.
  • On top of that, one IP address cannot send more than 100 writes per minute in total.
  • One sync every 5 minutes is the expected pace. More often changes nothing on your listing.

Prefer a library?

The official packages make these same calls for you, on a timer, and collect your commands automatically.

npm
npm install discordplace-sync
PyPI
pip install discordplace-sync

The paths under /api/bots/sdk used by installed SDKs keep working and are the same handlers: nothing to migrate.

← Bots