Skip to main content
Integrate webhooks to respond to votes in real-time. Automatically grant rewards, track voting patterns, and build engagement features.

Overview

When a user votes for your server on Rankly, a webhook delivers a JSON payload to your configured endpoint. This enables you to:
  • 🎁 Grant instant rewards to voters
  • 📊 Track voting patterns and trends
  • 🔄 Automate community engagement
  • 🎯 Build loyalty programs
Webhooks are completely optional. Your server earns credits regardless of webhook configuration.

Configuration

Set up webhooks from your server dashboard:
1

Navigate to Server Settings

Go to your server dashboard on rankly.live
2

Find Vote Webhooks

Locate the “Vote Webhooks” section in settings
3

Enter Webhook URL

Paste your endpoint URL (must use HTTPS)
4

Add Authorization (Optional)

Configure an authorization header for additional security
5

Test Your Webhook

Click “Test Webhook” to send a test payload
6

Save Configuration

Save your settings and you’re ready!

Webhook Delivery

Request Format

All vote webhooks are delivered as POST requests:
POST https://your-endpoint.com/webhook/votes
Content-Type: application/json
Authorization: <your-header> (if configured)

Payload Structure

{
  "userId": "987654321098765432",
  "serverId": "123456789012345678",
  "timestamp": "2025-11-25T10:00:00.000Z",
  "creditsAwarded": true,
  "adBlocker": false,
  "creditDenialReason": null,
  "test": false
}

Field Reference

userId
string
required
The Discord user ID of the voter. Use this to identify the user in your bot.
serverId
string
required
Your server’s Discord ID. Useful if handling webhooks for multiple servers.
timestamp
string
required
ISO 8601 formatted timestamp of when the vote occurred.
creditsAwarded
boolean
required
Whether Rankly credits were awarded to the server for this vote. Can be false for various reasons (duplicate vote, ad blocker, etc.).
adBlocker
boolean
required
true if an ad blocker was detected during the voting process.
creditDenialReason
string | null
Explanation if credits weren’t awarded. Value is null when credits are awarded.
test
boolean
required
true if this is a test webhook from the dashboard. false for real votes.

Understanding Vote Context

Use these fields to determine reward eligibility:
ScenariocreditsAwardedadBlockerAction
Normal votetruefalse✅ Grant full reward
Ad blocker usedfalsetrue⚠️ Consider reduced reward
Duplicate votefalsefalseℹ️ Skip reward or acknowledge
Test from dashboardanyany🧪 Log but don’t reward
Even if creditsAwarded is false, consider rewarding users for voting. Your server benefits from engagement regardless!

Authorization & Security

Optional Authorization Header

Configure a custom authorization header for additional security: Common formats:
Authorization: Bearer your-secret-token
Authorization: Basic base64-credentials
Authorization: your-simple-secret
Rankly will include this header in every webhook request:
const authHeader = req.headers['authorization'];
if (authHeader !== process.env.RANKLY_WEBHOOK_AUTH) {
  return res.status(401).json({ error: 'Unauthorized' });
}

Best Practices

  • ✅ Use HTTPS endpoints (required)
  • ✅ Configure authorization headers
  • ✅ Validate payload structure
  • ✅ Log webhook activity
  • ✅ Implement rate limiting
  • ✅ Return 200 quickly and process asynchronously

Implementation Examples

Node.js with Express

const express = require('express');
const app = express();

app.use(express.json());

const EXPECTED_AUTH = process.env.RANKLY_WEBHOOK_AUTH;

