Integration

Vote rewards

Users can vote for your bot once every 24 hours. Give them something back, like credits, a premium day or a badge, by checking their vote from your bot.

Pick a method

MethodUse it when
PollingYour bot asks us when a user claims a reward, e.g. a /claim button. Simplest; no public server needed.
WebhookWe POST to your server the moment someone votes, so you can reward them without them asking.

Both live under Dashboard → your bot → Integration. You can use either or both. Users are identified by their Telegram user ID, the same from.id your bot sees in updates.

Send users to your listing to vote: https://tglist.ing/bots/your-bot-username.

Polling: the vote-check API

Get an API key

On the Integration page, press Create key. The key is shown once; store it as an environment variable on your bot server (the examples use TGLIST_API_KEY). Rotating creates a new key and the old one stops working immediately.

Request

GET https://tglist.ing/api/integrations/vote-status?user_id=123456789
Authorization: Bearer YOUR_API_KEY

Response

{
  "voted": true,
  "lastVotedAt": "2026-08-02T10:30:00.000Z",
  "nextVoteAt": "2026-08-03T10:30:00.000Z"
}
  • voted is true if the user voted in the last 24 hours.
  • lastVotedAt and nextVoteAt are null if they have never voted. Use nextVoteAt to tell users when they can vote again.
  • Errors: 401 for a missing or revoked key, 400 without user_id, 429 above 120 requests a minute per bot.
voted stays true for 24 hours, so a user can claim many times inside one vote. Store lastVotedAt with each reward you grant and only reward again when it changes.

Examples

const res = await fetch(`/api/integrations/vote-status?user_id=${telegramUserId}`, {
  headers: { Authorization: `Bearer ${process.env.TGLIST_API_KEY}` },
});
const { voted } = await res.json();
if (voted) await grantReward(telegramUserId);

Webhook: get told when someone votes

Set it up

  1. On the Integration page, switch to Webhook and enter a public https:// URL on your server.
  2. Copy the signing secret (whsec_…) into your server's environment, e.g. TGLIST_WEBHOOK_SECRET.
  3. Press Test endpoint. It sends a ping event; reply with any 2xx.

Payload

Each vote is a JSON POST:

{
  "id": "cmh2x9v0k0001",
  "type": "vote.created",
  "createdAt": "2026-08-02T10:30:00.000Z",
  "data": {
    "botId": "cmgq1b2c30000",
    "user": { "id": 123456789, "username": "alice" },
    "votedAt": "2026-08-02T10:30:00.000Z",
    "nextVoteAt": "2026-08-03T10:30:00.000Z"
  }
}

data.user.id is the voter's Telegram user ID. username can be null.

Headers

Content-Type: application/json
X-Tglist-Event: vote.created
X-Tglist-Event-Id: cmh2x9v0k0001
X-Tglist-Signature: t=1754130600,v1=5f2b9c…

Verify the signature

v1 is the hex HMAC-SHA256 of {t}.{raw body}, keyed with your signing secret. Compute it over the raw request bytes, not re-serialized JSON. Reject with 401 if it doesn't match or t is more than 5 minutes old.

import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.TGLIST_WEBHOOK_SECRET;

// Use the raw body: the signature covers the exact bytes we sent.
app.post("/tglist/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-Tglist-Signature") ?? "";
  const { t, v1 = "" } = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${req.body}`).digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
  const valid = v1.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
  if (!fresh || !valid) return res.sendStatus(401);

  const event = JSON.parse(req.body);
  if (event.type === "vote.created") grantReward(event.data.user.id);
  res.sendStatus(200);
});

Stuck on a mismatch? Paste a real delivery below to see what the signature should be.

Try it: check a signature

Fill in all three fields to check.

Runs in your browser. Nothing you type here is sent anywhere.

Delivery rules

  • Reply with a 2xx within 5 seconds. Redirects aren't followed.
  • Failures are retried after 30 s, 2 min, 10 min, 1 h, 6 h, 12 h and 24 h, then dropped (about 43 hours after the vote).
  • The same event can arrive more than once. Skip any X-Tglist-Event-Id you've already handled.
  • After 5 failures in a row we message you on Telegram. After 50 the webhook is turned off; fix your endpoint and press Enable on the Integration page.
  • The URL must be a public domain over https://. IP addresses and private hosts are refused.

Checklist

  • Keys and secrets live in environment variables, never in your bot's code or chats.
  • You reward once per vote, not once per check.
  • Your bot links to the listing, e.g. from a /vote command.
  • Webhook failure alerts are on in Notifications.