GLOSSARY

What is an Access Hash?

An access hash is a 64-bit integer that Telegram's MTProto API returns next to every user, chat, and channel ID, and that your client must send back to prove it is allowed to reference that peer. Access hashes are issued per account, so a hash resolved by one Telegram account is meaningless to every other account.

Free forever plan · No credit card required · Cancel anytime

Quick definition

An access hash is a 64-bit integer that Telegram's MTProto API returns next to every user, chat, and channel ID, and that your client must send back to prove it is allowed to reference that peer. Access hashes are issued per account, so a hash resolved by one Telegram account is meaningless to every other account.

In a single sentence: the ID says who, the access hash proves you are allowed to ask.

What it means

Every peer in Telegram's MTProto API has two parts. There is an ID, which is a plain number that identifies a user, a basic group, or a channel. And there is an access hash, a 64-bit integer that Telegram hands you when you legitimately discover that peer.

Almost every method that takes a peer takes both. The input constructors make this explicit: inputPeerUser(user_id, access_hash), inputPeerChannel(channel_id, access_hash), inputUser(user_id, access_hash), inputChannel(channel_id, access_hash). The same pattern shows up outside peers too: photos, documents and file locations all carry an access hash of their own, because Telegram uses the same mechanism to gate every referenceable object on the platform.

The reason it exists is straightforward. User IDs are small, sequential-ish numbers. If IDs alone were enough to address a person, anyone could enumerate the ID space and message the entire user base. The access hash turns an ID into a capability: you can only talk to peers you have actually encountered, through a channel you are in, a username you resolved, a group whose members you can list, or a message someone sent you.

The rule that catches everybody: hashes are per account

This is the single most important operational fact about access hashes, and it is the one that breaks multi-account systems.

An access hash is derived from the authorization that fetched it. Account A resolves @example_user and gets hash 7734928472113049182. Account B resolves the same username five seconds later and gets a completely different number. Both are valid. Neither works from the other account.

The consequence for a CRM that rotates across a pool of Telegram accounts is concrete: your contact table cannot have a single access_hash column. It needs a separate mapping table keyed by (account_id, peer_id). The moment you store one hash per contact and let any account in the pool send to it, roughly (n-1)/n of your sends start failing with PEER_ID_INVALID, and the failure looks exactly like a deleted account, so the usual first reaction is to delete perfectly good contacts.

The same rule explains a behaviour people find surprising: a contact list scraped by account A is not immediately usable by account B. Account B has to resolve those peers itself, which costs API calls and, at volume, triggers flood waits. Scraped IDs transfer between accounts. Scraped hashes do not.

Min constructors: the hash that only works in one room

There is a second class of access hash that behaves differently, and almost every self-built Telegram integration gets bitten by it once.

When Telegram sends you a user or channel object for a peer you have not properly discovered, it sets the min flag on the object. You see min constructors constantly: the author of a message in a large group you just joined, the original sender of a forward, the admin who pinned a post. Telegram gives you enough to render a name and an avatar, but the access hash inside a min object is scoped to the chat that delivered it.

Cache that hash as though it were a normal one and a DM attempt later fails, usually with PEER_ID_INVALID, sometimes with nothing more useful. The correct reference for a min peer is inputPeerUserFromMessage(peer, msg_id, user_id) (and inputPeerChannelFromMessage for channels), which tells the server "this is the person from that message, in that chat", so the server can look up a valid hash on your behalf.

A production-grade peer cache therefore stores a flag: was this entity full or min? Only full entities may be used for cold outbound. Min entities are display data.

Where valid hashes come from

You never construct an access hash. You only ever receive one. The practical sources, in rough order of how much they cost you in rate limit budget, are listed in the table further down this page. The general principle is that any response containing a users or chats array is free cache: a client that parses only the part of the response it asked for and discards the peer arrays will re-resolve the same people over and over, and pay for it in flood waits.

Why it matters for a CRM

Access hash handling is invisible when it works and catastrophic when it does not, which is a bad combination for anyone running outbound at volume.