app.post('/webhook/votes', (req, res) => {
  // Verify authorization header if configured
  const authHeader = req.headers['authorization'];
  if (EXPECTED_AUTH && authHeader !== EXPECTED_AUTH) {
    console.error('Unauthorized webhook request');
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const { userId, serverId, creditsAwarded, adBlocker, test } = req.body;

  // Ignore test webhooks
  if (test) {
    console.log('✅ Test webhook received successfully');
    return res.status(200).json({ received: true, test: true });
  }

  console.log(`Vote: User ${userId} voted for server ${serverId}`);

  // Grant reward if credits were awarded
  if (creditsAwarded && !adBlocker) {
    grantVoteReward(userId, serverId);
  }

  // Always return 200 immediately
  return res.status(200).json({ received: true });
});

async function grantVoteReward(userId, serverId) {
  // Implement your reward logic
  console.log(`Granted reward to ${userId}`);
}

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Python with Flask

import os
from flask import Flask, request, jsonify

app = Flask(__name__)

EXPECTED_AUTH = os.environ.get('RANKLY_WEBHOOK_AUTH')

@app.route('/webhook/votes', methods=['POST'])
def handle_vote():
    # Verify authorization
    auth_header = request.headers.get('Authorization')
    if EXPECTED_AUTH and auth_header != EXPECTED_AUTH:
        return jsonify({'error': 'Unauthorized'}), 401

    payload = request.json
    user_id = payload.get('userId')
    server_id = payload.get('serverId')
    credits_awarded = payload.get('creditsAwarded')
    ad_blocker = payload.get('adBlocker')
    is_test = payload.get('test', False)

    # Ignore test webhooks
    if is_test:
        print('✅ Test webhook received')
        return jsonify({'received': True, 'test': True}), 200

    print(f'Vote: User {user_id} voted for server {server_id}')

    # Grant reward
    if credits_awarded and not ad_blocker:
        grant_vote_reward(user_id, server_id)

    return jsonify({'received': True}), 200

def grant_vote_reward(user_id, server_id):
    # Implement your reward logic
    pass

if __name__ == '__main__':
    app.run(port=3000)

Discord.py Integration

import discord
from discord.ext import commands
from flask import Flask, request, jsonify
import asyncio

app = Flask(__name__)
bot = commands.Bot(command_prefix='!')

VOTE_ROLE_ID = 123456789  # Your role ID
EXPECTED_AUTH = 'your-secret'

@app.route('/webhook/votes', methods=['POST'])
def handle_vote():
    # Verify auth
    if request.headers.get('Authorization') != EXPECTED_AUTH:
        return jsonify({'error': 'Unauthorized'}), 401

    payload = request.json
    user_id = int(payload['userId'])
    server_id = int(payload['serverId'])
    credits_awarded = payload['creditsAwarded']
    is_test = payload.get('test', False)

    if is_test:
        return jsonify({'received': True}), 200

    if not credits_awarded:
        return jsonify({'received': True}), 200

    # Grant role asynchronously
    asyncio.run_coroutine_threadsafe(
        grant_vote_role(user_id, server_id),
        bot.loop
    )

    return jsonify({'received': True}), 200

async def grant_vote_role(user_id, server_id):
    try:
        guild = bot.get_guild(server_id)
        if not guild:
            return

        member = await guild.fetch_member(user_id)
        role = guild.get_role(VOTE_ROLE_ID)

        if member and role:
            await member.add_roles(role)
            await member.send('🎉 Thank you for voting!')
    except Exception as e:
        print(f'Error granting role: {e}')

Discord.js Integration

const express = require('express');
const { Client, GatewayIntentBits, EmbedBuilder } = require('discord.js');

const app = express();
app.use(express.json());

const client = new Client({
  intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMembers]
});

const VOTE_ROLE_ID = 'YOUR_ROLE_ID';
const EXPECTED_AUTH = process.env.RANKLY_WEBHOOK_AUTH;

app.post('/webhook/votes', async (req, res) => {
  // Verify auth
  if (EXPECTED_AUTH && req.headers['authorization'] !== EXPECTED_AUTH) {
    return res.status(401).json({ error: 'Unauthorized' });
  }

  const { userId, serverId, creditsAwarded, test } = req.body;

  if (test) {
    return res.status(200).json({ received: true });
  }

  if (!creditsAwarded) {
    return res.status(200).json({ received: true });
  }

  try {
    const guild = await client.guilds.fetch(serverId);
    const member = await guild.members.fetch(userId);
    const role = guild.roles.cache.get(VOTE_ROLE_ID);

    if (member && role) {
      await member.roles.add(role);

      // Send DM
      const embed = new EmbedBuilder()
        .setColor('#Green')
        .setTitle('🎉 Thank you for voting!')
        .setDescription('You received the Voter role!');

      await member.send({ embeds: [embed] });
    }

    return res.status(200).json({ received: true, rewarded: true });
  } catch (error) {
    console.error('Error:', error);
    return res.status(200).json({ received: true, rewarded: false });
  }
});

client.login(process.env.DISCORD_TOKEN);
app.listen(3000);

Response Requirements

Your endpoint must meet these specifications:
RequirementSpecification
ProtocolHTTPS required
Response TimeReturn within 5 seconds
Status CodeHTTP 200 to acknowledge
TimeoutRequests timeout after 5 seconds
FailuresDon’t affect the vote
{
  "received": true
}
Always return HTTP 200 immediately, even if processing fails. Webhook failures don’t affect votes.

