DEV Community

Cover image for Telegram's username field can be an empty string — and which handle to use instead
Oleg Starnikov
Oleg Starnikov

Posted on Originally published at tgatlas.org

Telegram's username field can be an empty string — and which handle to use instead

Ask the Telegram API for @durov and read the username field. It comes back as "".

Not null, not missing. An empty string, on the account of the person who founded Telegram. If your code does chat.username or chat.id and builds a link out of the result, you have just produced t.me/1006503122, which resolves to nothing for a human being.

It is not a bug, and the reason is worth five minutes. Every response below is live output, captured on 16 September 2026.

A channel can hold more than one handle

Telegram lets a chat own several public handles. When it does, the primary username field goes empty and the handles move into a usernames array, each entry carrying its own flags.

curl 'https://telegram155.p.rapidapi.com/v1/usernames/durov' \
  --header 'x-rapidapi-key: YOUR_KEY' \
  --header 'x-rapidapi-host: telegram155.p.rapidapi.com'
Enter fullscreen mode Exit fullscreen mode
{
  "chats": [
    {
      "id": 1006503122,
      "username": "",
      "usernames": [
        { "username": "durov", "active": true, "editable": true,  "flags": 3 },
        { "username": "rove",  "active": true, "editable": false, "flags": 2 },
        { "username": "paul",  "active": true, "editable": false, "flags": 2 },
        { "username": "snow",  "active": true, "editable": false, "flags": 2 },
        { "username": "feed",  "active": true, "editable": false, "flags": 2 },
        { "username": "lean",  "active": true, "editable": false, "flags": 2 }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Six handles. All six active. Exactly one also editable, and that one is durov.

The flags number is the same information in bitfield form: 2 is the active bit alone, 3 is active plus editable. You never need to decode it, the booleans are already there — but seeing 3 against 2 tells you which entry is canonical at a glance.

So the rule is not "take the first active handle". It is take the active and editable one. That is the handle the owner controls and the one that belongs in a link. The other five are aliases: they work, but they are not the name.

The opposite case, from the same endpoint

curl 'https://telegram155.p.rapidapi.com/v1/usernames/telegram' \
  --header 'x-rapidapi-key: YOUR_KEY' \
  --header 'x-rapidapi-host: telegram155.p.rapidapi.com'
Enter fullscreen mode Exit fullscreen mode
{ "chats": [ { "id": 1005640892, "username": "telegram", "usernames": null } ] }
Enter fullscreen mode Exit fullscreen mode

One handle, sitting in username, and usernames is null. Both shapes are normal and both arrive from the same route, so code that handles only one of them is wrong half the time.

Resolving it once, properly

def handle(chat):
    """The handle a link should use: active and editable, then any active, then the plain field."""
    names = chat.get("usernames") or []

    for u in names:
        if u.get("active") and u.get("editable"):
            return u["username"]

    for u in names:
        if u.get("active"):
            return u["username"]

    return chat.get("username") or None
Enter fullscreen mode Exit fullscreen mode

Three things worth noticing in nine lines.

or [] on the array, because usernames is null rather than empty for single-handle channels — a plain for u in chat["usernames"] raises on the majority case.

The two loops in that order, because "editable" is the distinguishing bit and "active" alone is not. Collapse them into one loop and you will return an alias whenever it happens to sort first.

And None at the end rather than the numeric id, because a link built from an id is worse than no link: it looks valid, it renders, and it goes nowhere.

Where this bites in practice

Anywhere you turn stored data back into a link: a digest email listing channels, a dashboard row, a CSV export someone opens next month. The case that produces it is a channel that renamed itself and kept the old handles working — exactly when username empties out and nobody notices until the links ship.

The check is cheap: for every chat you store, assert that the handle you saved is non-empty and is not the id in string form. One line in a test, and a class of dead links never reaches anyone.

Resolving a handle is a single request, and the response carries whichever shape that channel has. We run this as an HTTP API over public Telegram data — no phone number, no session file. The free plan is 2,500 calls a month with no card, so mapping a few hundred channels and getting this right costs nothing: full endpoint reference, 19 routes, one API key.

Top comments (0)