Consider a campaign of 5,000 messages spread across six Telegram accounts. Every send needs a valid inputPeerUser for that specific sending account. If the peer cache is keyed correctly, the sender picks the right hash and the message goes out. If the cache is keyed by contact only, five of the six accounts fail on every attempt, the retry loop re-resolves the username to recover, the re-resolution burns through the resolve limit, and the account earns a flood wait for behaviour that was really just a schema bug.

The second-order effect is worse. Cascading PEER_ID_INVALID failures that get auto-retried look, to Telegram's abuse systems, a lot like an account probing IDs it does not have. That is the fastest route from a working account to PEER_FLOOD.

Common mistakes

  • One access hash column on the contact row. The schema has to be per account. This is the mistake, and every other item on this list is downstream of it.
  • Storing hashes from min constructors. Flag them, or reference those peers through inputPeerUserFromMessage instead of caching at all.
  • Retrying a PEER_ID_INVALID with the same hash. It will never succeed. The retry path for an invalid peer is re-resolve, not repeat.
  • Storing the hash in a signed 32-bit column. An access hash is a full 64-bit value and routinely negative. Use BIGINT, and be careful with JSON, where an int64 that round-trips through a double loses precision silently.
  • Discarding the users array from update payloads. Those entries are free, already-authorised peers. Ignoring them means paying resolve calls for contacts you were handed.
  • Assuming a hash proves the peer still exists. A valid cached hash for a deleted account still returns an error on send. Handle INPUT_USER_DEACTIVATED as a contact status, not as a transient failure to retry.

Related concepts

  • Peer ID: the other half of a peer reference, and the part that is portable between accounts.
  • MTProto: the protocol that defines the input constructors described here.
  • Session string: the authorization an access hash is bound to. New session, new account context, new hashes.
  • Telegram Bot API: the wrapper that hides access hashes entirely, at the cost of never being able to message a stranger.
  • Flood wait: what you get when broken hash handling turns into a re-resolve loop.
  • Supergroup: the richest source of full user constructors, and therefore of usable hashes.

How Pinlyx handles it

Pinlyx keeps a per-account peer cache keyed on (account_id, peer_id) rather than on the contact, so a campaign that rotates across a pool always sends with the hash that belongs to the account doing the sending. Min entities are stored with a flag and are never used as cold-outbound targets. When a send returns PEER_ID_INVALID the engine re-resolves the peer once for that account, records the result, and only then marks the contact unreachable, so a schema-shaped failure never gets mistaken for a dead contact. The whole path runs on WTelegramClient over MTProto, which is why the Telegram CRM can DM people who never wrote to us first.

Cheat sheet · the wire shape

What an access hash looks like on the wire.

Field names come straight from the TL schema. The values are illustrative, and the error at the bottom is the one you will actually see when the hash belongs to a different account.

// What contacts.resolveUsername gives back
{
  "_": "contacts.resolvedPeer",
  "peer":  { "_": "peerUser", "user_id": 641307856 },
  "users": [
    {
      "_": "user",
      "id": 641307856,
      "access_hash": 7734928472113049182,   // int64, account-scoped
      "username": "example_user",
      "min": false                          // full entity: safe to cache
    }
  ]
}

// How you reference that peer on the next call
messages.sendMessage(
  peer     = inputPeerUser(641307856, 7734928472113049182),
  message  = "Hi there",
  random_id = 8462719304857263
)

// Same peer_id, hash taken from a DIFFERENT account in the pool
{
  "_": "rpc_error",
  "error_code": 400,
  "error_message": "PEER_ID_INVALID"
}
// Not a deleted user. Wrong authorization.

Per account

Key the cache on (account_id, peer_id). Never on peer_id alone.

Min means scoped

A min entity's hash works only inside the chat that delivered it.

64 bits, signed

Store as BIGINT. A JSON double will corrupt the low digits.

Where hashes come from

Five ways an account legitimately acquires a peer.

contacts.resolveUsernameOne user or channel, by @username

