Skip to main content
Receive instant notifications when users vote for your bot. Use vote webhooks to grant rewards, track voting patterns, and build engagement features.

Overview

When a user successfully votes for your bot on Rankly, an HTTP POST request is sent to your configured webhook endpoint. This enables you to:
  • πŸ† Grant instant rewards to voters
  • πŸ“Š Track voting participation
  • 🎯 Build loyalty programs
  • 🎁 Handle credit awards automatically
Vote webhooks are distinct from premium webhooks. Each uses a separate webhook secret (RANKLY_VOTE_WEBHOOK_SECRET).

Webhook Delivery

Request Format

Vote webhooks are delivered as JSON POST requests:
POST https://your-endpoint.com/webhook/votes
Content-Type: application/json
X-Webhook-Signature: <hmac_sha256_hex>

Payload Structure

{
  "event": "bot_vote",
  "timestamp": "2025-11-25T10:00:00.000Z",
  "voter": {
    "userId": "987654321098765432",
    "username": "exampleuser"
  },
  "bot": {
    "id": "1234567890123456789",
    "name": "Your Bot Name"
  },
  "creditsAwarded": true,
  "adBlocker": false,
  "creditDenialReason": null
}

Field Reference

event
string
required
Event identifier. Always bot_vote for vote webhooks.
timestamp
string
required
ISO 8601 formatted timestamp when the vote occurred.
voter
object
required
bot
object
required
creditsAwarded
boolean
required
Whether Rankly platform credits were awarded for this vote.
adBlocker
boolean
required
true if an ad blocker was detected during the vote process.
creditDenialReason
string | null
Explanation if credits weren’t awarded. null when credits were successfully awarded.

Understanding Credit Awards

The creditsAwarded and adBlocker fields help you understand the vote context:
ScenariocreditsAwardedadBlockercreditDenialReason
Successful vote, no blockertruefalsenull
Vote blocked by ad blockerfalsetrueAd blocker detected
Duplicate recent votefalsefalseAlready voted recently
Even if creditsAwarded is false, you may want to grant your own bot rewards for user engagement and support.

Signature Verification

All webhooks include a cryptographic signature. Verify it before processing to prevent unauthorized requests:

Verification Steps

  1. Extract X-Webhook-Signature header
  2. Stringify the JSON payload as received
  3. Compute HMAC SHA256 with your webhook secret
  4. Compare using timing-safe comparison
  • Node.js
  • Python
  • Go
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  if (!signature || !secret) {
    return false;
  }

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  } catch {
    return false;
  }
}
Never process webhooks without verifying the signature. Always use timing-safe comparison functions.

Complete Handler Implementation

Here’s a production-ready webhook handler with verification, idempotency, and reward logic:
const crypto = require('crypto');
const express = require('express');
const Redis = require('redis');

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

const WEBHOOK_SECRET = process.env.RANKLY_VOTE_WEBHOOK_SECRET;
const redisClient = Redis.createClient();

function verifyWebhookSignature(payload, signature, secret) {
  if (!signature || !secret) {
    return false;
  }

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');

  try {
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(expectedSignature)
    );
  } catch {
    return false;
  }
}

app.post('/webhook/votes', async (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const payload = req.body;

  // Verify signature before processing
  if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
    console.error('Webhook signature verification failed');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const { voter, bot, timestamp, creditsAwarded, adBlocker } = payload;
  const voteKey = `${voter.userId}:${timestamp}`;

  try {
    // Check for duplicates using Redis
    const isDuplicate = await redisClient.exists(voteKey);
    if (isDuplicate) {
      return res.status(200).json({ received: true, duplicate: true });
    }

    // Mark as processed
    await redisClient.setex(voteKey, 86400, '1'); // 24 hour expiry

    console.log(`Vote received: ${voter.username} (${voter.userId}) voted for ${bot.name}`);

    // Process the vote asynchronously
    await processVote({
      userId: voter.userId,
      username: voter.username,
      botId: bot.id,
      botName: bot.name,
      creditsAwarded,
      adBlocker,
      timestamp
    });

    return res.status(200).json({ received: true });
  } catch (error) {
    console.error('Error processing vote:', error);
    return res.status(500).json({ error: 'Processing failed' });
  }
});

async function processVote(data) {
  try {
    // Record the vote in your database
    await database.votes.insertOne({
      userId: data.userId,
      username: data.username,
      botId: data.botId,
      botName: data.botName,
      creditsAwarded: data.creditsAwarded,
      adBlocker: data.adBlocker,
      votedAt: new Date(data.timestamp)
    });

    // Grant rewards
    if (data.creditsAwarded && !data.adBlocker) {
      await grantVoteReward(data.userId, data.botId);
    } else if (!data.adBlocker) {
      // Still reward users for voting, even if Rankly didn't award credits
      await grantVoteReward(data.userId, data.botId);
    }

    console.log(`Processed vote from ${data.username}`);
  } catch (error) {
    console.error('Error in processVote:', error);
    throw error;
  }
}

