# tdl wraps TDLib so a Node process can speak the Telegram API

> tdl is a JavaScript wrapper around the Telegram Database library that loads the tdjson shared library dynamically, exposes TDLib methods as one invoke call, and ships with type definitions, prebuilt native addons and a separate package for prebuilt TDLib binaries.

**eilvelia/tdl** — Node.js bindings to TDLib 🥒

- Repository: https://github.com/eilvelia/tdl
- Stars: 537 · Forks: 63
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/eilvelia-tdl

## One shared library, loaded at runtime

tdl is a fairly simple JavaScript wrapper for TDLib, the Telegram Database library, which is the library for creating Telegram clients or bots. The wrapper is thin by design, and the part that matters is how it finds the native library.

TDLib is not compiled into the npm package. Instead, tdl loads the tdjson shared library dynamically at runtime, and the library name is platform specific: libtdjson.so on Linux, libtdjson.dylib on macOS and tdjson.dll on Windows, from TDLib version 1.8.0 or newer. The configuration call decides where that library comes from, and the three options are a path to a specific file, a library directory to search in, or nothing at all, in which case the system search paths are used.

That split is the whole installation story. You install the wrapper with npm install tdl, and you separately get a TDLib, either from the prebuilt-tdlib package or by building TDLib yourself. The wrapper is a node addon, and in rare cases you also need a C++ compiler and Python to build it.

## Prebuilt addons cover most platforms, and arm64 is the gap

The packaging footnote explains what you get for free. tdl ships pre-built addons for Windows on x86_64, for Linux on x86_64 and arm64 with glibc 2.22 or newer or musl, and for macOS on x86_64 and arm64 with version 11.0 or newer. If a pre-built binary is not available for your system, the addon is built with node-gyp, which needs Python and a C++ toolchain, with C++14 required, or MSVS or Build Tools on Windows.

Two details in that footnote are worth holding on to. A flag named --build-from-source exists to never use the pre-built binaries, which is what you want when a prebuilt binary misbehaves on your platform. And arm64 binaries are not tested in CI, so the arm64 path is the one to verify yourself if you are on an Apple Silicon or ARM Linux machine.

There is a related point about linking. The Linux binaries are statically linked against libstdc++, which is what makes a single file work on distributions that would otherwise need a matching runtime version. The prebuilt-tdlib package covers a similar platform matrix for the TDLib libraries themselves, on x86_64 and arm64 Linux, x86_64 and arm64 macOS, and x86_64 Windows.

## Configure, create a client, listen for updates

The getting started example is one long script and it shows the whole shape of the library. The first thing is the configuration, which points tdl at a tdjson library, in the example the one that comes with the prebuilt-tdlib package:

```javascript
// Import using CommonJS:
const tdl = require('tdl')

// Configure tdl to use the tdjson shared library from the prebuilt-tdlib
// package (should be installed separately)
const { getTdjson } = require('prebuilt-tdlib')
tdl.configure({ tdjson: getTdjson() })
```

Then a client is created with the apiId and apiHash, which the README says are mandatory and which you obtain from the Telegram site. The example uses placeholder values, 2222 and a hex string, which is the usual way to show the shape without shipping real credentials.

```javascript
// Create a client. It is mandatory to pass apiId and apiHash, these can be
// obtained at https://my.telegram.org/
const client = tdl.createClient({
  apiId: 2222, // Replace with your api_id
  apiHash: '0123456789abcdef0123456789abcdef' // Replace with your api_hash
})

client.on('error', console.error)
```

The rest of the example is the event loop of a Telegram client. Updates are pushed to you as events, which is the model TDLib uses for anything that happens without you asking.

## invoke is the whole method surface

Every TDLib method goes through a single call, client.invoke, with an object whose first field is the method name. The getting started example calls getMe with no arguments, then getChats with a chat list and a limit of 10, and logs both.

```javascript
  // Call a TDLib method. The information regarding TDLib method list and
  // documentation is below this code block.
  const me = await client.invoke({ _: 'getMe' })
  console.log('My user:', me)

  // Invoke some other TDLib method.
  const chats = await client.invoke({
    _: 'getChats',
    chat_list: { _: 'chatListMain' },
    limit: 10
  })
  console.log('A part of my chat list:', chats)
```

That single-method-call design is what makes the wrapper small. There is no per-method JavaScript function to generate and maintain, and the method reference stays in TDLib's own documentation and in the td_api.tl schema file, which is written in the TL language. The README points at three places to read the method list, the annotated documentation, the schema file, and the TypeScript definitions if they are installed, since the types carry JSDoc comments and can be browsed from the editor's autocompletion.

One naming detail is worth knowing: the TDLib JSON interface actually sends a field called @type, and tdl renames it to _. So the leading character you see on every call is this wrapper's convention, not Telegram's.

## Type mapping, login, and the closing handshake

The type mapping between the TDLib schema and JavaScript is short and has two traps. In the schema the bytes type means a base64-encoded string, so a bytes field arrives as a string you have to decode yourself. The int64 type accepts either a number or a string, and the README says to pass a string for large numbers, because a JavaScript number cannot hold the full range. The int32, int53 and double types are all mapped to the plain number type.

Login is handled by the wrapper rather than by you, with client.login asking for the phone number and the rest in the console when called with no arguments. If you are logging in as a bot instead of a user, there is a separate call for a bot token.

