Server Helpers
The @tma.sh/sdk/server entry point provides utilities for API routes — Hono handlers that run on the edge alongside your static app. These helpers handle auth middleware, typed KV access, initData validation, and Telegram Bot API calls.
import { requireUser, createKV, validateInitData, createTelegramApiClient,} from '@tma.sh/sdk/server';requireUser()
Section titled “requireUser()”Hono middleware that authenticates requests using the TMA.sh JWT from the Authorization header. Apply it to any route that requires an authenticated user.
import { Hono } from 'hono';import { requireUser } from '@tma.sh/sdk/server';
const app = new Hono();
// Protect all /api/* routesapp.use('/api/*', requireUser());
app.get('/api/me', (c) => { const user = c.get('user'); return c.json({ telegramId: user.telegramId, firstName: user.firstName, lastName: user.lastName, username: user.username, });});How it works
Section titled “How it works”- Extracts the
Bearertoken from theAuthorizationheader. - Verifies the JWT signature using the project’s JWKS endpoint.
- Checks that the token has not expired.
- Injects the decoded user object into the Hono context via
c.get('user').
If the token is missing, invalid, or expired, the middleware returns a 401 Unauthorized response automatically.
User object
Section titled “User object”The user object injected by requireUser() contains:
| Property | Type | Description |
|---|---|---|
telegramId | number | Telegram user ID |
firstName | string | User’s first name |
lastName | string | User’s last name (may be empty) |
username | string | Telegram username (may be empty) |
createKV(binding)
Section titled “createKV(binding)”Typed wrapper around the Cloudflare KV binding. Provides the same get, set, delete, and list API as the client-side SDK, with TypeScript generics for value types.
import { createKV } from '@tma.sh/sdk/server';
app.get('/api/score', async (c) => { const kv = createKV(c.env.KV); const user = c.get('user');
const score = await kv.get<number>(`score:${user.telegramId}`); return c.json({ score });});
app.post('/api/score', async (c) => { const kv = createKV(c.env.KV); const user = c.get('user'); const { score } = await c.req.json();
await kv.set(`score:${user.telegramId}`, score); return c.json({ ok: true });});The generic parameter on kv.get<T>() narrows the return type. If the key does not exist, it returns null.
Methods
Section titled “Methods”| Method | Signature | Description |
|---|---|---|
get | get<T>(key: string): Promise<T | null> | Retrieve a value |
set | set(key: string, value: unknown, ttl?: number): Promise<void> | Store a value (optional TTL in seconds) |
delete | delete(key: string): Promise<void> | Remove a key |
list | list(prefix: string): Promise<{ keys: { name: string }[], list_complete: boolean }> | List keys by prefix |
The server-side set() method accepts an optional third argument for TTL (time-to-live) in seconds. The client-side SDK does not support TTL. Note that the server-side method is delete(), while the client-side SDK uses remove().
validateInitData(initData, botToken)
Section titled “validateInitData(initData, botToken)”Performs local HMAC-SHA256 verification of Telegram’s initData string. No external API call — the validation runs entirely on the edge.
import { validateInitData } from '@tma.sh/sdk/server';
app.post('/api/verify', async (c) => { const { initData } = await c.req.json(); const verified = await validateInitData(initData, c.env.BOT_TOKEN);
if (!verified) { return c.json({ error: 'Invalid initData' }, 401); }
// verified is a flat object with camelCase fields: // verified.telegramId (number) // verified.firstName (string) // verified.lastName (string | undefined) // verified.username (string | undefined) // verified.authDate (number) return c.json({ telegramId: verified.telegramId, firstName: verified.firstName, authDate: verified.authDate, });});Validation steps
Section titled “Validation steps”- Parses the
initDataquery string. - Computes the HMAC-SHA256 hash using the bot token as the secret key.
- Compares the computed hash against the
hashfield ininitData. - Checks that
auth_dateis within a 5-minute window to prevent replay attacks.
Returns a ValidatedInitData object on success, or null if validation fails.
When to use this
Section titled “When to use this”Use validateInitData() when you need to verify initData directly in your API route without going through the full JWT flow. This is useful for:
- Initial authentication before issuing your own tokens
- Webhook handlers that receive
initDatafrom the client - Custom auth flows that do not use the SDK’s built-in JWT system
For most use cases, prefer requireUser() — it handles token verification automatically and gives you a clean user object.
createTelegramApiClient(botToken)
Section titled “createTelegramApiClient(botToken)”A client for the Telegram Bot API. Used primarily for Stars payments but supports any Bot API method via the generic call() method.
import { createTelegramApiClient } from '@tma.sh/sdk/server';
app.post('/api/notify', async (c) => { const telegram = createTelegramApiClient(c.env.BOT_TOKEN); const user = c.get('user');
await telegram.sendMessage(user.telegramId, 'Your order is ready!'); return c.json({ ok: true });});Methods
Section titled “Methods”| Method | Signature | Description |
|---|---|---|
call | call<T>(method: string, params: Record<string, unknown>): Promise<T> | Call any Telegram Bot API method by name |
sendMessage | sendMessage(chatId: number, text: string, options?): Promise<TelegramMessage> | Send a text message to a chat |
sendPhoto | sendPhoto(chatId: number, photo: string, options?): Promise<TelegramMessage> | Send a photo to a chat |
createInvoiceLink | createInvoiceLink(params: CreateInvoiceLinkParams): Promise<string> | Create a Stars invoice link |
answerPreCheckoutQuery | answerPreCheckoutQuery(queryId: string, ok: boolean, errorMessage?): Promise<boolean> | Respond to a pre-checkout query (must answer within 10 seconds) |
Use call() for any Bot API method not covered by the convenience methods.
Creating a Stars invoice
Section titled “Creating a Stars invoice”const invoiceUrl = await telegram.createInvoiceLink({ title: 'Premium', description: '30 days of premium', payload: JSON.stringify({ plan: 'premium' }), currency: 'XTR', prices: [{ label: 'Premium', amount: 100 }],});Answering a pre-checkout query
Section titled “Answering a pre-checkout query”// In your bot webhook handlerawait telegram.answerPreCheckoutQuery(query.id, true);
// Or reject with an error messageawait telegram.answerPreCheckoutQuery(query.id, false, 'Item out of stock');Full example
Section titled “Full example”A complete API route with auth, KV, and the Telegram API:
import { Hono } from 'hono';import { requireUser, createKV, createTelegramApiClient } from '@tma.sh/sdk/server';
type Env = { Bindings: { KV: KVNamespace; BOT_TOKEN: string; OPENAI_API_KEY: string; };};
const app = new Hono<Env>();
// Public health checkapp.get('/api/health', (c) => c.json({ status: 'ok' }));
// Protected routesapp.use('/api/*', requireUser());
app.get('/api/profile', async (c) => { const kv = createKV(c.env.KV); const user = c.get('user');
const profile = await kv.get(`profile:${user.telegramId}`); return c.json({ user, profile });});
app.post('/api/profile', async (c) => { const kv = createKV(c.env.KV); const user = c.get('user'); const body = await c.req.json();
await kv.set(`profile:${user.telegramId}`, { bio: body.bio, updatedAt: Date.now(), });
return c.json({ ok: true });});
export default app;This file lives at server/api/index.ts in your project. TMA.sh detects it at build time, bundles it with esbuild, and deploys it to {project}--api.tma.sh. See API Routes for the full setup guide.