01 Quick Start
- Generate an External API key in your admin panel.
- Copy your Discord Server ID from Discord.
- Add required environment variables: TDS_API_KEY and TDS_DISCORD_SERVER_ID.
- In your bot command, call POST /api/external/discord-servers/{discordServerId}/vote.
- Send user Discord ID and direction in JSON.
- Send the success or cooldown response back to Discord.
Keep your API key private. Don’t put it in browser code or a public repository.
Environment variables (required):
TDS_API_KEY=your_external_api_key
TDS_DISCORD_SERVER_ID=your-discord-server-id
Examples by runtime:
# Node.js / Python / PHP (dotenv)
TDS_API_KEY=your_external_api_key
TDS_DISCORD_SERVER_ID=your-discord-server-id
# Java (JVM args)
-DTDS_API_KEY=your_external_api_key
-DTDS_DISCORD_SERVER_ID=your-discord-server-id
# C# / Windows PowerShell
$env:TDS_API_KEY="your_external_api_key"
$env:TDS_DISCORD_SERVER_ID="your-discord-server-id"
# Linux/macOS shell
export TDS_API_KEY="your_external_api_key"
export TDS_DISCORD_SERVER_ID="your-discord-server-id"
02 API Contract
Use any of these auth methods:
Authorization: Bearer YOUR_API_KEY X-API-Key: YOUR_API_KEY api_key in request JSON
Payload fields:
{
"discord_user_id": "123456789012345678",
"direction": "up"
}
Also accepted:
{
"user_id": "123456789012345678",
"direction": "upvote"
}
Success response:
{
"message": "Vote recorded.",
"data": {
"discordServerId": "123456789012345678",
"votes": 101,
"appliedDelta": 1,
"direction": "up",
"voterDiscordId": "123456789012345678"
}
}
03 Variables and Errors
What each variable means:
| Variable | Example of the output |
|---|---|
| TDS_API_KEY |
tds_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
External API key generated in admin.
|
| TDS_DISCORD_SERVER_ID |
123456789012345678
Discord server ID (guild ID) for the target server.
|
| discord_user_id |
123456789012345678
Discord ID of the user who ran the command.
|
| direction |
up
Vote direction. Normally use up for positive votes.
|
| user_id / voter_id |
123456789012345678
Accepted aliases for discord_user_id.
|
Allowed direction values:
| Input value | How the API treats it |
|---|---|
| up | Up vote |
| down | Down vote |
| upvote | Normalized to up |
| downvote | Normalized to down |
| 1 | Normalized to up |
| -1 | Normalized to down |
Possible error responses:
| Status | Example of the output |
|---|---|
| 401 Unauthorized |
Missing API key.
{
"message": "API key required."
}
|
| 401 Unauthorized |
Invalid or revoked API key.
{
"message": "Invalid or revoked API key."
}
|
| 404 Not Found |
Discord server ID not found.
{
"message": "Server not found."
}
|
| 403 Forbidden |
Discord user is not registered on the site.
{
"message": "User not registered. Please register and link Discord before voting.",
"registerUrl": "https://topdiscordservers.com/login"
}
|
| 422 Unprocessable Entity |
Validation failed (usually bad discord_user_id).
{
"message": "The given data was invalid.",
"errors": {
"discord_user_id": [
"discord_user_id must be a numeric Discord user ID."
]
}
}
|
| 429 Too Many Requests |
User is on cooldown and cannot vote yet.
{
"message": "Vote limit reached.",
"retryAfterSeconds": 20473,
"retryAfterHuman": "5h 41m",
"voteLimitPerDay": 4,
"cooldownWindow": "6h",
"nextAllowedAt": "2026-08-04T08:43:15+00:00"
}
|
| 503 Service Unavailable |
Temporary backend/API-key storage issue.
{
"message": "External voting is temporarily unavailable."
}
|
Handling tip: if you get 429, show a friendly cooldown message in Discord using retryAfterHuman.
04 Command Examples (Tabbed)
Click a language tab to show that bot command example.
Install:
npm install discord.js node-fetch
const { SlashCommandBuilder, EmbedBuilder } = require('discord.js');
const fetch = require('node-fetch');
const API_KEY = process.env.TDS_API_KEY;
const DISCORD_SERVER_ID = process.env.TDS_DISCORD_SERVER_ID;
module.exports = {
data: new SlashCommandBuilder().setName('vote').setDescription('Vote for our server'),
async execute(interaction) {
const response = await fetch(`https://topdiscordservers.com/api/external/discord-servers/${DISCORD_SERVER_ID}/vote`, {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify({ discord_user_id: interaction.user.id, direction: 'up' }),
});
const data = await response.json();
if (!response.ok) {
const errorEmbed = new EmbedBuilder()
.setTitle('Vote Failed')
.setColor(0xED4245)
.setDescription(data.message || 'Unknown error')
.addFields(
{ name: 'Status', value: String(response.status), inline: true },
{ name: 'Code', value: data.error_code || 'n/a', inline: true }
)
.setTimestamp();
if (response.status === 429 && data.retryAfterHuman) {
errorEmbed.addFields({ name: 'Try Again In', value: data.retryAfterHuman, inline: true });
}
await interaction.reply({ embeds: [errorEmbed], ephemeral: true });
return;
}
await interaction.reply({ content: `Vote recorded. Total votes: ${data.data.votes}` });
},
};
Install:
pip install discord.py aiohttp
import asyncio
import os
from typing import Any
import aiohttp
import discord
from discord import app_commands
from discord.ext import commands
API_BASE_URL = os.getenv("TDS_API_BASE_URL", "https://topdiscordservers.com").rstrip("/")
API_KEY = os.getenv("TDS_API_KEY", "")
DISCORD_SERVER_ID = os.getenv("TDS_DISCORD_SERVER_ID", "")
class Vote(commands.Cog):
def __init__(self, bot: commands.Bot) -> None:
self.bot = bot
@app_commands.command(name="vote", description="Vote for our server")
async def vote(self, interaction: discord.Interaction) -> None:
if not API_KEY or not DISCORD_SERVER_ID:
await interaction.response.send_message(
"Vote integration is not configured yet.",
ephemeral=True,
)
return
endpoint = f"{API_BASE_URL}/api/external/discord-servers/{DISCORD_SERVER_ID}/vote"
payload = {
"discord_user_id": str(interaction.user.id),
"direction": "up",
}
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Accept": "application/json",
}
timeout = aiohttp.ClientTimeout(total=10)
data: dict[str, Any] = {}
status = 0
try:
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.post(endpoint, json=payload, headers=headers) as resp:
status = resp.status
data = await resp.json(content_type=None)
except asyncio.TimeoutError:
await interaction.response.send_message(
"Vote request timed out. Please try again.",
ephemeral=True,
)
return
except aiohttp.ClientError:
await interaction.response.send_message(
"Vote service is unavailable right now. Please try again later.",
ephemeral=True,
)
return
if status >= 400:
message = data.get("message", "Unknown error")
error_embed = discord.Embed(
"Vote Failed",
description=message,
color=discord.Color.red(),
)
error_embed.add_field(name="Status", value=str(status), inline=True)
if status == 429 and data.get("retryAfterHuman"):
error_embed.add_field(name="Try Again In", value=str(data["retryAfterHuman"]), inline=True)
if status == 403 and data.get("registerUrl"):
error_embed.add_field(name="Register", value=str(data["registerUrl"]), inline=False)
await interaction.response.send_message(embed=error_embed, ephemeral=True)
return
votes = data.get("data", {}).get("votes", "?")
await interaction.response.send_message(f"Vote recorded. Total votes: {votes}")
async def setup(bot: commands.Bot) -> None:
await bot.add_cog(Vote(bot))
import net.dv8tion.jda.api.EmbedBuilder;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.awt.Color;
public class VoteCommand {
private static final String API_KEY = System.getenv("TDS_API_KEY");
private static final String DISCORD_SERVER_ID = System.getenv("TDS_DISCORD_SERVER_ID");
public static void handle(SlashCommandInteractionEvent event) throws Exception {
String url = "https://topdiscordservers.com/api/external/discord-servers/" + DISCORD_SERVER_ID + "/vote";
String payload = "{\"discord_user_id\":\"" + event.getUser().getId() + "\",\"direction\":\"up\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(url))
.header("Authorization", "Bearer " + API_KEY)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
HttpResponse<String> response = HttpClient.newHttpClient().send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
EmbedBuilder embed = new EmbedBuilder()
.setTitle("Vote Failed")
.setDescription("Status " + response.statusCode() + "\\n```json\\n" + response.body() + "\\n```")
.setColor(Color.RED);
event.replyEmbeds(embed.build()).setEphemeral(true).queue();
return;
}
event.reply("Vote recorded.").queue();
}
}
using System.Net.Http;
using System.Text;
using System.Text.Json;
using DSharpPlus.Entities;
using DSharpPlus.SlashCommands;
public class VoteCommands : ApplicationCommandModule
{
private static readonly HttpClient Http = new HttpClient();
private static readonly string ApiKey = Environment.GetEnvironmentVariable("TDS_API_KEY");
private static readonly string DiscordServerId = Environment.GetEnvironmentVariable("TDS_DISCORD_SERVER_ID");
[SlashCommand("vote", "Vote for our server")]
public async Task Vote(InteractionContext ctx)
{
var url = $"https://topdiscordservers.com/api/external/discord-servers/{DiscordServerId}/vote";
var body = JsonSerializer.Serialize(new { discord_user_id = ctx.User.Id.ToString(), direction = "up" });
using var request = new HttpRequestMessage(HttpMethod.Post, url);
request.Headers.Add("Authorization", $"Bearer {ApiKey}");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");
var response = await Http.SendAsync(request);
var payload = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
string message = "Unknown error";
string code = "n/a";
string retryAfterHuman = null;
try
{
using var doc = JsonDocument.Parse(payload);
var root = doc.RootElement;
if (root.TryGetProperty("message", out var m)) message = m.GetString() ?? message;
if (root.TryGetProperty("error_code", out var c)) code = c.GetString() ?? code;
if (root.TryGetProperty("retryAfterHuman", out var r)) retryAfterHuman = r.GetString();
}
catch
{
message = payload;
}
var embed = new DiscordEmbedBuilder()
.WithTitle("Vote Failed")
.WithDescription(message)
.WithColor(DiscordColor.Red)
.AddField("Status", ((int)response.StatusCode).ToString(), true)
.AddField("Code", code, true);
if ((int)response.StatusCode == 429 && !string.IsNullOrWhiteSpace(retryAfterHuman))
{
embed.AddField("Try Again In", retryAfterHuman, true);
}
await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().AddEmbed(embed), true);
return;
}
await ctx.CreateResponseAsync("Vote recorded.");
}
}
use GuzzleHttp\Client;
$client = new Client();
$apiKey = getenv('TDS_API_KEY');
$discordServerId = getenv('TDS_DISCORD_SERVER_ID');
$response = $client->post("https://topdiscordservers.com/api/external/discord-servers/{$discordServerId}/vote", [
'headers' => [
'Authorization' => 'Bearer '.$apiKey,
'Accept' => 'application/json',
],
'json' => [
'discord_user_id' => (string) $discordUserId,
'direction' => 'up',
],
]);
$payload = json_decode((string) $response->getBody(), true);
package main
import (
"bytes"
"net/http"
"os"
)
func vote(discordUserID string) (*http.Response, error) {
discordServerID := os.Getenv("TDS_DISCORD_SERVER_ID")
apiKey := os.Getenv("TDS_API_KEY")
body := []byte(`{"discord_user_id":"` + discordUserID + `","direction":"up"}`)
req, _ := http.NewRequest("POST", "https://topdiscordservers.com/api/external/discord-servers/"+discordServerID+"/vote", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
return client.Do(req)
}
curl -X POST "https://topdiscordservers.com/api/external/discord-servers/123456789012345678/vote" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"discord_user_id":"123456789012345678","direction":"up"}'
Tip: send the slash-command user’s ID as discord_user_id so cooldowns apply to the right person.
05 Safety Checklist
- Store the API key in an environment variable. Never hard-code it in source control.
- Use ephemeral Discord replies for errors and cooldown notices.
- Log non-200 responses so you can trace invalid keys and malformed payloads.
- Use one API key per bot or environment. That makes rotation easier.
- Run every change through the API Tester before deploying.