async function grantVoteReward(userId, botId) {
  // Implement your reward logic here
  // Examples: +100 currency, +5 XP, etc.
  console.log(`Granted vote reward to ${userId}`);
}

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

Response Requirements

Your endpoint must meet these specifications for reliable delivery:
RequirementSpecification
Response TimeReturn within 10 seconds
Success StatusHTTP 200 to acknowledge receipt
IdempotencyHandle duplicate deliveries gracefully
Retry BehaviorNon-2xx responses trigger retries
{
  "received": true
}

Idempotency & Deduplication

Implement deduplication using voter.userId and timestamp to prevent duplicate reward grants:
Use Redis or database persistence for production. In-memory storage is only suitable for local development.

Reward Implementation

Design your reward system based on vote behavior:
async function grantVoteReward(userId, botId) {
  const rewardAmount = 100; // Your bot's currency unit
  
  try {
    // Award currency
    await database.users.updateOne(
      { discordId: userId },
      { $inc: { balance: rewardAmount } }
    );

    // Track reward
    await database.rewardLog.insertOne({
      userId,
      botId,
      type: 'vote_reward',
      amount: rewardAmount,
      awardedAt: new Date()
    });

    // Optional: Notify user via DM
    await notifyUserOfReward(userId, rewardAmount);
  } catch (error) {
    console.error('Error granting reward:', error);
    throw error;
  }
}

Database Schema

Votes Table

CREATE TABLE votes (
  id SERIAL PRIMARY KEY,
  user_id VARCHAR(20) NOT NULL,
  username VARCHAR(100) NOT NULL,
  bot_id VARCHAR(20) NOT NULL,
  bot_name VARCHAR(100) NOT NULL,
  credits_awarded BOOLEAN DEFAULT false,
  ad_blocker BOOLEAN DEFAULT false,
  voted_at TIMESTAMP NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_votes_user_id ON votes(user_id);
CREATE INDEX idx_votes_bot_id ON votes(bot_id);
CREATE INDEX idx_votes_voted_at ON votes(voted_at);
CREATE UNIQUE INDEX idx_votes_dedup ON votes(user_id, voted_at);

Reward Log Table

CREATE TABLE reward_log (
  id SERIAL PRIMARY KEY,
  user_id VARCHAR(20) NOT NULL,
  bot_id VARCHAR(20) NOT NULL,
  type VARCHAR(50) NOT NULL,
  amount INTEGER NOT NULL,
  awarded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_reward_log_user_id ON reward_log(user_id);
CREATE INDEX idx_reward_log_awarded_at ON reward_log(awarded_at);

Security Best Practices

Verify Signatures

Always validate webhook signatures before processing any vote data.

Use HTTPS

Ensure your webhook endpoint uses TLS encryption.

Rate Limiting

Implement rate limiting on your webhook endpoint to prevent abuse.

Async Processing

Process votes asynchronously to avoid timeout errors.

Monitor Failures

Log and monitor signature verification failures for security issues.

Rotate Secrets

Regenerate webhook secrets immediately if compromised.

Troubleshooting

Cause: Endpoint not publicly accessibleSolutions:
  • Verify your server is reachable from the internet
  • Check firewall rules allow inbound HTTPS traffic
  • Confirm endpoint URL is correct in Rankly dashboard
  • Test: curl -X POST https://your-endpoint.com/webhook/votes
Cause: Wrong secret or payload modificationSolutions:
  • Ensure you’re using RANKLY_VOTE_WEBHOOK_SECRET (not premium)
  • Don’t modify the payload before verification
  • Check for encoding issues (UTF-8)
  • Verify secret hasn’t been regenerated
Cause: Processing takes too longSolutions:
  • Return HTTP 200 immediately
  • Process reward grants asynchronously
  • Use background job queue (Bull, Celery, etc.)
  • Set up monitoring for long-running tasks
Cause: Missing idempotency implementationSolutions:
  • Implement deduplication using voter.userId:timestamp
  • Use Redis for high-traffic scenarios
  • Add database constraints for uniqueness
  • Record votes before processing rewards
Cause: Could be ad blocker, duplicate vote, or other restrictionsSolutions:
  • Check if adBlocker flag is true in payload
  • Review creditDenialReason for specific cause
  • Still grant your own rewards for engagement
  • Monitor credit denial patterns for issues

Best Practices

Next Steps

  • πŸ‘‘ Review Premium Purchase Webhooks for monetization
  • πŸ“Š Set up analytics for vote tracking
  • 🎁 Design tier-based reward systems
  • πŸ§ͺ Test with sandbox votes before production