# A fully-generated,
strongly-typed Go client
for the Telegram Bot API

176 methods. 1408 generated tests. Zero `any` in the public surface.

Get started View on GitHub
176 methods · 301 types · 1,408 tests · MIT licensed
echo_bot.go

    package main

    import (
        "context"
        "log"
        "os"

        "github.com/lukaszraczylo/go-telegram/api"
        "github.com/lukaszraczylo/go-telegram/client"
        "github.com/lukaszraczylo/go-telegram/dispatch"
    )

    func main() {
        bot, _ := client.NewRetryDoer(os.Getenv("BOT_TOKEN"), nil)
        d := dispatch.New(bot)

        d.OnMessage(func(ctx context.Context, msg *api.Message) {
            bot.SendMessage(ctx, &api.SendMessageParams{
                ChatID: api.ChatIDFromInt(msg.Chat.ID),
                Text:   msg.Text,
            })
        })

        if err := d.Run(context.Background()); err != nil {
            log.Fatal(err)
        }
    }

## Features

Built for correctness, composability, and production reliability

### Generated from the live docs

IR + emitter pipeline runs weekly; a PR opens automatically for any Telegram-side change.

### No `any` in the public API

Typed unions — `ChatID`, `MessageOrBool` — sealed interfaces with marker methods and auto-decode.

### Pluggable transport + codec

Drop in fasthttp, sonic, or goccy/go-json with a one-line swap.

### Production-ready out of the box

Retry middleware honouring `retry_after`, panic recovery, structured errors with sentinels.

### Typed dispatcher

Generic `Handler[T]`, composable filters, conversation handler with pluggable storage, per-update goroutine pool.

### Self-verifying codegen

Every regen runs scrape → audit → emit → 1408 generated tests. Nothing ships without passing.

## Comparison

How go-telegram compares to other Go Telegram bot libraries

#### go-telegram

HTML scraper · this library

✓Generated from spec (HTML)
✓Typed unions, no any
✓1,408 auto-generated tests
✓Conversation handler
✓Retry middleware (retry_after)
✓Pluggable JSON codec
gtb

#### gotgbot/v2

JSON spec · popular library

✓Generated from spec (JSON)
△Partial typed unions
✗Auto-generated tests
✓Conversation handler
△User-implemented retry
✗Pluggable JSON codec
tba

#### telegram-bot-api/v5

Hand-coded

✗Generated from spec
✗Typed unions
✗Auto-generated tests
✗Conversation handler
✗Retry middleware
✗Pluggable JSON codec

Feature | go-telegram | gotgbot/v2 | telegram-bot-api/v5
--- | --- | --- | ---
Generated from spec | ✓ HTML scraper | ✓ JSON spec | ✗ hand-coded
Typed unions (no `any`) | ✓ ChatID, MessageOrBool, sealed interfaces | △ partial | ✗
Auto-generated tests | ✓ 1,408 (8 scenarios/method) | ✗ | ✗
Conversation handler | ✓ pluggable storage | ✓ | ✗
Retry middleware | ✓ honours retry_after | △ user-implemented | ✗
Pluggable JSON codec | ✓ | ✗ | ✗

## Installation

One command. No CGO, no system deps.

### go get

Requires Go 1.21+

`go get github.com/lukaszraczylo/go-telegram`

### pkg.go.dev

Full API reference

View on pkg.go.dev

### Markdown reference

Auto-generated, browse on GitHub

Browse reference docs

### Telegram Bot API reference

Upstream spec this library tracks

core.telegram.org/bots/api

## Usage

Echo bot — the minimal working example