The end of the example closes the client, and the comment explains why it matters. Closing the instance lets TDLib finish gracefully and lets the JavaScript runtime exit the process, which is the difference between a script that terminates and one that hangs on an open native handle.

```javascript
  // Close the instance so that TDLib finishes gracefully and the JS runtime can
  // exit the process.
  await client.close()
}

main().catch(console.error)
```

The CommonJS require in the example is not the only import style. You can import the package as an EcmaScript module through the CommonJS interoperability, either with a namespace import or by importing only the functions you need.

## Types, workspaces and a monorepo with three packages

The repository is an npm workspace rather than a single package. The workspace holds packages/tdl, which is the wrapper, packages/prebuilt-tdlib, which publishes the TDLib libraries, and packages/tdl-install-types, which handles the type installation. The root package is a private one named tdl-dev at version 0.0.0, and the top-level entries also include a packages directory, a typings directory, a flow-typed directory, an examples directory, tests, scripts and a vitest configuration.

The presence of both TypeScript and Flow is unusual and tells you the project cares about the type story in more than one ecosystem. The check script runs tsc, then flow check, then eslint with warnings treated as errors. The test scripts are split by kind, with unit tests, integration tests and authenticated tests as separate commands, and the unit run excludes the other two directories explicitly, so a contributor can run the fast suite without Telegram credentials.

For TypeScript users the types come from a separate package, tdlib-types, which can be imported with import type * as Td. The README has a section on generating types, and a more exhaustive API reference lives in the TypeScript declaration file at packages/tdl/index.d.ts.

## Other runtimes, and where the wrapper stops

The repository topics list Bun and Deno alongside Node, and the examples directory includes a deno-example.ts, so the wrapper is intended to work outside Node as well as inside it. The examples directory also holds a get-chats script, a send-message script, a custom-login script, a multiple-accounts script, an async-iterators example and a TypeScript example, which is a decent map of the use cases people actually have.

The first limitation is the one the project itself is honest about, that you need a tdjson shared library at runtime. A deployment that cannot ship a native library with the application, a bundler that cannot handle a native addon, or a serverless platform with no filesystem for a shared library, are all places where this wrapper will not run. The fix in each case is a different design, not a configuration flag.

The second is scope. tdl is a TDLib wrapper, and TDLib is a full Telegram client library with its own state, its own database and its own update stream. If what you need is a bot that sends a message when something happens, the Bot API with a token is far simpler and needs no native library, no apiId and apiHash and no shared library at all. tdl earns its complexity when you need a user session, a local database, or the full method set.

The third is the version floor. The README asks for Node.js v16 or newer, while the workspace manifest requires Node 20.17.0 or newer through its engines field, so provision against the manifest rather than the prose.

## Conclusion

tdl fits a Node, Deno or Bun project that needs a real Telegram client rather than the Bot API, especially one that already has a TDLib binary available, since the wrapper adds a typed invoke interface rather than a new protocol layer. It is the wrong choice for a bot that only needs to send messages, where the Bot API token is enough, and it is the wrong choice on a platform with no prebuilt addon and no toolchain to build one. Before you install it, check three things: that a TDLib 1.8.0 or newer tdjson shared library is reachable at runtime, since tdl loads it dynamically rather than bundling it, that your Node version satisfies the engine requirement of 20.17.0 or newer, which is stricter than the v16 minimum the README mentions, and that you know which TDLib version your client is talking to, since prebuilt-tdlib ships a specific one. The code is MIT licensed, there are no GitHub releases, and the last push was on 2026-07-23.

## FAQ

### How do I install tdl and get TDLib for it?

Install the wrapper with `npm install tdl`, and separately install the pre-built TDLib libraries with `npm install prebuilt-tdlib` or build TDLib yourself. The wrapper loads the tdjson shared library dynamically at runtime, so TDLib 1.8.0 or newer has to be present.

### How do I call a TDLib method from JavaScript?

Through one call, `client.invoke`, with an object whose first field names the method, for example `client.invoke({ _: 'getMe' })`. The TDLib JSON interface sends a field called @type, and tdl renames it to `_`.

### Which platforms have pre-built addons for tdl?

Windows on x86_64, Linux on x86_64 and arm64 with glibc 2.22 or newer or musl, and macOS on x86_64 and arm64 with version 11.0 or newer. If none fits, the addon is built with node-gyp, needing Python and a C++14 toolchain, and `--build-from-source` forces that path. arm64 binaries are not tested in CI.

### How are large integers and byte strings typed in tdl?

In the TDLib schema the bytes type means a base64-encoded string. The int64 type accepts a number or a string, and the README says to pass a string for large numbers, while int32, int53 and double map to the plain number type.

### Can tdl log in as a bot as well as a user?

Yes. `client.login()` with no arguments asks for the phone number and the rest in the console, and there is a separate call for logging in as a bot with a bot token.

## Sources

- [eilvelia/tdl on GitHub](https://github.com/eilvelia/tdl)
- [Issues](https://github.com/eilvelia/tdl/issues)
- [License: MIT](https://github.com/eilvelia/tdl/blob/develop/LICENSE)
- [README](https://github.com/eilvelia/tdl/blob/develop/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/eilvelia-tdl
