Skip to content

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';

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/* routes
app.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,
});
});
  1. Extracts the Bearer token from the Authorization header.
  2. Verifies the JWT signature using the project’s JWKS endpoint.
  3. Checks that the token has not expired.
  4. 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.

The user object injected by requireUser() contains:

PropertyTypeDescription
telegramIdnumberTelegram user ID
firstNamestringUser’s first name
lastNamestringUser’s last name (may be empty)
usernamestringTelegram username (may be empty)

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.

MethodSignatureDescription
getget<T>(key: string): Promise<T | null>Retrieve a value
setset(key: string, value: unknown, ttl?: number): Promise<void>Store a value (optional TTL in seconds)
deletedelete(key: string): Promise<void>Remove a key
listlist(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().

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,
});
});
  1. Parses the initData query string.
  2. Computes the HMAC-SHA256 hash using the bot token as the secret key.
  3. Compares the computed hash against the hash field in initData.
  4. Checks that auth_date is within a 5-minute window to prevent replay attacks.

Returns a ValidatedInitData object on success, or null if validation fails.

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 initData from 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.

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 });
});
MethodSignatureDescription
callcall<T>(method: string, params: Record<string, unknown>): Promise<T>Call any Telegram Bot API method by name
sendMessagesendMessage(chatId: number, text: string, options?): Promise<TelegramMessage>Send a text message to a chat
sendPhotosendPhoto(chatId: number, photo: string, options?): Promise<TelegramMessage>Send a photo to a chat
createInvoiceLinkcreateInvoiceLink(params: CreateInvoiceLinkParams): Promise<string>Create a Stars invoice link
answerPreCheckoutQueryanswerPreCheckoutQuery(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.

const invoiceUrl = await telegram.createInvoiceLink({
title: 'Premium',
description: '30 days of premium',
payload: JSON.stringify({ plan: 'premium' }),
currency: 'XTR',
prices: [{ label: 'Premium', amount: 100 }],
});
// In your bot webhook handler
await telegram.answerPreCheckoutQuery(query.id, true);
// Or reject with an error message
await telegram.answerPreCheckoutQuery(query.id, false, 'Item out of stock');

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 check
app.get('/api/health', (c) => c.json({ status: 'ok' }));
// Protected routes
app.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.