echo_bot.go

    package main

    import (
        "context"
        "log"
        "os"

        "github.com/lukaszraczylo/go-telegram/api"
        "github.com/lukaszraczylo/go-telegram/client"
        "github.com/lukaszraczylo/go-telegram/dispatch"
    )

    func main() {
        // NewRetryDoer wraps the default transport with retry middleware
        // that honours Telegram's retry_after field automatically.
        bot, err := client.NewRetryDoer(os.Getenv("BOT_TOKEN"), nil)
        if err != nil {
            log.Fatal(err)
        }

        d := dispatch.New(bot)

        // Handler[T] is generic — the type parameter is the concrete update type.
        d.OnMessage(func(ctx context.Context, msg *api.Message) {
            _, err := bot.SendMessage(ctx, &api.SendMessageParams{
                // ChatIDFromInt returns a typed ChatID — no interface{} here.
                ChatID: api.ChatIDFromInt(msg.Chat.ID),
                Text:   msg.Text,
            })
            if err != nil {
                log.Printf("send error: %v", err)
            }
        })

        if err := d.Run(context.Background()); err != nil {
            log.Fatal(err)
        }
    }

### Codegen pipeline

The emitter scrapes the live Telegram Bot API docs (HTML), builds an intermediate representation (`api.json`), then generates Go code and tests. An audit step validates every method signature against the IR before emission.

HTML docs
→
IR (api.json)
→
audit
→
Go code + tests
`make snapshot && make regen`

## Examples

14 runnable bots covering the most common patterns

### echo

Minimal echo bot — get up and running in 30 lines.

### callback

Inline keyboard buttons and callback query handling.

### inline

Inline mode queries and result sets.

### conversation

Multi-step conversation flows with pluggable state storage.

### stateful

Per-user state machine pattern.

### admin

Admin commands — ban, kick, restrict members.

### middleware

Composable middleware chain — logging, auth, rate limiting.

### files

Upload and download photos, documents, audio.

### webhook

Webhook server instead of long-polling.

### polls

Send polls and handle poll-answer updates.

### payments

Telegram Payments — invoices, pre-checkout, successful payment.

### pagination

Paginated inline keyboards for long result sets.

### welcome

Greet new members joining a group or channel.

### moderation

Automated content moderation with filter chains.

## Advanced

Power-user patterns — expand to read

### Conversation flows

The conversation handler chains multiple message steps and stores state between them. Any storage backend implementing the `StateStorage` interface works — in-memory, Redis, Postgres.

    conv := conversation.New(storage)

    conv.AddState("ask_name", func(ctx context.Context, msg *api.Message) (string, error) {
        bot.SendMessage(ctx, &api.SendMessageParams{
            ChatID: api.ChatIDFromInt(msg.Chat.ID),
            Text:   "What's your name?",
        })
        return "ask_age", nil
    })

    conv.AddState("ask_age", func(ctx context.Context, msg *api.Message) (string, error) {
        name := conv.GetData(ctx, "name")
        // ... handle age input
        return conversation.Done, nil
    })

    d.OnMessage(conv.Handler("start"))

### Custom filters

Filters compose with `And`, `Or`, `Not`. A filter is just a function `func(*api.Update) bool`.

    // Only handle private messages from admins
    adminOnly := filters.And(
        filters.IsPrivate,
        filters.UserIDIn(adminIDs...),
    )

    d.OnMessage(func(ctx context.Context, msg *api.Message) {
        // handler body
    }, adminOnly)

    // Custom filter — any function works
    isLong := func(u *api.Update) bool {
        return u.Message != nil && len(u.Message.Text) > 200
    }
    d.OnMessage(handleLongMsg, isLong)

### Custom HTTP / JSON codec

Pass a `transport.Options` to swap the HTTP client or JSON codec. Useful for squeezing throughput on high-volume bots.

    import (
        "github.com/lukaszraczylo/go-telegram/client"
        "github.com/lukaszraczylo/go-telegram/transport"
        jsoniter "github.com/json-iterator/go"
    )

    opts := &client.Options{
        Transport: transport.NewFasthttpTransport(nil),
        Codec:     transport.JSONCodec(jsoniter.ConfigCompatibleWithStandardLibrary),
    }

    bot, err := client.NewRetryDoer(token, opts)