What it means
A peer in Telegram is anything you can hold a conversation with: a person, a small group, or a channel (which includes supergroups). A peer ID is the number that identifies one of them.
The subtlety is that Telegram has two APIs and they disagree about what that number looks like. In MTProto, the peer is a tagged union: peerUser(user_id), peerChat(chat_id), and peerChannel(channel_id). The type tag carries the namespace, so the raw numbers are allowed to overlap. User 42, basic group 42, and channel 42 can all exist at the same time and mean three unrelated things.
The Telegram Bot API has no type tag. It has a single field called chat_id, and it has to make the three namespaces fit into it without collisions. That is where the encoding comes from, and it is why a group ID in a webhook payload looks like -1001234567890 while the same group in an MTProto trace looks like 1234567890.
The encoding, precisely
Users keep their number and stay positive. Basic groups have their sign flipped. Channels and supergroups have 1000000000000 added and then the whole thing negated, which produces the familiar -100 prefix when you read the digits left to right.
The reverse conversions matter just as much, because half of the Telegram ecosystem speaks one dialect and half speaks the other. A deep link like t.me/c/1234567890/42 uses the raw channel ID with no prefix at all, so building that URL from a Bot API chat_id means stripping the prefix first. Get that wrong and every "open in Telegram" link in your CRM 404s.
The 52-bit problem
Telegram IDs used to be comfortably inside a 32-bit signed integer, and a great deal of software was written on that assumption. That stopped being true. Bot API 5.0 carried an explicit warning that user IDs could now exceed 32 bits and might use up to 52 significant bits, with the same warning later extended to supergroup and channel IDs.
Fifty-two bits is a very specific number, and it is not an accident: it is what fits exactly inside the mantissa of an IEEE 754 double-precision float. Telegram chose it so that JavaScript, PHP and every other language whose default numeric type is a double can still represent a Telegram ID without losing digits.
The practical consequences:
- Database columns are
BIGINT, neverINTEGER. A 32-bit column truncates silently and you discover it months later as contacts that cannot be messaged. - In C#,
long. In PostgreSQL,bigint. In TypeScript, anumberis fine for IDs but not for the matching access hash, which is a full 64-bit value and does overflow a double. - Any JSON boundary in your stack is a risk. If an int64 hash rides through a JavaScript parser, the low digits change and nothing logs an error.
Migration: the ID that dies
Peer IDs are stable, with exactly one important exception. When a basic group is upgraded to a supergroup, Telegram does not change the type of the existing object. It creates a new channel, copies the members across, and deactivates the old chat.
The old chat receives a messageActionChatMigrateTo(channel_id) service message and the new supergroup opens with a messageActionChannelMigrateFrom(title, chat_id). Those two service messages are the only bridge between the old ID and the new one, and a client that ignores them is left with a stored ID that now resolves to a dead group.
For a CRM this is a data-integrity event, not a cosmetic one. Every conversation, note, and pipeline card attached to the old chat ID has to be re-pointed at the new channel ID, or the whole history silently detaches from the live conversation.
Message IDs are scoped too
Peer IDs are only half of any message reference. The other half, the message ID, has its own scoping rule that surprises people.
In a channel or supergroup, message IDs are chat-scoped. They start at 1 and increment for that chat, and every member sees the same message with the same ID. That is what makes t.me/c/<channel_id>/<msg_id> a stable permalink.
In private chats and basic groups, message IDs come from your own account's message box, which is shared across all of your private conversations. The message you sent has one ID for you and a different ID for the person who received it. There is no permalink, and there is no way to reference "message 812" without also saying whose box you mean.
The storage rule that follows is simple and non-negotiable: the primary key for a message row is (account_id, peer_id, message_id). Anything shorter will collide.
Why it matters
Peer IDs are the join key for everything a Telegram CRM does. Conversations, contacts, pipeline stages, assignment rules, automation triggers and analytics all hang off them. Get the representation wrong once, at the schema level, and every feature built on top inherits the bug.
The failure modes are quiet by design. A truncated 32-bit ID still looks like a plausible number. A Bot API chat_id stored in a column that a MTProto sender reads will target a peer that does not exist rather than throwing a type error. A migrated group looks like a customer who suddenly stopped replying. None of these raise an exception, which is exactly why they survive to production.
Common mistakes
- Mixing the two dialects in one column. Pick one canonical representation for your database and convert at the edges. Most teams store the MTProto form plus a peer-type enum, because it is lossless.
- Using ABS() to "normalise" an ID. Stripping the sign destroys the namespace tag. A supergroup and a user can then collide in the same table.
- Building t.me/c links from a Bot API chat_id. The
-100prefix has to come off first. - Assuming a peer ID identifies a person. It identifies an account. Deleted accounts keep their ID; the profile just empties out.
- Storing message IDs without their peer. Fine in a channel, actively wrong in private chats.
- Importing system peers as contacts. 777000 is Telegram's own service account, not a lead.
Related concepts
- Access hash: the other half of an MTProto peer reference, and the part that is not portable between accounts.
- Supergroup: the peer type that lives in the channel namespace and inherits the
-100prefix. - Telegram Bot API: where the flattened
chat_idencoding comes from. - MTProto: where the three separate namespaces come from.
- Forum topic: the reason a peer ID alone is no longer a unique conversation key.
- Webhook: the payload where a Bot API
chat_idmost often enters your system.
How Pinlyx handles it
Pinlyx stores peers in MTProto form with an explicit peer-type column and converts to the Bot API encoding only at the boundary where a payload requires it, so no single column ever holds two dialects. All ID columns are bigint in PostgreSQL and long in the .NET API. Migration service messages are watched, and when a basic group becomes a supergroup the linked conversation, notes and pipeline card follow the new channel ID rather than being orphaned. System peers are filtered out of contact import so nobody ends up with Telegram itself sitting in their Telegram CRM pipeline.