Endfix.Telegram.BotAPI 0.6.0

dotnet add package Endfix.Telegram.BotAPI --version 0.6.0
                    
NuGet\Install-Package Endfix.Telegram.BotAPI -Version 0.6.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Endfix.Telegram.BotAPI" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Endfix.Telegram.BotAPI" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="Endfix.Telegram.BotAPI" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Endfix.Telegram.BotAPI --version 0.6.0
                    
#r "nuget: Endfix.Telegram.BotAPI, 0.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Endfix.Telegram.BotAPI@0.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Endfix.Telegram.BotAPI&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=Endfix.Telegram.BotAPI&version=0.6.0
                    
Install as a Cake Tool

Telegram Bot API (C#)

Bot%20API Targets NuGet

Typed .NET client for the Telegram Bot API. The package provides native net8.0 and compatible netstandard2.0 assets and uses System.Text.Json for request and response contracts.

Features

  • strongly typed Bot API methods, parameters and response models;
  • polymorphic JSON serialization for Telegram union types;
  • JSON and multipart/form-data requests;
  • local, in-memory and stream-backed uploads, Telegram file IDs and attach:// references;
  • sequential or parallel long polling and an ASP.NET Core webhook example;
  • contract, transport and live Telegram integration tests;
  • BenchmarkDotNet suites and million-request stress profiles.

Status

Use this package when you need a strongly typed, low-level Telegram Bot API client with explicit control over requests, polling, webhooks, transport and serialization.

The project follows the Telegram Bot API release it targets. While the package remains below 1.0, public contracts may still change to correct modeling issues while the stable contract is being finalized. After 1.0, incompatible public API changes will require a major version.

See CHANGELOG.md for release notes and unreleased changes.

Installation

dotnet add package Endfix.Telegram.BotAPI --version 0.6.0

Quick start

Each bot is given a unique authentication token when it is created. Store the token in User Secrets or an environment variable; do not put it in appsettings.json or source control. You can learn about obtaining tokens and generating new ones in this document.

using var httpClient = new HttpClient(new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromSeconds(5),
    MaxConnectionsPerServer = 10
})
{
    Timeout = TimeSpan.FromMinutes(5)
};

using var api = new BotApiClient(
    "<token>",
    httpClient);

var message = await api.SendMessageAsync(
    chatId: 1234567890,
    text: "Hello from Endfix.Telegram.BotAPI");

Custom Bot API base URLs may include a path prefix. For example, passing https://example.com/telegram/ as url sends API requests below that path; the trailing slash is optional.

Receiving updates

Long polling raises OnUpdate for each received update. It processes updates sequentially in FIFO order by default:

api.OnUpdate += async (client, update, cancellationToken) =>
{
    if (update.Message?.Text is not { } text)
        return;

    await client.SendMessageAsync(
        update.Message.Chat.Id,
        $"Echo: {text}",
        cancellationToken: cancellationToken);
};

using var stopping = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
    eventArgs.Cancel = true;
    stopping.Cancel();
};

await api.StartPollingAsync(cancellationToken: stopping.Token);

Set maxParallel above 1 to enable concurrent processing. Use sequential processing for stateful workflows that depend on update ordering.

For webhooks, deserialize an Update at an HTTPS endpoint and validate Telegram's secret-token header before processing it. See the complete examples for both receiving models below.

Uploading files

Typed input files accept a local path or a repeatable InputFileSource. Choose the source according to where the content already lives and how often it will be sent.

Use a path for a large or one-off local file. The source is lazy and does not buffer the complete file in memory:

var document = new InputDocumentFile(
    InputFileSource.FromPath("report.pdf"));

Passing the path directly as new InputDocumentFile("report.pdf") remains a short equivalent. Constructing a path source does not access the filesystem. The file is opened again for every request attempt, and normal FileStream exceptions surface when the request consumes it or when InputFile.GetStream() is called directly.

Use an in-memory source when the content is already available as bytes, or when a small or medium payload is sent repeatedly and the application deliberately wants to avoid opening its file for every attempt:

var document = new InputDocumentFile(
    InputFileSource.FromMemory(reportBytes, "report.pdf"));

