Telegram Group Migration: Keep CRM Routing When the Chat ID Changes

11 min read

Handle migrate_to_chat_id and migrate_from_chat_id without creating a second CRM conversation. Includes alias rules, conflict handling and a local Python check.

Telegram Group Migration: Keep CRM Routing When the Chat ID Changes

When a Telegram group becomes a supergroup, do not treat its new chat ID as a new customer conversation. Record the relationship between the old and new IDs, keep your internal conversation identifier unchanged, and resolve the current destination before sending an authorized notification. If the two IDs already belong to different conversations, stop automatic processing and investigate the conflict rather than silently merging records.

This guide is for developers maintaining their own Telegram Bot API integration and CRM routing. It assumes a bot legitimately present in a working group, authenticated incoming updates, durable storage and a staging environment. The design is not a verified built-in feature of 24go.site. It does not require collecting members or sending unsolicited messages. A group is not an individual contact, and continuity of a group does not grant new permission to contact its members.

Read the migration fields in the right direction

The official Message reference defines two optional fields. They describe the same relationship from different sides:

Received evidenceOld group IDNew supergroup ID
A message with migrate_to_chat_idThat message's chat.idIts migrate_to_chat_id
A message with migrate_from_chat_idIts migrate_from_chat_idThat message's chat.id
Failed request with parameters.migrate_to_chat_idThe chat_id used in that exact requestThe parameter's value
The third row comes from ResponseParameters, not from a service-message body. Keep the request context: without the original destination, the error response alone does not identify both ends. Do not look for an old identifier in a translated error description. Branch on the documented structured field, not on a particular English sentence or on every HTTP 400 response.

The fields are optional. Your handler must work when it observes only one direction, receives equivalent evidence again, or encounters migration evidence after normal traffic to the new group. Do not require two service messages to arrive together or in an assumed order. This is a defensive processing rule, not a claim that every installation will see all these cases.

Store the identifiers without narrowing them to 32-bit integers. Telegram notes that migration IDs can have more than 32 significant bits and at most 52 significant bits. A signed 64-bit database integer is a straightforward choice. If a CRM custom field cannot preserve that integer exactly, use a validated decimal string at that boundary and test round trips. Do not manufacture a new ID by adding a prefix to the old one; use the value supplied by Telegram.

Separate the conversation from its transport address

A fragile schema uses chat_id simultaneously as the CRM conversation key, the destination for notifications and the historical identity of every event. A simple replacement then either loses history or makes the new chat look like a new lead.

Use separate concepts in your own data model:

  • conversation_id: your stable internal identity for the working conversation.
  • chat_alias: a Telegram identifier known to refer to that conversation, scoped to your tenant and bot.
  • active_chat_id: the current destination proposed for new operations, subject to current permissions and application policy.
  • received_chat_id: the actual ID recorded on a historical event; keep this value unchanged.
The scope is your integration's isolation rule, not Telegram's identifier format. Include tenant and bot context in lookups so one installation cannot accidentally attach evidence to another tenant's conversation. A unique constraint on scope plus alias should prevent the same alias from owning two internal conversations. With concurrent workers, you still need transactional checks; an in-memory lookup is not enough.

An alias preserves continuity of routing. It does not mean that two CRM people are the same person, that all deals should be merged, or that a message identifier from the old chat is valid in the new one. Preserve message references as their original chat-and-message pair. Test historical link or reply behavior separately instead of rewriting old message IDs on the strength of a chat migration alone.

For the wider integration problem, see handling repeated Telegram webhooks. Event deduplication remains necessary, but it solves a different problem: a migration can arrive as a new update and still concern the existing conversation.

Make migration one atomic local decision

Process the migration branch before a generic “unknown chat means create a CRM conversation” branch. Otherwise, a service message from the supergroup may create an unnecessary second record before your handler examines its migration fields.

  1. Validate the update at your normal trusted intake boundary. Persist the incoming evidence and its source event key. A user's text containing an ID is not equivalent to a Telegram migration field.
  2. Extract the old and new IDs using the table above. Reject malformed values and self-mappings. Retain which field supplied the relationship.
  3. In a database transaction, lock or otherwise serialize the scoped alias records involved. Read their current conversation ownership again inside that transaction.
  4. If the old alias has one owner and the new alias is unclaimed, attach the new alias to the existing conversation. Record the migration relationship and update its active destination.
  5. If both aliases already belong to the same conversation, treat the operation as a repeat. Preserve the evidence, but do not create another CRM object or notification job.
  6. If the aliases belong to different conversations, quarantine the operation for reconciliation. Do not choose the newer row, overwrite a foreign key or delete either conversation automatically.
  7. Commit the mapping and any local follow-up job together. Only then acknowledge or finish the corresponding processing step according to your intake contract.
If neither alias is known, store unresolved evidence or use an explicit onboarding rule approved for that group. Do not guess which existing CRM record should own it based on the group title. If only the new alias is known, link the old alias only after the same scope and evidence checks; a reverse lookup is not permission to overwrite a conflict.

A database transaction does not include a remote CRM write. If the CRM also stores the active destination, update it through a separately tracked operation and retain its result. After an ambiguous timeout, reconcile the actual result instead of assuming the update failed. The local alias table should remain the routing authority while the external projection catches up under a documented policy.

Check direction, repeats and conflicts locally

