API Reference
Every public developer endpoint. Base URL: https://api.discordium.org/api/v1. Authentication is always Authorization: Bearer <api token>. A machine-readable OpenAPI reference is at https://api.discordium.org/api/docs.
Report bot statistics
POST/bots/:id/stats
Scope: stats:write. The token must belong to the bot named by :id. Limit: 1/minute per bot. Each request is a complete snapshot: omitted optional fields are cleared, not preserved.
{
"serverCount": 12482,
"shardCount": 12
}serverCount: required integer, 0–10,000,000. shardCount: optional integer, 0–10,000. Answers 200 with { serverCount, shardCount, updatedAt }. Full guide: Statistics.
Check a vote
GET/webhooks/votes/check?userId=<discord-user-id>
Scope: votes:read, bot tokens only. Answers { voted, votedAt, nextVoteAt }. voted is true only while the vote is inside the active 12-hour window; otherwise all three are false/null.
POST/webhooks/votes/check
Batch form: body { "userIds": ["…"] }, 1–50 Discord IDs, answers { "votes": { "<userId>": { voted, votedAt, nextVoteAt } } }. Limit for both forms: 120 checked users/minute per bot (single = 1, batch = unique IDs). Full guide: Vote Checking.
Read your own vote data
GET/webhooks/votes?startDate=&endDate=&limit=
Scope: votes:read. Your listing's recent votes, newest first (default 50, max 100 per request; both date filters optional). Each row: { id, userId, username, discriminator, avatar, votedAt, metadata }. discriminator is always "" (Discord retired discriminators; the field remains for compatibility) and metadata is always {}.
GET/webhooks/stats
Scope: votes:read. Aggregates for your listing: { dailyVotes (30 days), totalVotes, totalUniqueVoters, lastVote, topVoters }.
Manage the vote webhook from code
The dashboard (Bot → Integrations) is the recommended way to configure webhooks, but the same operations exist as API endpoints for programmatic setups:
POST/webhooks/register
Scope: webhooks:write (tokens holding only the older votes:read continue to work). Body: { "webhookUrl": "https://…", "secret": "…", "events": ["vote.created"], "isActive": true }. Creates or replaces your listing's webhook. This is a complete statement, so omitting secret clears a stored one. Re-registering also clears an auto-disable.
POST/webhooks/test
Same scope. Body: { "webhookUrl": "https://…" }. Sends one test delivery (test: true) through the production delivery path and reports the result.
Rate limits
| Endpoint | Type | Required | Description |
|---|---|---|---|
| POST /bots/:id/stats | 1/min | - | Per bot. Faster reports answer 429 and change nothing. |
| votes/check (both forms) | 120 users/min | - | Per bot; single check = 1, batch = unique IDs. Use batches for volume. |
| everything | per-IP | - | A general per-IP request limit also applies across the whole API. |
Limits are keyed to your listing, not your token, so regenerating a token never resets a window. A refused request answers 429 with a Retry-After header saying how many seconds to wait.
Errors
Every error body carries a stable, machine-readable code alongside the human-readable message. Branch on code: message may be reworded at any time, so never parse it.
{
"statusCode": 429,
"error": "HttpException",
"code": "RATE_LIMITED",
"message": "Vote-check rate limit exceeded (120 checked users per minute); retry in 42s",
"retryAfterSeconds": 42,
"path": "/api/v1/webhooks/votes/check",
"timestamp": "2026-08-24T12:00:00.000Z"
}| Code | Type | Required | Description |
|---|---|---|---|
| VALIDATION_FAILED | 400 | - | A body field or parameter is malformed or outside its documented range. |
| WEBHOOK_URL_INVALID | 400 | - | The webhook target is not a valid http(s) URL. |
| WEBHOOK_URL_NOT_PUBLIC | 400 | - | The webhook target points at a private network or internal host. |
| INVALID_TOKEN | 401 | - | No token, or a token the API does not recognize. |
| MISSING_SCOPE | 401 | - | A valid token that lacks the scope this endpoint requires. |
| FORBIDDEN | 403 | - | A valid credential for the wrong thing: another bot's ID, or a non-bot token. |
| NOT_FOUND | 404 | - | The listing does not exist. |
| RATE_LIMITED | 429 | - | Over a rate limit. Comes with a Retry-After header and retryAfterSeconds in the body. |
| INTERNAL_ERROR | 500 | - | Something went wrong on Discordium's side. Safe to retry with backoff. |
429 responses additionally carry a Retry-After header (seconds) and the same number as retryAfterSeconds in the body. Wait that long, then retry.