The cheapest way to get a usable peer. Public usernames only; a private channel has no username to resolve.

messages.getDialogsEvery chat in your dialog list

The bulk warm path. One paged call fills the entire cache for chats you already talk to.

channels.getParticipantsMembers of a group you can list

The users array in the response carries full user objects with access hashes. This is how scraping produces DM-able contacts.

contacts.getContactsYour saved address book

Returns full user constructors, never min ones, so these hashes are safe to store and reuse anywhere.

Any update with a users / chats arrayWhoever appears in the event

Every update container ships the peers it references. A client that ignores those arrays throws away free cache entries.

Watch out for

PEER_ID_INVALID almost never means the peer is invalid.

Three causes account for nearly every occurrence, in this order: the hash belongs to a different account in your pool, the entity you cached was a min constructor, or the peer was never resolved by this account at all and something upstream invented an inputPeer from an ID it found in your own database.

The diagnostic takes one minute: retry the exact same send from the account that originally discovered the contact. If it works, the peer is fine and your cache key is wrong.

Peer cache checklist

Six rules for a peer cache that survives account rotation.

  • Primary key is (account_id, peer_id, peer_type). One row per account, per peer.
  • Store access_hash as a 64-bit signed integer, never as a JSON number.
  • Record whether the entity was full or min, and refuse cold sends against min entities.
  • Harvest the users and chats arrays out of every response, including updates.
  • On PEER_ID_INVALID or CHANNEL_INVALID, re-resolve once for that account, then stop.
  • Re-resolve on a schedule for high-value contacts instead of waiting for a send to fail.

Access hashes: FAQ

The questions that come up the first time a multi-account send starts failing.

Because the access hash is not a property of the user, it is a property of the relationship between your account and that user. Telegram derives it server-side from the authorization that resolved the peer. That design is deliberate: it stops one leaked ID list from being usable by anyone who did not legitimately discover those peers. Practically it means your database key has to be (account_id, peer_id), never peer_id alone.
For users you normally get an RPC error 400 with the message PEER_ID_INVALID or USER_ID_INVALID. For channels and supergroups you get CHANNEL_INVALID. None of these say "wrong access hash" in plain words, which is why the error is so often misdiagnosed as a deleted account or a removed channel. If the same peer works from another account in your pool, the hash is the problem, not the peer.
They do not have a timer on them, but they can stop working. Losing visibility of the peer is the usual cause: you leave a private channel, the user deletes their account, or the object you cached was a "min" constructor whose hash was only ever valid inside one specific chat. Treat a hash as a cache entry that can be invalidated at any time, and always have a re-resolve path.
When Telegram sends you a user or channel object that you have not properly discovered, for example the author of a forwarded message in a group you just joined, it sets the min flag on that object. The access hash inside a min constructor is only valid in the context of the chat that delivered it. Storing it as if it were a normal hash produces PEER_ID_INVALID later. The correct reference for those peers is inputPeerUserFromMessage, which passes the chat and message ID that the peer was seen in.
Not in anything you can see. The Bot API is a server-side wrapper around MTProto, so Telegram resolves and stores the access hashes for your bot on its own infrastructure and gives you a flat numeric chat_id instead. That is one of the real conveniences of the Bot API, and it is also why bots cannot message a user who has not written to them first: there is no way for the bot to acquire a peer it never met.
It is not a credential in the way a session string is, because it only works from the account that obtained it. Someone who steals your (peer_id, access_hash) table cannot use it without also stealing the matching session. Still, a scraped hash table is evidence of what your accounts have seen, so keep it in your database and out of logs, exports, and API responses.
Ready to ship

Multi-account Telegram, without the peer cache bugs.

Pinlyx keeps a per-account peer cache, re-resolves automatically, and never confuses a schema bug for a dead contact.

Free forever plan · GDPR-ready · No credit card required

We value your privacy

We use cookies to improve our site, analyze traffic, and personalize ads. You can accept all, reject non-essential, or customize your choices. Read our Cookie Policy.