await api.SendDocumentAsync(chatId, document);

FromMemory takes its own snapshot of the supplied bytes. Reading a large file into a byte array solely for one upload usually adds memory use and a copy without avoiding the original disk read. Local benchmarks show that an already loaded memory source has much lower request-preparation overhead than opening a path, but they intentionally exclude the cost of loading that byte array and network transfer normally dominates a Telegram upload.

For databases, object storage, generated content and other stream-backed data, provide a factory that returns a new readable stream for each request attempt. In this example, objectStorage represents an application-owned storage client, such as an Amazon S3, Azure Blob Storage or MinIO client:

var photo = new InputPhotoFile(
    InputFileSource.FromStream(
        () => objectStorage.OpenRead("photos/current.jpg"),
        "current.jpg"));

During an API request, the library owns and disposes every stream returned by the factory. The factory may be called repeatedly for retries or repeated requests, and concurrently when the same source is used by parallel requests. Every call must return an independent readable stream. The library reads from its current position without seeking or rewinding, then disposes it. Do not return the same stream instance from multiple calls. Streams opened while retrying the same request must expose equivalent upload content. An exception thrown by the factory propagates to the caller and stops that request, including an automatic retry. Code that calls InputFile.GetStream() directly owns and must dispose the returned stream itself.

Downloading files

Use this method to get basic information about a file and prepare it for downloading. For the moment, bots can download files of up to 20MB in size.

var message = await api.SendDocumentAsync(
    chatId: 1234567890,
    document: new InputDocumentFile("report.pdf"));

var file = await api.GetFileAsync(message.Document!.FileId);
var fileBytes = await api.GetFileBytesAsync(file.FilePath!);

await File.WriteAllBytesAsync("downloaded-report.pdf", fileBytes);

For larger files, stream the response directly to a destination instead of buffering it in a byte[]:

await using var destination = File.Create("downloaded-report.pdf");
await api.DownloadFileAsync(file.FilePath!, destination);

DownloadFileAsync leaves the destination stream open and positioned after the downloaded content.

Production usage

Use one singleton IBotApiClient per bot. In hosted applications, let a singleton BackgroundService own StartPollingAsync, subscribe when the service starts, unsubscribe in finally, and create a DI scope inside the event callback before resolving scoped handlers or database contexts. Never subscribe a scoped or transient object directly to the singleton client's OnUpdate event.

A supplied HttpClient remains owned by the caller and should normally be reused for the application's lifetime. Configure SocketsHttpHandler.PooledConnectionLifetime when pooled connections must be refreshed without replacing the application-level bot client. BotApiClient disposes the HttpClient only when it created that client internally.

The Long Polling example demonstrates the hosted lifetime and scope-per-update pattern without adding DI dependencies to the core package. Both server-side examples pass ILogger<IBotApiClient> from their application's logging pipeline to the client.

Complete examples

The Long Polling and Webhook examples accept TELEGRAM_BOT_TOKEN from an environment variable or .NET User Secrets while retaining their existing appsettings.json keys as a fallback:

dotnet user-secrets set "TELEGRAM_BOT_TOKEN" "<token>" --project Telegram.BotAPI.Examples/LongPolling/Telegram.BotAPI.Example.LongPolling.csproj
dotnet user-secrets set "TELEGRAM_BOT_TOKEN" "<token>" --project Telegram.BotAPI.Examples/Webhook/Telegram.BotAPI.Example.Webhook.csproj

The Long Polling and Webhook projects include placeholder configuration files. Replace the placeholders locally or use User Secrets before running them.

Behavior and guarantees

The following behavior is part of the library's current runtime contract and is covered by deterministic tests. Intentional changes to these guarantees should update both the tests and this documentation.