Idempotency & Deduplication

Implement idempotency to handle potential duplicate deliveries:
Use Redis or database persistence in production. In-memory storage only works for local development.

Reward Implementation Patterns

Role-Based Rewards

async function grantVoteReward(userId, serverId) {
  const guild = await client.guilds.fetch(serverId);
  const member = await guild.members.fetch(userId);
  const role = guild.roles.cache.get(VOTE_ROLE_ID);

  if (member && role) {
    await member.roles.add(role);
  }
}

Currency-Based Rewards

async function grantVoteReward(userId, serverId) {
  const rewardAmount = 100; // Your currency

  await database.users.updateOne(
    { discordId: userId },
    { $inc: { balance: rewardAmount } }
  );
}

Mixed Rewards

async function grantVoteReward(userId, serverId) {
  const guild = await client.guilds.fetch(serverId);
  const member = await guild.members.fetch(userId);

  // Add role
  if (member) {
    const role = guild.roles.cache.get(VOTE_ROLE_ID);
    await member.roles.add(role);
  }

  // Add currency
  await database.users.updateOne(
    { discordId: userId },
    { $inc: { balance: 100 } }
  );

  // Send notification
  await notifyUserOfReward(userId);
}

Testing Your Webhook

Using the Dashboard

  1. Navigate to your server settings
  2. Find the “Vote Webhooks” section
  3. Click “Test Webhook”
  4. A test payload will be sent with "test": true

Test Payload Format

{
  "userId": "123456789012345678",
  "serverId": "987654321098765678",
  "timestamp": "2025-11-25T10:00:00.000Z",
  "creditsAwarded": true,
  "adBlocker": false,
  "creditDenialReason": null,
  "test": true
}
Always check for "test": true and skip reward processing for test webhooks.

Manual Testing with cURL

curl -X POST https://your-endpoint.com/webhook/votes \
  -H "Content-Type: application/json" \
  -H "Authorization: your-secret" \
  -d '{
    "userId": "123456789012345678",
    "serverId": "987654321098765678",
    "timestamp": "2025-11-25T10:00:00.000Z",
    "creditsAwarded": true,
    "adBlocker": false,
    "creditDenialReason": null,
    "test": true
  }'

Best Practices

Troubleshooting

Causes:
  • Endpoint not publicly accessible
  • Using HTTP instead of HTTPS
  • Firewall blocking Rankly IP addresses
Solutions:
  • Verify endpoint is publicly reachable: curl https://your-endpoint.com/webhook/votes
  • Ensure HTTPS is enabled
  • Check firewall/security group rules
Causes:
  • Wrong authorization header value
  • Header format mismatch
  • Secret regenerated in dashboard
Solutions:
  • Verify header value matches exactly
  • Check header format (Bearer, Basic, custom)
  • Regenerate and update if changed
Causes:
  • Processing takes too long
  • Database queries blocking response
  • External API calls
Solutions:
  • Return 200 immediately
  • Process rewards in background job queue
  • Use caching to speed up lookups
Causes:
  • Missing idempotency implementation
  • Multiple webhook deliveries
Solutions:
  • Implement deduplication using userId + timestamp
  • Use Redis or database for tracking
  • Add unique constraints to vote table
Causes:
  • Different endpoint URL
  • Authorization header misconfigured
  • Webhook disabled or changed
Solutions:
  • Verify webhook URL is correct in dashboard
  • Test with real vote from another user
  • Check server logs for webhook requests

Database Schema

CREATE TABLE votes (
  id SERIAL PRIMARY KEY,
  user_id VARCHAR(20) NOT NULL,
  server_id VARCHAR(20) NOT NULL,
  credits_awarded BOOLEAN DEFAULT false,
  ad_blocker BOOLEAN DEFAULT false,
  voted_at TIMESTAMP NOT NULL,
  processed BOOLEAN DEFAULT false,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(user_id, server_id, voted_at)
);

CREATE TABLE vote_rewards (
  id SERIAL PRIMARY KEY,
  user_id VARCHAR(20) NOT NULL,
  server_id VARCHAR(20) NOT NULL,
  reward_type VARCHAR(50),
  reward_value VARCHAR(255),
  granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_votes_user ON votes(user_id);
CREATE INDEX idx_votes_server ON votes(server_id);
CREATE INDEX idx_votes_processed ON votes(processed);

Next Steps