How Telegram Desktop Processes Deep Links: A Complete Technical Guide

Telegram Desktop routes deep links through a centralized DeepLinks::Router that parses URLs into section/path/context pairs and dispatches them to registered handlers in core/deep_links/ modules.

The telegramdesktop/tdesktop repository implements a modular deep link processing pipeline that converts raw tg:// URLs into concrete application actions. This system handles everything from settings screens to bot interactions through a lightweight router architecture defined in the core/deep_links/ directory.

Entry Points: How Deep Links Enter the Application

Deep links reach the application through three primary channels, all converging on a single routing function.

When the OS launches Telegram with a tg:// URL scheme, Core::Application::handleUrl (defined in main/main_app_config.cpp) forwards the command to TryRouterForLocalUrl. This same function handles in-app links (such as t.me/... or internal tg://resolve?... URLs) parsed by Core::LocalUrlHandlers. Internal media URLs like tg://open?url=… also flow through this pipeline.

The common entry point is TryRouterForLocalUrl in core/local_url_handlers.cpp:

bool TryRouterForLocalUrl(
        Window::SessionController *controller,
        const QString &command) {
    return DeepLinks::Router::Instance().tryHandle(controller, command);
}

Parsing and Context Creation

Before routing occurs, the raw command string undergoes structured parsing. The ParseCommand function in core/deep_links/deep_links_router.cpp splits the URL into section, path, and query parameters, stripping leading slashes and separating the query string into a key-value map.

The resulting data populates the Context struct defined in core/deep_links/deep_links_types.h:

struct Context {
    Window::SessionController *controller = nullptr;
    QString section;
    QString path;
    QMap<QString, QString> params;
};

This context object encapsulates everything the router needs: the active session controller, the target section (e.g., "settings"), the specific path (e.g., "privacy"), and any query parameters passed by the link.

The Router Architecture

Core::DeepLinks::Router maintains a map of section → list<Entry>, where each Entry couples a path pattern with an Action variant and behavioral flags.

The Entry struct (defined in the router headers) contains:

struct Entry {
    QString path;
    Action action;
    bool requiresAuth = true;
    bool skipActivation = false;
};

The Action type is a std::variant supporting:

  • SettingsSection – opens a specific settings page
  • SettingsControl – highlights a UI control within settings
  • CodeBlock – executes a custom lambda handler
  • AliasTo – forwards to another section/path combination

Processing occurs in three stages:

  1. dispatch – locates the matching Entry for the supplied section/path pair
  2. executeAction – invokes the specific action (opening UI, running code, or redirecting)
  3. Result handling – returns Result::Handled, Result::NeedsAuth, Result::NotFound, or Result::Unsupported

If the result is Handled and the entry does not set skipActivation, the router automatically brings the application window to the foreground.

Handlers register during application startup through module-specific Register…Handlers functions called from the router’s constructor.

The RegisterSettingsHandlers function in core/deep_links/deep_links_settings.cpp declares all settings-related routes:

void RegisterSettingsHandlers(Router &router) {
    router.add(u"settings"_q, {
        .path = u"privacy"_q,
        .action = SettingsSection{ ::Settings::PrivacySecurityId() },
    });
    router.add(u"settings"_q, {
        .path = u"language"_q,
        .action = CodeBlock{ [](const Context &ctx) {
            return ShowLanguageBox(ctx);
        }},
        .requiresAuth = false,
    });
}

This registration pattern allows tg://settings/privacy to open the Privacy & Security screen, while tg://settings/language executes the ShowLanguageBox lambda without requiring authentication.

Other Handler Modules

Complete Processing Flow Example

Consider the user clicking tg://settings/language?lang=es:

  1. The OS launches Telegram with the URL string as an argument
  2. Application::handleUrl calls TryRouterForLocalUrl(controller, "tg://settings/language?lang=es")
  3. Router::tryHandle invokes ParseCommand, producing Context{ section="settings", path="language", params={"lang":"es"} }
  4. dispatch queries the section map for "settings", finding the entry with path="language"
  5. executeAction runs the registered CodeBlock which calls ShowLanguageBox(ctx)
  6. The language picker UI opens, potentially pre-highlighting Spanish based on the query parameter
  7. The router returns Result::Handled and activates the main window

If the link targeted tg://settings/privacy while the user was logged out, the router would return Result::NeedsAuth and the action would not execute.

Internal vs. External URL Handlers

When the deep link router fails to find a matching section, control falls through to internal handlers defined in core/local_url_handlers.cpp. These handle specialized URL patterns not routed through the standard pipeline:

  • ^media_timestamp/?\?base=…&t=(\d+) – OpenMediaTimestamp jumps to specific timestamps in media files
  • ^url:(.+)$ – OpenExternalLink forwards to the system default browser
  • ^copy:(.+)$ – CopyPeerId copies numeric peer IDs to the clipboard

These patterns are evaluated after the router returns NotFound, ensuring all tg:// URLs receive appropriate handling.

Summary

  • Entry Point: All deep links converge on TryRouterForLocalUrl in core/local_url_handlers.cpp, which delegates to DeepLinks::Router::Instance().tryHandle()
  • Parsing: ParseCommand in core/deep_links/deep_links_router.cpp decomposes URLs into Context objects containing section, path, and parameters
  • Architecture: The router maintains a section-indexed map of Entry structs containing Action variants (SettingsSection, CodeBlock, etc.) and flags for authentication requirements
  • Registration: Module-specific files (deep_links_settings.cpp, deep_links_chats.cpp, etc.) register handlers during router construction
  • Results: The router returns distinct results (Handled, NeedsAuth, NotFound) and manages window activation based on skipActivation flags
  • Fallback: Non-router patterns are handled by internal URL handlers in the same local URL handlers file

Frequently Asked Questions

What is the primary entry point for deep link processing in Telegram Desktop?

The primary entry point is the TryRouterForLocalUrl function located in Telegram/SourceFiles/core/local_url_handlers.cpp. This function is called by Core::Application::handleUrl when the OS launches the app with a tg:// URL, and by Core::LocalUrlHandlers when processing in-app links. It delegates to DeepLinks::Router::Instance().tryHandle() to perform the actual routing.

How does the router determine which handler to execute?

The router uses a two-level lookup system. First, it extracts the section from the parsed URL (the component immediately following tg://). It then searches the list of Entry objects registered for that section, matching the path component against each entry's path string. The first matching entry's Action variant (SettingsSection, CodeBlock, etc.) is then executed via executeAction.

Yes, but only for handlers explicitly registered with requiresAuth = false. For example, the language settings handler in deep_links_settings.cpp sets this flag to allow tg://settings/language to function before login. Links targeting protected areas like tg://settings/privacy return Result::NeedsAuth when no session exists, effectively queuing or ignoring the request until authentication completes.

New handlers are added in the module-specific registration files within Telegram/SourceFiles/core/deep_links/. Settings handlers belong in deep_links_settings.cpp, contact-related handlers in deep_links_contacts.cpp, chat and username resolution in deep_links_chats.cpp, and experimental features in deep_links_new.cpp. Each file exports a Register…Handlers function that populates the router's section map during application initialization.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →