# tglist.ing > Directory of Telegram bots. Owners list their bot, verify ownership, and can reward users who vote for it via a vote-check API or signed webhooks. Human-readable docs: - [Overview](https://tglist.ing/docs): What tglist.ing does for bot owners and where to start. - [Submit your bot](https://tglist.ing/docs/submit): List a bot, verify you own it, and what each listing status means. - [Vote rewards](https://tglist.ing/docs/votes): Reward users who vote: poll the vote-check API or receive signed webhooks. - [Notifications](https://tglist.ing/docs/notifications): Telegram messages about your listing, reviews, milestones and webhook failures. ## What an agent can and can't do The owner must do these in the browser themselves (Telegram login required): submit the listing, verify ownership, create the vote-check API key, and set the webhook URL and copy its signing secret. Ask the owner for the key/secret and read them from environment variables (TGLIST_API_KEY, TGLIST_WEBHOOK_SECRET). Never hard-code them. ## Submitting a bot Logged-in owners submit at https://tglist.ing/submit: Telegram username, name (≤60 chars), category, supported languages, short description (≤120), long description (≤1000), optional tags (≤8), commands and image. Listings are reviewed before publishing. Bots already listed by someone else are claimed at https://tglist.ing/claim. Listing URL: https://tglist.ing/bots/. ## Ownership verification 1. Owner presses "Get verification code" (format `tglist-XXXXXXXX`, valid 15 minutes). 2. In @BotFather, enable Bot-to-Bot Communication Mode for the bot. 3. The bot sends the code to @tgl_official_bot: curl "https://api.telegram.org/bot/sendMessage?chat_id=@tgl_official_bot&text=tglist-XXXXXXXX" The token never leaves the owner's machine. Changing the bot's username later resets verification. ## Votes Logged-in users can vote for a bot once every 24 hours. Users are identified by their Telegram user ID (the `from.id` a bot sees in updates). ### Polling: vote-check API GET https://tglist.ing/api/integrations/vote-status?user_id= Authorization: Bearer 200 response: { "voted": true, "lastVotedAt": "2026-08-02T10:30:00.000Z", "nextVoteAt": "2026-08-03T10:30:00.000Z" } - `voted`: true if the user voted in the last 24 hours. - `lastVotedAt` / `nextVoteAt`: null if the user never voted. - Errors: 401 missing/revoked key, 400 missing user_id, 429 over 120 requests/minute per bot. - `voted` stays true for 24 hours. To reward once per vote, store `lastVotedAt` with each reward and only reward again when it changes. Node.js: const res = await fetch(`https://tglist.ing/api/integrations/vote-status?user_id=${userId}`, { headers: { Authorization: `Bearer ${process.env.TGLIST_API_KEY}` }, }); const { voted, lastVotedAt } = await res.json(); ### Webhook: vote.created Each vote is POSTed as JSON to the owner's https URL: { "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 Telegram user ID (number); `username` may be null. The dashboard's "Test endpoint" sends `"type": "ping"` events: reply 2xx and ignore them. Headers: X-Tglist-Event: vote.created X-Tglist-Event-Id: cmh2x9v0k0001 X-Tglist-Signature: t=,v1= Verification: `v1` = hex HMAC-SHA256 of `${t}.${rawBody}` keyed with the signing secret (`whsec_...`). Use the raw request bytes, compare in constant time, and reject (401) if it doesn't match or `t` is more than 300 seconds from now. Node.js (Express): app.post("/tglist/webhook", express.raw({ type: "application/json" }), (req, res) => { const { t, v1 = "" } = Object.fromEntries((req.get("X-Tglist-Signature") ?? "").split(",").map((p) => p.split("="))); const expected = crypto.createHmac("sha256", process.env.TGLIST_WEBHOOK_SECRET).update(`${t}.${req.body}`).digest("hex"); const valid = v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected)); if (!valid || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(401); const event = JSON.parse(req.body); if (event.type === "vote.created") grantReward(event.data.user.id); res.sendStatus(200); }); Python (Flask): body = request.get_data() parts = dict(p.split("=", 1) for p in request.headers.get("X-Tglist-Signature", "").split(",") if "=" in p) expected = hmac.new(SECRET, f"{parts.get('t', '')}.".encode() + body, hashlib.sha256).hexdigest() if not parts.get("t", "").isdigit() or abs(time.time() - int(parts["t"])) > 300 or not hmac.compare_digest(expected, parts.get("v1", "")): abort(401) Delivery rules: - Reply 2xx within 5 seconds. Redirects are not followed. - Retries after 30s, 2m, 10m, 1h, 6h, 12h, 24h, then the delivery is dropped. - At-least-once: deduplicate on X-Tglist-Event-Id. - After 50 consecutive failures the webhook is disabled until the owner re-enables it. - URL must be public https with a domain name (no IPs or private hosts).