Official TypeScript SDK for the TOP.TL Telegram directory API.
- Zero dependencies (uses native
fetch) - Full TypeScript types
- Built-in autoposter with on-change detection and server-driven intervals
- grammY plugin for automatic stat tracking
- Webhook setup and testing
- Batch stat posting for multi-listing bots
- Works with Node.js 18+, Deno, Bun, and edge runtimes
npm install toptlyarn add toptlpnpm add toptlimport { TopTL } from 'toptl';
const toptl = new TopTL('toptl_your_api_key');
// Get listing info
const listing = await toptl.getListing('mybot');
console.log(listing.title, listing.votes);Get your API key from top.tl/settings/api. Pass it to the constructor:
const toptl = new TopTL('toptl_xxx');All requests are authenticated via Authorization: Bearer toptl_xxx header.
const listing = await toptl.getListing('mybot');
console.log(listing.title); // "My Bot"
console.log(listing.memberCount); // 12500
console.log(listing.votes); // 342
console.log(listing.verified); // true// Get all active voters
const votes = await toptl.getVotes('mybot');
console.log(votes.totalVotes); // 342
console.log(votes.voters); // [{ telegramId, firstName, votedAt }]
// Limit results
const top10 = await toptl.getVotes('mybot', 10);const result = await toptl.hasVoted('mybot', 123456789);
if (result.hasVoted) {
console.log(`User voted at ${result.votedAt}`);
} else {
console.log('User has not voted');
}await toptl.postStats('mybot', {
memberCount: 12500,
groupCount: 35,
channelCount: 12,
botServes: ['en', 'ru', 'es'],
});Post stats for multiple listings in a single request. Useful for bots that manage several listings:
await toptl.batchPostStats([
{ username: 'mybot', memberCount: 12500, groupCount: 35 },
{ username: 'mygroup', memberCount: 8400, channelCount: 3 },
]);const stats = await toptl.getStats();
console.log(`${stats.totalListings} listings, ${stats.totalVotes} votes`);Set up webhooks to receive real-time vote notifications.
await toptl.setWebhook('mybot', 'https://example.com/webhook');
// With a reward title shown to voters
await toptl.setWebhook('mybot', 'https://example.com/webhook', 'Premium Access');Send a test event to your configured webhook to verify it works:
const result = await toptl.testWebhook('mybot');
console.log(result.statusCode); // 200The SDK includes a built-in autoposter that reports your bot's stats to TOP.TL. It posts immediately on start, then repeats on an interval.
import { TopTL } from 'toptl';
const toptl = new TopTL('toptl_xxx');
// Start auto-posting every 30 minutes (default)
toptl.startAutopost('mybot', async () => {
return { memberCount: await getUserCount() };
});
// Stop when needed (e.g., on shutdown)
process.on('SIGINT', () => {
toptl.stopAutopost();
process.exit(0);
});// Post every 10 minutes
toptl.startAutopost('mybot', getStats, { interval: 10 * 60 * 1000 });Skip posting when stats haven't changed since the last post:
toptl.startAutopost('mybot', getStats, { onlyOnChange: true });When the server responds with a retryAfter value, the autoposter automatically uses it as the next interval instead of the configured default. This lets the server throttle or accelerate posting as needed.
The SDK provides a built-in grammY middleware that automatically tracks unique chat IDs from incoming updates and posts them as memberCount stats.
import { Bot } from 'grammy';
import { TopTL } from 'toptl';
const bot = new Bot('BOT_TOKEN');
const toptl = new TopTL('toptl_xxx');
// One line: tracks chats and auto-posts stats
bot.use(toptl.grammy('mybot'));
bot.start();With options:
bot.use(toptl.grammy('mybot', { interval: 10 * 60 * 1000, onlyOnChange: true }));If you need more control, use the autoposter directly:
import { Bot } from 'grammy';
import { TopTL } from 'toptl';
const bot = new Bot('BOT_TOKEN');
const toptl = new TopTL('toptl_xxx');
toptl.startAutopost('mybot', async () => {
const count = await bot.api.getChatMemberCount('@mybot');
return { memberCount: count };
});
// Vote-lock command: only allow access if the user voted
bot.command('premium', async (ctx) => {
const { hasVoted } = await toptl.hasVoted('mybot', ctx.from!.id);
if (!hasVoted) {
return ctx.reply(
'Please vote for us on TOP.TL to unlock this feature!\nhttps://top.tl/mybot'
);
}
return ctx.reply('Welcome, premium user!');
});
bot.start();Create a new client.
| Parameter | Type | Description |
|---|---|---|
apiKey |
string |
Your TOP.TL API key (starts with toptl_) |
options.baseUrl |
string |
Override API base URL (default: https://top.tl/api/v1) |
Get listing info. Requires scope listing:read.
Returns: Promise<Listing>
Get votes with active voters. Requires scope votes:read.
| Parameter | Type | Description |
|---|---|---|
username |
string |
Listing username |
limit |
number |
Max voters to return (optional) |
Returns: Promise<VotesResponse>
Check if a user has voted. Requires scope votes:check.
Returns: Promise<HasVotedResponse>
Post stats for a listing. Requires scope listing:write.
| Parameter | Type | Description |
|---|---|---|
username |
string |
Listing username |
stats.memberCount |
number |
Current member count (optional) |
stats.groupCount |
number |
Current group count (optional) |
stats.channelCount |
number |
Current channel count (optional) |
stats.botServes |
string[] |
Languages or regions the bot serves (optional) |
Returns: Promise<PostStatsResponse>
Post stats for multiple listings in one request. Requires scope listing:write.
| Parameter | Type | Description |
|---|---|---|
stats |
BatchStatsEntry[] |
Array of { username, memberCount?, groupCount?, channelCount?, botServes? } |
Returns: Promise<BatchStatsResponse>
Get global TOP.TL platform stats. Requires scope listing:read.
Returns: Promise<GlobalStats>
Set or update the webhook URL for a listing. Requires scope listing:write.
| Parameter | Type | Description |
|---|---|---|
username |
string |
Listing username |
url |
string |
Webhook endpoint URL |
rewardTitle |
string |
Reward title shown to voters (optional) |
Returns: Promise<WebhookResponse>
Send a test event to the configured webhook. Requires scope listing:write.
Returns: Promise<WebhookTestResponse>
Start auto-posting stats. Posts immediately, then on interval.
| Parameter | Type | Description |
|---|---|---|
username |
string |
Listing username |
statsFn |
() => PostStatsBody | Promise<PostStatsBody> |
Function returning current stats |
options.interval |
number |
Interval in ms (default: 30 min). Overridden by server retryAfter. |
options.onlyOnChange |
boolean |
Skip posting if stats are unchanged (default: false) |
Returns: Promise<void>
Stop the autoposter.
Returns grammY-compatible middleware that tracks unique chat IDs and auto-posts stats.
| Parameter | Type | Description |
|---|---|---|
username |
string |
Listing username |
options |
AutopostOptions |
Same options as startAutopost (optional) |
Returns: (ctx: any, next: () => Promise<void>) => Promise<void>
interface Listing {
username: string;
title: string;
description: string;
category: string;
memberCount: number;
votes: number;
verified: boolean;
featured: boolean;
createdAt: string;
updatedAt: string;
}
interface VotesResponse {
username: string;
totalVotes: number;
voters: Voter[];
}
interface Voter {
telegramId: number;
firstName: string;
votedAt: string;
}
interface HasVotedResponse {
hasVoted: boolean;
votedAt: string | null;
}
interface PostStatsBody {
memberCount?: number;
groupCount?: number;
channelCount?: number;
botServes?: string[];
}
interface PostStatsResponse {
success: boolean;
retryAfter?: number;
}
interface GlobalStats {
totalListings: number;
totalVotes: number;
totalUsers: number;
categories: number;
}
interface WebhookResponse {
success: boolean;
url: string;
rewardTitle?: string;
}
interface WebhookTestResponse {
success: boolean;
statusCode: number;
body: unknown;
}
interface BatchStatsEntry {
username: string;
memberCount?: number;
groupCount?: number;
channelCount?: number;
botServes?: string[];
}
interface BatchStatsResponse {
success: boolean;
processed: number;
}
interface AutopostOptions {
interval?: number;
onlyOnChange?: boolean;
}All API errors throw a TopTLError with the HTTP status and response body:
import { TopTL, TopTLError } from 'toptl';
try {
await toptl.getListing('nonexistent');
} catch (err) {
if (err instanceof TopTLError) {
console.log(err.status); // 404
console.log(err.body); // API error response
}
}MIT - see LICENSE for details.