Concern Guarantee
Telegram API errors RequestAsync returns the response; ExecuteAsync throws ApiRequestException when ok is false.
Rate limits Error 429 is retried after Telegram's retry_after, up to six retries by default.
Other failures Timeouts, cancellation, transport failures and non-429 API errors are not retried automatically.
Polling session Only one session can run per client instance; the client can be started again after that session stops.
Update ordering maxParallel = 1 is sequential and FIFO; parallel mode does not guarantee update start or completion order.
Update handlers Subscribers run in registration order for each update and each returned task is awaited. A failing subscriber does not prevent later subscribers from running.
Delivery Long polling is best effort. Handler failures are not retried and interrupted sessions may receive an update again.
Cancellation The polling token is forwarded to handlers. Caller cancellation and standard timeout or transport exception types are preserved.
External HttpClient Remains owned by the caller and is never disposed by BotApiClient.
Internal HttpClient Is owned by BotApiClient and disposed with it.
Upload source A fresh stream is opened for each attempt and disposed by the request.
Download destination Remains open and positioned after the downloaded content.

Requests and retries

The client automatically retries Telegram responses with error code 429, waiting for the server-provided retry_after interval before the next attempt. It makes at most six retries by default; configure maxRetryAttempts in the constructor or set it to 0 to disable automatic retries. Timeouts, cancellations and other transport failures are not retried automatically because the client cannot know whether Telegram processed the original request.

RequestAsync returns Telegram API responses, while ExecuteAsync throws ApiRequestException when Telegram returns ok = false. Argument errors, caller cancellation, timeouts, HTTP and network failures, and malformed JSON responses retain their standard .NET exception types.

Polling and update handlers

Only one StartPollingAsync session can run on a client instance at a time. A concurrent start fails with InvalidOperationException; after the active session stops, the same client can be started again.

All OnUpdate subscribers are invoked in registration order and each returned task is awaited before processing for that update completes. Handlers receive the polling session's cancellation token so long-running work can stop cooperatively. A failing subscriber is logged without preventing later subscribers from running; cancellation caused by the polling token is treated as a normal shutdown.

StartPollingAsync uses best-effort delivery. Handler failures are logged and are not retried; a failed update is confirmed if the polling loop later sends a higher offset. Because the offset is neither sent until the next getUpdates request nor persisted by the client, an interrupted polling session may receive an update again even after its handler ran. Applications that require durable processing or explicit delivery guarantees should own the GetUpdatesAsync loop and persist their checkpoint explicitly.

Resource ownership

Path-based upload sources are lazy. Stream factories may be called repeatedly for retries or repeated requests, and concurrently when the same source is used by parallel requests. Each call must return an independent readable stream with equivalent content. The library disposes streams returned to an API request; code that calls InputFile.GetStream() directly owns that returned stream.

The client never disposes a supplied HttpClient. If no client is supplied, BotApiClient creates one and disposes it with the bot client. Streaming file downloads never dispose the caller's destination stream.

Runtime compatibility

The package targets net8.0 and netstandard2.0. NuGet selects the native net8.0 asset for .NET 8 and later applications. Older runtimes use the compatible netstandard2.0 fallback; this is a library target, not a requirement to install a particular .NET SDK in the consuming application.

Consumer target Status Guidance
.NET 10, .NET 9, .NET 8 Supported via net8.0 Recommended targets for new applications.
.NET 7, .NET 6 Compatible via netstandard2.0 CI executes the packaged fallback on .NET 6. These runtimes are end of support and should be used only where an application is intentionally pinned to them.
.NET 5, .NET Core 3.1 NuGet asset compatibility only These runtimes are not exercised or supported. Treat them as migration targets.
.NET Core 2.0 through 2.2 Compatible in principle NuGet compatibility is possible through netstandard2.0, but this is not a current CI target.
.NET Framework 4.7.2 through 4.8.1 Compatible Practical choice for maintained classic Windows applications.
.NET Framework 4.6.2 through 4.7.1 Package-dependent May resolve the package graph, but is not a recommended baseline for new builds.
.NET Framework 4.6.1 Not a recommended baseline Although .NET Standard compatibility tables list it, Microsoft documents compatibility issues for consuming higher .NET Standard libraries from this framework version.

The compatibility column describes framework and NuGet asset compatibility; it is not a claim that every listed runtime is actively tested by this repository. The test and example projects exercise the net8.0 asset, while CI compiles and package-validates both library targets.

NuGet does not need a dedicated net6.0 or net7.0 asset to resolve this package: applications targeting those frameworks select netstandard2.0. CI executes that packaged fallback on .NET 6 as a compatibility canary; this does not extend the support lifetime of the consuming runtime.

