@toptl/sdk
v1.0.1
Published
Official TypeScript SDK for the TOP.TL Telegram directory API
Readme
toptl
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
Installation
npm install toptlyarn add toptlpnpm add toptlQuick Start
import { 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);Authentication
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.
Usage
Get Listing Info
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); // trueGet Votes
// 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);Check if a User Voted
const result = await toptl.hasVoted('mybot', 123456789);
if (result.hasVoted) {
console.log(`User voted at ${result.votedAt}`);
} else {
console.log('User has not voted');
}Post Stats
await toptl.postStats('mybot', {
memberCount: 12500,
groupCount: 35,
channelCount: 12,
botServes: ['en', 'ru', 'es'],
});Batch Post Stats
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 },
]);Get Global Stats
const stats = await toptl.getStats();
console.log(`${stats.totalListings} listings, ${stats.totalVotes} votes`);Webhooks
Set up webhooks to receive real-time vote notifications.
Set Webhook
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');Test Webhook
Send a test event to your configured webhook to verify it works:
const result = await toptl.testWebhook('mybot');
console.log(result.statusCode); // 200Autoposter
The 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.
Basic Usage
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);
});Custom Interval
// Post every 10 minutes
toptl.startAutopost('mybot', getStats, { interval: 10 * 60 * 1000 });On-Change Detection
Skip posting when stats haven't changed since the last post:
toptl.startAutopost('mybot', getStats, { onlyOnChange: true });Server-Driven Intervals
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.
grammY Plugin
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 }));Manual grammY Integration
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();API Reference
new TopTL(apiKey, options?)
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) |
getListing(username)
Get listing info. Requires scope listing:read.
Returns: Promise<Listing>
getVotes(username, limit?)
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>
hasVoted(username, telegramId)
Check if a user has voted. Requires scope votes:check.
Returns: Promise<HasVotedResponse>
postStats(username, stats)
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>
batchPostStats(stats)
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>
getStats()
Get global TOP.TL platform stats. Requires scope listing:read.
Returns: Promise<GlobalStats>
setWebhook(username, url, rewardTitle?)
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>
testWebhook(username)
Send a test event to the configured webhook. Requires scope listing:write.
Returns: Promise<WebhookTestResponse>
startAutopost(username, statsFn, options?)
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>
stopAutopost()
Stop the autoposter.
grammy(username, options?)
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>
Types
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;
}Error Handling
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
}
}License
MIT - see LICENSE for details.