Save this example as migration_check.py and run python3 migration_check.py. It uses only the Python standard library, artificial IDs and process memory. It never calls Telegram or a CRM and does not create a database. The scope tuple represents one tenant and one bot. The two message dictionaries are deliberately simplified input fixtures, not recordings from a real group.


def pair(message):
    chat = message["chat"]["id"]
    if "migrate_to_chat_id" in message:
        return chat, message["migrate_to_chat_id"]
    if "migrate_from_chat_id" in message:
        return message["migrate_from_chat_id"], chat
    raise ValueError("no migration field")


def apply(aliases, scope, old, new):
    if type(old) is not int or type(new) is not int or old == new:
        raise ValueError("invalid pair")
    before = aliases.get((scope, old))
    after = aliases.get((scope, new))
    if before is None:
        raise ValueError("old alias needs reconciliation")
    if after is not None and after != before:
        raise ValueError("conversation conflict")
    aliases[(scope, new)] = before
    return before


scope = ("tenant-demo", "bot-demo")
old, new = -12345, -1009876543210
left = {"chat": {"id": old}, "migrate_to_chat_id": new}
right = {"chat": {"id": new}, "migrate_from_chat_id": old}
assert pair(left) == pair(right) == (old, new)
aliases = {(scope, old): "conversation-A"}
assert apply(aliases, scope, *pair(left)) == "conversation-A"
assert apply(aliases, scope, *pair(right)) == "conversation-A"
assert len(aliases) == 2
aliases[(scope, -1001111111111)] = "conversation-B"
try:
    apply(aliases, scope, old, -1001111111111)
except ValueError as exc:
    assert str(exc) == "conversation conflict"
else:
    raise AssertionError("conflict accepted")
assert aliases[(scope, -1001111111111)] == "conversation-B"
try:
    apply(aliases, ("other-tenant", "bot-demo"), old, new)
except ValueError as exc:
    assert str(exc) == "old alias needs reconciliation"
else:
    raise AssertionError("scope crossed")
print("direction=ok repeat=ok conflict=blocked scope=isolated")

The expected final line is direction=ok repeat=ok conflict=blocked scope=isolated. A failed assertion means the checked policy has not held; do not remove the assertion just to get the line. The example intentionally stops on an unknown old alias. It does not implement the optional reverse-linking or onboarding paths described above.

This check establishes field direction and a small sequential policy only. It does not validate a full Telegram update schema, integer bounds, authenticated intake, concurrent transactions, durable recovery, active-destination changes or the remote CRM projection. Add those checks at the layers that actually implement them. Keeping the example small makes its result inspectable instead of presenting a toy dictionary as a production integration.

Resolve queued work without erasing its history

Old jobs may still contain the old chat ID. Deleting them loses work; changing every historical payload obscures what originally happened. Prefer storing a stable conversation reference on the job and resolving its current destination at execution time. Record both the originally requested destination and the resolved destination in the operation receipt.

Before sending, verify that the bot still has the required access and that the operation remains appropriate for the conversation. A migration alias alone does not establish current membership or posting permission. Do not automatically replay an operation whose earlier delivery result is unknown: resolving a destination does not determine whether a message was already sent.

If your system follows alias chains, impose a hop limit and detect cycles. A repeated mapping from old to new should not create a reverse edge just because migrate_from_chat_id was observed. Both service-message forms express a direction from old to new. If a chain conflicts with current ownership or loops, stop routing that job and preserve the evidence for an operator. A fixed hop limit is your safety policy, not a Telegram migration limit.

Cached destinations need a clear invalidation strategy. Otherwise, the database can be correct while a worker continues using an obsolete address. In staging, make one worker hold an old cached value and check how the migration version or cache expiry causes it to reload. Do not claim this is solved because a single fresh process finds the new row.

Diagnose the failure before editing CRM records

ObservationCheck nextAvoid
Structured migration target in a failed responseMatch it to the stored request and scoped conversationParsing any error text as a new ID
New-group traffic creates a second conversationHandler ordering and alias lookupDeleting the new record before checking ownership
Database has the new ID, worker sends to the old oneQueue resolution and cache invalidationReplaying every queued operation
New alias belongs to another conversationTenant/bot scope and previous onboardingAutomatic conversation merge
New destination still rejects an operationCurrent permissions and the actual errorTreating migration as a blanket access repair
An operator should be able to see the source evidence, both IDs, conversation ownership and the decision without opening entire message histories. Log only the metadata required for diagnosis under your retention policy. Keep bot tokens and unnecessary personal data out of operational receipts.

Verify the real integration before resuming work

Back up the affected mapping records and test in an authorized staging group. Deliver the same migration evidence sequentially and concurrently: there should be one ownership decision and no duplicate local job. Stop the worker after the transaction commits, restart it, and confirm that unfinished projection work can resume. Introduce an explicit ownership conflict and check that neither conversation is silently reassigned.

Then inspect an old queued job. Its historical source ID should remain intact, its resolved destination should follow the accepted mapping, and its prior delivery state should still determine whether it may run. Check the CRM projection separately from the alias table. Finally, verify an ordinary authorized interaction in the current group and record what actually happened; a local test line is not evidence of delivery.

The rollout is ready only when continuity, isolation and failure handling have all been observed in your own stack. If trustworthy evidence does not establish the old-to-new relationship, leave the mapping unresolved. Pausing one route with a clear reason is safer than attaching a working group to the wrong customer record.