Dependency footprint

.NET 8 and later consumers select the net8.0 asset and use the runtime-provided System.Text.Json. The netstandard2.0 fallback references System.Text.Json 8.0.6 and its compatibility dependencies for older .NET and .NET Framework consumers. NuGet resolves that graph automatically; an application normally should not pin its transitive packages manually.

The package does not claim trimming or Native AOT compatibility and has no platform-specific runtime identifier or operating-system requirement. The consuming runtime must still support the selected .NET target and dependency graph.

Development

Repository builds are pinned to .NET SDK 9.0.317 through global.json. The complete solution also runs net8.0 tests and examples, so install the .NET 8 SDK or runtime alongside SDK 9 when working locally. CI installs both SDK versions explicitly.

Tests

Run the solution test suite (live integration tests are skipped when their secrets are not configured):

dotnet test Telegram.BotAPI.sln

Telegram integration tests are disabled unless their credentials are configured. They can send real messages and are intended for a dedicated test bot and chat.

Run local tests without contacting Telegram:

dotnet test Telegram.BotAPI.Tests/Telegram.BotAPI.Tests.csproj --filter "Category!=Integration"

See the test project guide for the live test topology, BotFather settings, administrator rights, secrets, rollback behavior and focused run commands.

CI also installs the locally packed NuGet package into two small consumer applications. One executes the native net8.0 asset on .NET 8; the other executes the netstandard2.0 fallback in a .NET 6 Docker container. These smoke tests use an in-memory HTTP handler and require neither a bot token nor network access to Telegram. See the package smoke guide for the exact checks and local commands.

Benchmarks

The benchmark project contains serialization, rich-message and local transport benchmarks, plus sequential and bounded-parallel stress profiles:

dotnet run --project Telegram.BotAPI.Benchmarks/Telegram.BotAPI.Benchmarks.csproj -c Release -- --filter * --join
dotnet run --project Telegram.BotAPI.Benchmarks/Telegram.BotAPI.Benchmarks.csproj -c Release -- --stress
dotnet run --project Telegram.BotAPI.Benchmarks/Telegram.BotAPI.Benchmarks.csproj -c Release -- --stress-parallel 10

Latest benchmark snapshot

The current reference snapshot was recorded on September 15, 2026 at commit a229efd (v0.5.0-16-ga229efd) on .NET 9.0.20, Windows 10 x64, and an Intel Xeon E5-2690 v3. The benchmarks use local transports and exclude Telegram and network latency.

Scenario Mean Allocated
Serialize parameters 537.8 ns 264 B
Deserialize message 1.310 us 1,712 B
Request and deserialize 6.633 us 5,264 B
Request without parameters and deserialize 3.278 us 3,680 B
Send scalar message 4.626 us 3,232 B
Prepare one local-file photo 219.252 us 3,768 B
Prepare 10-part path-backed media group 2.047 ms 28,249 B

The complete environment, interpretation, all 82 measurements, focused repeats, and million-call stress profiles are in the latest benchmark report, with normalized measurements in benchmark-data.json.

API documentation source

Descriptions of Telegram objects, fields and method parameters are aligned with the official Bot API documentation. The current repository snapshot targets Bot API 10.3 (published August 24, 2026). Telegram may clarify or extend descriptions without changing a .NET type, so the source version and date should be reviewed when updating XML documentation. Library-specific behavior, such as stream ownership, retryability and polling semantics, is documented locally and is not copied from Telegram's object descriptions.

Releases

Push the intended release commit to main and wait for CI to pass before creating a vX.Y.Z tag. Pushing the tag starts the publish workflow, which independently restores, builds, tests, packs with the version derived from the tag, publishes the package to NuGet, and creates the corresponding GitHub Release from that version's CHANGELOG.md section.

License

This project is licensed under the MIT License.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.6.0 40 9/15/2026
0.5.0 85 9/9/2026
0.4.0 80 9/1/2026
0.3.1 90 8/31/2026
0.3.0 83 8/31/2026
0.2.0 99 8/28/2026
0.1.0 94 8/27/2026