<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ALEKSANDR MICHAILOVICH</title>
    <description>The latest articles on DEV Community by ALEKSANDR MICHAILOVICH (@dev24pro).</description>
    <link>https://dev.to/dev24pro</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4182452%2F6b3d5eb1-9638-46f1-b96b-36a929c9e0e2.jpg</url>
      <title>DEV Community: ALEKSANDR MICHAILOVICH</title>
      <link>https://dev.to/dev24pro</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dev24pro"/>
    <language>en</language>
    <item>
      <title>A Customer Edited a Telegram Message: Update the CRM Note Safely [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Thu, 05 Nov 2026 17:47:03 +0000</pubDate>
      <link>https://dev.to/dev24pro/a-customer-edited-a-telegram-message-update-the-crm-note-safely-cross-post-5gfd</link>
      <guid>https://dev.to/dev24pro/a-customer-edited-a-telegram-message-update-the-crm-note-safely-cross-post-5gfd</guid>
      <description>&lt;p&gt;When a customer edits a Telegram message, do not turn the correction into another lead or replace the entire CRM record. Find the original source record, compare meaningful data and update only fields owned by the integration. If the source link is missing, versions conflict or an operator has changed the target note, retain the correction for reconciliation. Request arrival time does not prove that its text is newer.&lt;/p&gt;

&lt;p&gt;This guide is for a developer maintaining a custom Telegram Bot API integration with a CRM. It assumes authorized receipt of enquiries, durable storage and an isolated test environment. It proposes a design policy, not a verified 24go.site feature or a ready-made configuration for any particular CRM. The scope is ordinary &lt;code&gt;message&lt;/code&gt; and &lt;code&gt;edited_message&lt;/code&gt; updates. Channel posts, business messages, guest formats and deletion need separate contracts. No live credentials are needed for the local policy check.&lt;/p&gt;

&lt;h2&gt;
  
  
  A message correction is not a repeated delivery
&lt;/h2&gt;

&lt;p&gt;In the &lt;a href="https://core.telegram.org/bots/api#update" rel="noopener noreferrer"&gt;Update object&lt;/a&gt;, &lt;code&gt;edited_message&lt;/code&gt; represents a new version of a message known to the bot that was edited. The documentation warns that changes to fields unavailable to, or not actively used by, the bot may also trigger this update. Receiving one therefore does not by itself require changing a visible CRM note or qualifying the customer again.&lt;/p&gt;

&lt;p&gt;Redelivery of an update and revision of a source message are different problems. An event deduplication key must not discard a different update merely because it concerns an existing message. Conversely, a new update with identical meaningful content does not need another note. Use the separate &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;webhook idempotency design&lt;/a&gt; for repeated delivery and ambiguous record creation. This guide starts with the relationship between a source message and an already-linked CRM object.&lt;/p&gt;

&lt;p&gt;Agree with the sales process owner on what a correction means. Changing “we need five licences” to “we need three” may amend an enquiry, but it does not independently authorize changing an approved deal amount. A spelling correction should not automatically send another email or reopen a task. Business effects belong to your agreed workflow, not to the name of a Telegram field.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give the source message its own identity
&lt;/h2&gt;

&lt;p&gt;Keep an event key separate from a message key. The event key needs your source context and &lt;code&gt;update_id&lt;/code&gt;. For an ordinary message, a proposed application key combines tenant, bot, &lt;code&gt;chat.id&lt;/code&gt; and &lt;code&gt;message_id&lt;/code&gt;. Here, tenant identifies the organization within your system; it is not a Telegram field. The additional namespaces prevent two customers of an integration provider from sharing a source record accidentally.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Message contract&lt;/a&gt; describes &lt;code&gt;message_id&lt;/code&gt; as unique inside its chat. That number alone is insufficient for a whole database. A username, sender label or enquiry text is also unsuitable for locating the original note: these can change or coincide. If a source link is missing, do not select a vaguely similar deal by matching a few words and silently write the correction there.&lt;/p&gt;

&lt;p&gt;Maintain an explicit mapping: source message key → internal source-record ID → target CRM note ID. Store the supported message shape, mapper version, accepted projection and delivery state alongside it. A projection is the selected data used to build your note, such as text with its associated entities or a caption with its own metadata. Define and version this application schema instead of assuming every field in an incoming object belongs in the CRM.&lt;/p&gt;

&lt;p&gt;If an input's identity does not fit the supported ordinary-message scheme, stop automatic replacement. Special identifiers and newly introduced API formats are not supported merely because the JSON contains a familiar field name. Route them to separate handling. This design is not a universal decoder for every Telegram message format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare source time and meaningful content separately
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;Message&lt;/code&gt; has an optional &lt;code&gt;edit_date&lt;/code&gt;, the time the message was last edited in Unix time. It helps compare revisions of one supported source message, but it is not a complete revision history. Do not substitute the HTTP receipt time or job completion time. A slow worker can finish an older job after a newer one has completed.&lt;/p&gt;

&lt;p&gt;The proposed conservative policy has distinct outcomes. If the selected projection equals the current one, there is no need to change the visible note. However, a later valid source edit time should still advance the boundary of versions already observed. Otherwise a subsequent late change may appear newer than an obsolete marker. Observation metadata and note content have different reasons to change.&lt;/p&gt;

&lt;p&gt;If the projection differs and the source edit time is strictly later than the accepted boundary, it is a delivery candidate. If the time is earlier, do not replace the current projection. If the times are equal but the projections differ, record a conflict rather than choosing whichever request arrived last. This policy can require manual decisions, but avoids presenting an inferred order as a proven one. A missing or invalid edit time also requires reconciliation before an automatic replacement.&lt;/p&gt;

&lt;p&gt;Retain &lt;code&gt;update_id&lt;/code&gt; for deduplication and investigation, but do not describe it as an everlasting per-message revision counter. The Update documentation allows the next identifier to be chosen randomly after at least a week without new updates. Its documented sequencing role is useful, but does not replace your conflict-resolution and field-ownership contract. Do not invent an exact revision number that the source does not provide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide who owns each CRM field
&lt;/h2&gt;

&lt;p&gt;Before enabling automatic corrections, build an ownership table. For each target field, specify its owner, permitted action and how an operator's intervening change will be detected. A source-text note, deal stage and verified contact number should not inherit one blanket “last event wins” rule.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target data&lt;/th&gt;
&lt;th&gt;Proposed owner&lt;/th&gt;
&lt;th&gt;Action on a message correction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated copy of the source enquiry&lt;/td&gt;
&lt;td&gt;Integration, where exclusive ownership is established&lt;/td&gt;
&lt;td&gt;Replace only that note after checking the source version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Note that operators may also edit&lt;/td&gt;
&lt;td&gt;Shared ownership&lt;/td&gt;
&lt;td&gt;Check the target version; on conflict, add a linked correction or review task&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deal stage and assignee&lt;/td&gt;
&lt;td&gt;Agreed sales workflow&lt;/td&gt;
&lt;td&gt;Do not change solely because edited_message arrived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verified contact details&lt;/td&gt;
&lt;td&gt;Separate verification process&lt;/td&gt;
&lt;td&gt;Do not copy arbitrary revised text automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Technical processing history&lt;/td&gt;
&lt;td&gt;Integration&lt;/td&gt;
&lt;td&gt;Record the outcome and correction link under the retention policy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the CRM supports a conditional write against a version, verify its actual contract and use it. Reading a note and then issuing an ordinary PATCH does not prevent an operator from changing it between those steps. Without confirmed conditional-write support, proposing an explicitly linked correction is safer than promising that replacement cannot lose work. Appending also needs a stable operation identity so retries do not add several identical corrections.&lt;/p&gt;

&lt;p&gt;Do not turn the revision ledger into an uncontrolled archive. Define source retention and access separately under your lawful processing arrangements. Secretly copying all previous texts into logs does not become acceptable because it makes investigation easier. General telemetry usually needs internal identifiers, outcome codes and the adapter version rather than complete private enquiries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle an edit whose original record is missing
&lt;/h2&gt;

&lt;p&gt;An unknown original does not prove that the customer opened a new enquiry. The initial job might be delayed, its CRM mapping might not yet be saved, or the relevant message might predate your observation window. Persist the correction as waiting for a source link, and inspect the local job queue and mapping ledger. Do not create another lead by default.&lt;/p&gt;

&lt;p&gt;Assign a deadline and an owner to this reconciliation. Once the original becomes available, process the retained correction under the same version rules rather than replaying its initial write command unchanged. If no link can be established, the operator should see an unresolved outcome, not a green synchronization status. Repeated missing-original cases may indicate routing or persistence defects.&lt;/p&gt;

&lt;p&gt;Do not promise to reconstruct complete message history through this design. An edit exposes an observed version, not every change ever made. The absence of another update does not prove that a user made no correction. Likewise, an ordinary &lt;code&gt;edited_message&lt;/code&gt; is not a deletion notification. Missing text in an unsupported message shape must not be converted into an instruction to erase the CRM note.&lt;/p&gt;

&lt;h2&gt;
  
  
  Apply the correction in explicit stages
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Persist the update and its type through your existing authenticated receiver. Webhook authentication and acknowledgement after durable acceptance remain necessary, but are not redesigned here. The dispatcher must explicitly distinguish a new message from an edited one.&lt;/li&gt;
&lt;li&gt;Resolve the source key and its target note mapping. Check the organization, bot and chat; do not trust a client-supplied ID for another organization's deal. If the source is unknown, create a reconciliation task without a remote replacement.&lt;/li&gt;
&lt;li&gt;Build the supported body projection together with its associated metadata. An absent field in an unsupported shape does not mean an empty string. Compare meaningful link destinations too if they are part of the note, not only the displayed link labels.&lt;/li&gt;
&lt;li&gt;Within one local transaction, decide against the currently accepted version and save an operation intent. Use an atomic claim or record-version check so two workers cannot independently accept incompatible decisions. A check held only in process memory is insufficient.&lt;/li&gt;
&lt;li&gt;For a delivery candidate, apply field ownership and the CRM's verified capabilities. Recheck that the operation is not already stale locally immediately before the write, and use a documented conditional write when available. A local transaction cannot lock an operator out of the remote CRM note.&lt;/li&gt;
&lt;li&gt;Read back the target record and compare the expected data. If a timeout leaves the result unknown, retain a reconciliation state; do not generate a new operation key or blindly repeat replacement. Link a confirmed outcome to the particular source version that produced it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Exercise the policy before connecting a CRM
&lt;/h2&gt;

&lt;p&gt;A reproducible local policy check needs only synthetic keys, texts and times. Implement a pure decision function with no network access. Inputs are whether the original is known, the accepted source time and projection, and the candidate time and projection. Outcomes must distinguish unchanged content, a stale input, a delivery candidate and reconciliation. Separately check advancement of the observation boundary when content matches: “do not write the note” is not the same as “forget this observation.”&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Synthetic scenario&lt;/th&gt;
&lt;th&gt;Expected decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Known original, newer time and different text&lt;/td&gt;
&lt;td&gt;Delivery candidate, not another lead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Older time and different text&lt;/td&gt;
&lt;td&gt;Do not replace the accepted version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Equal time and different projections&lt;/td&gt;
&lt;td&gt;Reconcile the conflict&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newer time, identical projection&lt;/td&gt;
&lt;td&gt;No note write; advance the observation boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No mapping to the original&lt;/td&gt;
&lt;td&gt;Wait for a link; do not create another lead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No valid edit time&lt;/td&gt;
&lt;td&gt;Reconcile without automatic replacement&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These cases check only the proposed decision policy on artificial inputs. Connector acceptance also needs competing jobs, a restart after a persisted operation intent, an operator's intervening edit and a lost CRM response. Separately test that a new caption does not reuse old entity metadata. A passing decision function establishes neither database atomicity nor conditional remote writes.&lt;/p&gt;

&lt;p&gt;In an isolated integration environment, create one authorized enquiry and record its CRM note ID. Correct a spelling error, then change a meaningful condition. Verify that both corrections remain linked to the same source, no additional deal appears and business effects follow the approved policy. Have an operator edit the target note before another source correction; check the conflict outcome rather than merely inspecting the HTTP response.&lt;/p&gt;

&lt;p&gt;Close the rollout with a precise statement of which decisions passed locally, which outcomes were confirmed by CRM readback and which remain under manual reconciliation. Track the age of unresolved corrections and conflict reasons. The operator needs the corrected enquiry without silently losing their own work. That is the acceptance result to establish before enabling automatic replacement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://core.telegram.org/bots/api#update" rel="noopener noreferrer"&gt;Telegram Bot API: Update&lt;/a&gt;, edited_message, update_id behavior and event scope; checked on the editorial date.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Telegram Bot API: Message&lt;/a&gt;, message identity, edit_date, text and captions; checked on the editorial date.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/telegram-message-edit-crm-reconciliation?locale=en" rel="noopener noreferrer"&gt;A Customer Edited a Telegram Message: Update the CRM Note Safely&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Клиент исправил сообщение Telegram: как обновить заметку в CRM [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Thu, 05 Nov 2026 16:43:17 +0000</pubDate>
      <link>https://dev.to/dev24pro/kliient-ispravil-soobshchieniie-telegram-kak-obnovit-zamietku-v-crm-pieriekriostnaia-publikatsiia-392n</link>
      <guid>https://dev.to/dev24pro/kliient-ispravil-soobshchieniie-telegram-kak-obnovit-zamietku-v-crm-pieriekriostnaia-publikatsiia-392n</guid>
      <description>&lt;p&gt;Если клиент исправил сообщение в Telegram, не создавайте из исправления новый лид и не заменяйте всю карточку. Найдите запись исходного сообщения, сравните значимые данные и обновите только те поля, которыми владеет интеграция. Если запись не найдена, версии спорят или менеджер уже изменил целевую заметку, сохраните исправление как отдельную задачу на сверку. Время получения запроса не доказывает, что его текст новее.&lt;/p&gt;

&lt;p&gt;Разбор предназначен для разработчика собственной интеграции Telegram Bot API с CRM. Предполагается разрешённое получение обращений, постоянное хранилище и изолированная тестовая среда. Это проектная политика, а не подтверждённая функция 24go.site и не готовая настройка для конкретной CRM. Ниже рассматриваются обычные &lt;code&gt;message&lt;/code&gt; и &lt;code&gt;edited_message&lt;/code&gt;; события каналов, business-сообщения, guest-форматы и удаление требуют отдельного контракта. Рабочие токены для локальной проверки не нужны.&lt;/p&gt;

&lt;h2&gt;
  
  
  Исправление сообщения не равно повтору доставки
&lt;/h2&gt;

&lt;p&gt;В &lt;a href="https://core.telegram.org/bots/api#update" rel="noopener noreferrer"&gt;объекте Update&lt;/a&gt; поле &lt;code&gt;edited_message&lt;/code&gt; обозначает новую версию известного боту сообщения, которое отредактировали. Документация предупреждает: событие иногда вызывают изменения полей, недоступных или не используемых ботом. Поэтому сам факт прихода такого обновления не означает, что нужно изменить видимую заметку или повторно квалифицировать клиента.&lt;/p&gt;

&lt;p&gt;Повтор одного обновления и исправление исходного сообщения решают разные задачи. Дедупликация по ключу события не должна отбросить другое обновление только потому, что оно относится к прежнему сообщению. И наоборот, новое обновление с тем же содержимым не требует второй заметки. Для повторов доставки и неоднозначного создания записи используйте отдельную &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;схему идемпотентности вебхука&lt;/a&gt;. Здесь задача начинается после связи источника с уже существующим объектом CRM.&lt;/p&gt;

&lt;p&gt;Сначала согласуйте с владельцем продаж смысл исправления. Замена «нужно пять лицензий» на «нужно три» может уточнять обращение, но сама по себе не разрешает менять утверждённую сумму сделки. Исправление опечатки не должно заново отправлять письмо или открывать задачу менеджеру. Решение о бизнес-действии принадлежит вашему процессу, а не имени поля Telegram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Дайте исходному сообщению собственный ключ
&lt;/h2&gt;

&lt;p&gt;Храните отдельно ключ события и ключ сообщения. Для события нужен ваш контекст источника и &lt;code&gt;update_id&lt;/code&gt;; для обычного сообщения предложен составной ключ из tenant, бота, &lt;code&gt;chat.id&lt;/code&gt; и &lt;code&gt;message_id&lt;/code&gt;. Tenant здесь обозначает организацию внутри вашей системы, а не поле Telegram. Дополнительные компоненты не дают двум клиентам интегратора случайно разделить одну запись.&lt;/p&gt;

&lt;p&gt;В &lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;описании Message&lt;/a&gt; &lt;code&gt;message_id&lt;/code&gt; уникален внутри чата. Одного этого числа для всей базы недостаточно. Имя пользователя, подпись отправителя и текст обращения также не подходят для поиска исходной заметки: они могут совпадать или меняться. Не пытайтесь при отсутствии связи подобрать похожую сделку по нескольким словам и молча записать исправление туда.&lt;/p&gt;

&lt;p&gt;Поддерживайте явное соответствие: ключ сообщения → внутренний ID исходной записи → ID целевой заметки CRM. Рядом храните поддерживаемый тип сообщения, версию преобразователя, принятую проекцию и состояние доставки. Проекция означает выбранные данные, из которых вы строите заметку: например, текст с относящейся к нему разметкой либо подпись с её метаданными. Это прикладная схема, которую надо определить и версионировать.&lt;/p&gt;

&lt;p&gt;Если идентичность входа не укладывается в поддерживаемую обычную схему, остановите автоматическую замену. Специальные идентификаторы и новые форматы API нельзя принимать только потому, что JSON содержит знакомое поле. Направьте такой вход на отдельную обработку. Эта инструкция не является универсальным декодером всех форматов Telegram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Сравнивайте содержимое и время источника раздельно
&lt;/h2&gt;

&lt;p&gt;У &lt;code&gt;Message&lt;/code&gt; есть необязательное поле &lt;code&gt;edit_date&lt;/code&gt;: время последнего редактирования в Unix time. Оно полезно для сравнения редакций одного поддерживаемого сообщения, но не является полной историей правок. Не подменяйте его временем приёма HTTP-запроса или окончания задания: медленный воркер может завершить старую работу после новой.&lt;/p&gt;

&lt;p&gt;Предложенная консервативная политика использует несколько исходов. Если выбранные данные совпадают с текущими, видимую заметку менять не надо. Однако более позднее допустимое время источника стоит сохранить как границу уже наблюдавшихся версий. Иначе последующее запоздавшее изменение может выглядеть новым относительно старого маркера. Метаданные наблюдения и содержимое заметки имеют разные причины обновления.&lt;/p&gt;

&lt;p&gt;Если содержимое отличается, а время источника строго позже принятого, это кандидат на исправление. Если время раньше, не заменяйте текущую проекцию. Если времена равны, а данные различаются, фиксируйте конфликт вместо выбора по очередности прихода. Такой консервативный режим иногда потребует ручного решения, но не выдаёт предположение за доказанный порядок. При отсутствующем или некорректном &lt;code&gt;edit_date&lt;/code&gt; для исправления тоже нужна сверка.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;update_id&lt;/code&gt; сохраняйте для дедупликации и расследования, но не объявляйте вечным счётчиком редакций сообщения. Документация Update допускает случайный выбор следующего идентификатора после как минимум недели без новых обновлений. Его описание помогает восстанавливать последовательность обновлений, но не заменяет ваш контракт разрешения конфликтов и владения полями. Не придумывайте отсутствующий точный номер правки.&lt;/p&gt;

&lt;h2&gt;
  
  
  Выберите, кто владеет полями CRM
&lt;/h2&gt;

&lt;p&gt;Перед автоматизацией составьте таблицу владельцев. Для каждого поля укажите источник, разрешённое действие и способ обнаружить изменение оператором. Заметка с исходным текстом, стадия сделки и подтверждённый телефон не должны случайно получить одно общее правило «последнее событие побеждает».&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Целевые данные&lt;/th&gt;
&lt;th&gt;Предложенный владелец&lt;/th&gt;
&lt;th&gt;Действие при исправлении&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Отдельная заметка-копия обращения&lt;/td&gt;
&lt;td&gt;Интеграция при подтверждённом эксклюзивном владении&lt;/td&gt;
&lt;td&gt;Заменить только эту заметку после проверки версии&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Заметка, которую вправе править менеджер&lt;/td&gt;
&lt;td&gt;Совместное владение&lt;/td&gt;
&lt;td&gt;Сверить целевую версию; при конфликте добавить связанную коррекцию или задачу&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Стадия и ответственный&lt;/td&gt;
&lt;td&gt;Согласованный процесс продаж&lt;/td&gt;
&lt;td&gt;Не менять только из-за edited_message&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Подтверждённые реквизиты контакта&lt;/td&gt;
&lt;td&gt;Отдельный процесс проверки&lt;/td&gt;
&lt;td&gt;Не переносить автоматически произвольную новую строку&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;История технической обработки&lt;/td&gt;
&lt;td&gt;Интеграция&lt;/td&gt;
&lt;td&gt;Записать исход и связь с исправлением в рамках срока хранения&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Если CRM поддерживает условную запись по версии, проверьте её реальный контракт и используйте его. Чтение заметки с последующим обычным PATCH не исключает, что менеджер изменит её между этими действиями. Без подтверждённой условной записи безопаснее предложить добавление явно связанного исправления, чем обещать, что правки не потеряются. И добавление требует устойчивого ключа операции, чтобы повтор не создал несколько одинаковых коррекций.&lt;/p&gt;

&lt;p&gt;Не превращайте историю в бесконтрольный архив. Сроки хранения исходников и доступ к ним задаются отдельно с учётом ваших оснований обработки данных. Скрытое копирование всех старых текстов в журналы не становится допустимым из-за удобства расследования. Для общей телеметрии обычно достаточно внутренних ключей, исхода и версии адаптера, а не полного обращения.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что делать, если исправление пришло раньше исходной записи
&lt;/h2&gt;

&lt;p&gt;Неизвестный исходник не доказывает, что клиент отправил новое обращение. Первичное задание могло задержаться, связь с CRM ещё не сохранена, событие могло попасть в систему до начала наблюдения. Сохраните исправление в состоянии ожидания связи и проверьте локальную очередь и журнал соответствий. Не создавайте новый лид по умолчанию.&lt;/p&gt;

&lt;p&gt;Назначьте срок и ответственного за такую сверку. После появления исходной записи обработайте сохранённое исправление по тем же правилам версии, а не просто повторите первоначальную команду на запись. Если связь так и не найдена, оператор должен видеть нерешённый исход, а не зелёную отметку синхронизации. Повторяющиеся неизвестные исходники могут указывать на проблему маршрутизации или хранения.&lt;/p&gt;

&lt;p&gt;Не обещайте восстановить всю историю сообщений через эту схему. Исправление представляет наблюдаемую версию, а не журнал всех изменений. Отсутствие очередного события не доказывает, что пользователь ничего не исправлял. Равным образом обычное &lt;code&gt;edited_message&lt;/code&gt; не является уведомлением об удалении: отсутствие текста в неподдерживаемом формате нельзя превращать в команду стереть заметку.&lt;/p&gt;

&lt;h2&gt;
  
  
  Применяйте исправление по этапам
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;В существующем проверенном приёмнике сохраните обновление и его тип. Аутентификация вебхука и подтверждение после надёжного сохранения остаются обязательными, но здесь не проектируются заново. Диспетчер должен явно различать новое и отредактированное сообщение.&lt;/li&gt;
&lt;li&gt;Найдите исходный ключ и связь с заметкой. Проверьте организацию, бота и чат; не доверяйте переданному клиентом ID чужой сделки. Если исходник неизвестен, создайте задачу сверки без удалённой замены.&lt;/li&gt;
&lt;li&gt;Постройте проекцию поддерживаемого тела вместе с его метаданными. Отсутствующее поле в неподдерживаемом сообщении не означает пустую строку. Сравнивайте и значимые адреса ссылок, если они входят в заметку, а не только видимые подписи.&lt;/li&gt;
&lt;li&gt;В одной локальной транзакции выберите исход по текущей принятой версии и сохраните намерение операции. Нужен атомарный захват или проверка версии записи, иначе два воркера могут одновременно принять несовместимые решения. Отдельная проверка в памяти этого не обеспечивает.&lt;/li&gt;
&lt;li&gt;Для кандидата на доставку примените политику владельца поля и возможности CRM. Непосредственно перед записью повторно проверьте, что операция не устарела локально; используйте документированную условную запись, когда она доступна. Локальная транзакция не блокирует редактирование удалённой заметки менеджером.&lt;/li&gt;
&lt;li&gt;Перечитайте целевую запись и сверьте ожидаемые данные. При таймауте с неизвестным исходом сохраните состояние сверки, не генерируйте новый ключ и не повторяйте замену вслепую. После подтверждения свяжите результат с конкретной версией источника.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Проверьте политику до подключения CRM
&lt;/h2&gt;

&lt;p&gt;Для воспроизводимой локальной проверки достаточно искусственных идентификаторов, текстов и времён. Реализуйте чистую функцию решения без сети: на входе известность исходника, время принятой версии, её проекция, время кандидата и его проекция. Выходы должны различать отсутствие изменения, устаревший вход, кандидат на доставку и необходимость сверки. Отдельно проверяйте обновление временной границы при совпадающем содержимом; решение «не писать заметку» не равно «забыть наблюдение».&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Искусственный сценарий&lt;/th&gt;
&lt;th&gt;Ожидаемое решение&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Известный исходник, новая дата и другой текст&lt;/td&gt;
&lt;td&gt;Кандидат на доставку, не новый лид&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Более старая дата и другой текст&lt;/td&gt;
&lt;td&gt;Не заменять текущую версию&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Одинаковая дата и разные проекции&lt;/td&gt;
&lt;td&gt;Сверка конфликта&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Более новая дата, та же проекция&lt;/td&gt;
&lt;td&gt;Не писать заметку, продвинуть границу наблюдения&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Нет связи с исходником&lt;/td&gt;
&lt;td&gt;Ожидание связи, без нового лида&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Нет допустимого времени правки&lt;/td&gt;
&lt;td&gt;Сверка, без автоматической замены&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Этот набор проверяет только предложенное решение на искусственных входах. Для приёмки коннектора добавьте конкурентные задания, перезапуск после сохранённого намерения, изменение заметки оператором и потерянный ответ CRM. Отдельно проверьте, что новая подпись не использует старые метаданные разметки. Успешная функция выбора не доказывает ни атомарность базы, ни условность удалённой записи.&lt;/p&gt;

&lt;p&gt;В тестовой среде сначала создайте одно разрешённое обращение и зафиксируйте ID его заметки. Исправьте текст без изменения смысла, затем измените существенное условие. Убедитесь, что сохраняется связь с тем же источником, не появляется новая сделка, а автоматические действия соответствуют утверждённой политике. После ручной правки менеджером повторите исправление и проверьте конфликт, а не только HTTP-ответ.&lt;/p&gt;

&lt;p&gt;Завершайте внедрение точным результатом: какие исходы проверены локально, какие подтверждены чтением CRM и что остаётся на ручной сверке. Отслеживайте возраст нерешённых исправлений и причины конфликтов. Исправленный текст обращения должен быть доступен оператору без молчаливой потери его собственной работы; именно это нужно доказать перед включением автоматической замены.&lt;/p&gt;

&lt;h2&gt;
  
  
  Источники
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://core.telegram.org/bots/api#update" rel="noopener noreferrer"&gt;Telegram Bot API: Update&lt;/a&gt;: edited_message, особенности update_id и границы событий; проверено в редакционную дату.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Telegram Bot API: Message&lt;/a&gt;: идентичность сообщения, edit_date, текст и подписи; проверено в редакционную дату.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/telegram-message-edit-crm-reconciliation?locale=ru" rel="noopener noreferrer"&gt;Клиент исправил сообщение Telegram: как обновить заметку в CRM&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram Message Entities: Keep Links and Emoji Intact in Your CRM [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Thu, 05 Nov 2026 02:16:13 +0000</pubDate>
      <link>https://dev.to/dev24pro/telegram-message-entities-keep-links-and-emoji-intact-in-your-crm-cross-post-24lo</link>
      <guid>https://dev.to/dev24pro/telegram-message-entities-keep-links-and-emoji-intact-in-your-crm-cross-post-24lo</guid>
      <description>&lt;p&gt;When a Telegram enquiry reaches a CRM with the wrong link or a clipped word after an emoji, inspect the text mapping before changing webhook delivery. Telegram's message entities identify ranges using UTF-16 code units. Those ranges belong to the original text or caption, not a cleaned copy. Preserve the source string with its entities, then produce a separate plain-text view and validated link records.&lt;/p&gt;

&lt;p&gt;This guide is for the maintainer of a custom Telegram Bot API integration. It is a proposed design and local diagnostic, not a verified built-in 24go.site capability. It assumes the integration already receives messages legitimately. The local example needs Python, not a bot token or a CRM account. It does not test a production connector, render untrusted HTML or authorize collecting unrelated people's data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identify the source field before reading a range
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Message object&lt;/a&gt; has optional &lt;code&gt;text&lt;/code&gt; and &lt;code&gt;entities&lt;/code&gt; for text messages. Media captions use &lt;code&gt;caption&lt;/code&gt; and &lt;code&gt;caption_entities&lt;/code&gt;. An entity list describes its corresponding string. A photo with a caption is not a text message with a mysteriously empty body; choosing only &lt;code&gt;text&lt;/code&gt; can discard a valid enquiry before formatting is even considered.&lt;/p&gt;

&lt;p&gt;Treat the two pairs as distinct inputs to the same mapper. Record which pair was selected. If your supported message has neither field, record an explicit unsupported or no-text outcome rather than making an empty sales note look successful. Other message shapes require their own schema handling; this procedure is not a complete decoder for every current Bot API message type.&lt;/p&gt;

&lt;p&gt;Do not substitute the text of a replied-to message, a forwarded source or a generated summary for the current message body. Those are different pieces of context. If the product needs them, store their role explicitly. Otherwise a manager may read an old quoted price as the customer's current request. Preserve the association between each body and the exact entity list that describes it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why an emoji can shift a link
&lt;/h2&gt;

&lt;p&gt;An entity is a typed range inside a string. The &lt;a href="https://core.telegram.org/bots/api#messageentity" rel="noopener noreferrer"&gt;MessageEntity contract&lt;/a&gt; defines both &lt;code&gt;offset&lt;/code&gt; and &lt;code&gt;length&lt;/code&gt; in UTF-16 code units. A Python string slice uses Unicode code-point positions instead. These coordinate systems agree for many ordinary characters but differ for characters represented by a UTF-16 surrogate pair, including the grinning face in the example below.&lt;/p&gt;

&lt;p&gt;Use the synthetic text &lt;code&gt;😀 offer&lt;/code&gt;. The entity covering &lt;code&gt;offer&lt;/code&gt; starts at UTF-16 offset 3 and has length 5. The emoji occupies two code units and the following space occupies one. In Python, directly slicing positions 3 through 8 returns &lt;code&gt;ffer&lt;/code&gt;, not the requested word. A parser may return a plausible-looking fragment without raising an error, which makes a silent mapping defect more dangerous than an obvious failure.&lt;/p&gt;

&lt;p&gt;Do not generalize this to “every emoji counts as two.” Some visible symbols use one code unit, while a displayed emoji sequence can contain several Unicode code points. What looks like one character to a reader is not a dependable offset unit. Use the documented encoding or a tested library helper; do not maintain a hand-written exception list of emoji.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep originals and derived representations separate
&lt;/h2&gt;

&lt;p&gt;Store the accepted source string and its associated entity metadata together under the integration's access and retention rules. Derived fields can include a plain-text note, link records and an optional sanitized rich-text view. The original is the reference for mapping and incident reproduction, not a reason to retain every conversation forever.&lt;/p&gt;

&lt;p&gt;Normalization is a separate transformation. Trimming leading spaces, replacing line breaks or applying Unicode normalization can change range coordinates or the text they select. Resolve and validate ranges against the unchanged source first. If a downstream view needs different text, calculate its own positions or omit positions from that view. Never attach the original offsets to a modified string and assume they remain valid.&lt;/p&gt;

&lt;p&gt;For an update that changes a message, treat the new body and its new metadata as one version. Do not reuse the old entities with the new text. A customer may alter both the label and destination of a link. Updating only the visible note leaves stale link records even when the UTF-16 arithmetic is correct. Agree separately on whether the CRM should replace a note, append a revision or require review.&lt;/p&gt;

&lt;h2&gt;
  
  
  A link label is not necessarily its destination
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;url&lt;/code&gt; entity identifies a URL appearing in the body. A &lt;code&gt;text_link&lt;/code&gt; entity has a visible label in the text and a separate optional &lt;code&gt;url&lt;/code&gt; field that provides the destination. Saving only the selected label loses the target. Presenting that target as though it were the label can hide what the user actually wrote. Keep both fields and the entity type when your workflow needs links.&lt;/p&gt;

&lt;p&gt;Neither a Telegram entity nor a familiar-looking label makes a destination trustworthy. The label “company portal” can point somewhere unrelated. Do not fetch the target automatically while creating a CRM note: fetching untrusted addresses introduces a separate network-security boundary. If previews are required, use an independently reviewed service with destination checks, network restrictions and a clear data policy.&lt;/p&gt;

&lt;p&gt;For a plain-text CRM, showing the label and a separately identified destination is often simpler than reconstructing Telegram formatting. For a rich-text CRM, escape ordinary text and build only supported markup through a safe renderer. Do not concatenate customer text into HTML or treat incoming formatting as permission to execute anything. A valid range confirms where an entity points, not whether its contents are safe.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incoming information&lt;/th&gt;
&lt;th&gt;Preserve&lt;/th&gt;
&lt;th&gt;Avoid inferring&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Selected source substring and entity type&lt;/td&gt;
&lt;td&gt;That the address is safe to open&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text_link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Label and separate destination&lt;/td&gt;
&lt;td&gt;That the label describes the actual host&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text_mention&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Selected label and permitted user reference, if needed&lt;/td&gt;
&lt;td&gt;That the label is a stable username&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bold&lt;/code&gt; or &lt;code&gt;italic&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Original text; optional supported presentation&lt;/td&gt;
&lt;td&gt;That formatting changes business priority&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;code&lt;/code&gt; or &lt;code&gt;pre&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Literal text and supported metadata&lt;/td&gt;
&lt;td&gt;That a command should be executed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown entity type&lt;/td&gt;
&lt;td&gt;Source text plus an explicit unsupported-type decision&lt;/td&gt;
&lt;td&gt;That deleting the whole message is necessary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A &lt;code&gt;text_mention&lt;/code&gt; can contain a user object according to the API. That is not proof that the sender owns the mentioned account or consented to creating a contact for it. Identity verification and contact collection remain separate decisions. Keep the mapping scope narrow instead of promoting any formatted mention into a new CRM person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate ranges before using them
&lt;/h2&gt;

&lt;p&gt;Require integer coordinates within the UTF-16 representation of the selected source. Reject negative offsets, nonpositive lengths for this mapper, ranges past the end and boundaries that split a surrogate pair. Distinguish invalid metadata from a text encoding error. A replacement character inserted during recovery can conceal which input failed; an explicit review outcome is easier to diagnose.&lt;/p&gt;

&lt;p&gt;The following code is a small extraction diagnostic, not a complete Telegram entity validator. It deliberately accepts only one string and one range. It checks exact integer types so Python booleans are not silently accepted as coordinates. Strict decoding rejects a boundary inside the emoji's surrogate pair. The tests are artificial and do not send any messages or follow any URLs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;entity_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text must be a string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coordinates must be integers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-16-le&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;range outside text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-16-le&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;UnicodeDecodeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;range splits a surrogate pair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;😀 offer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;entity_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;entity_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid range accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-16 extraction checks passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the block as a local Python file and run it in a disposable environment. The expected terminal line is &lt;code&gt;UTF-16 extraction checks passed&lt;/code&gt;. An assertion failure or exception means this diagnostic did not pass; do not silently change its expected values. The article's accompanying research artifacts record the actual local execution. There was no production bot or CRM test during this check.&lt;/p&gt;

&lt;p&gt;This function does not validate entity types, required type-specific fields, nesting rules, URL schemes or CRM permissions. A string containing an unpaired surrogate can fail during encoding before range decoding; route that error to the same explicit invalid-input handling in a real adapter. Do not advertise this short helper as a complete security boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a staged mapping procedure
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Select a supported message body and its matching entity list. Keep source identity, access scope and message version with the mapping job. Do not use a caption's ranges on a text message or combine entity lists from different versions.&lt;/li&gt;
&lt;li&gt;Validate the body and metadata before producing derived fields. Resolve ranges in the documented coordinate system. Keep malformed inputs available only through the approved review process; avoid copying full enquiries into general application logs.&lt;/li&gt;
&lt;li&gt;Produce the plain-text note without removing meaningful customer content. Resolve required links into separate records with labels and destinations. Apply a specific allowlist or review policy for supported entity kinds rather than assuming every new type has an equivalent CRM representation.&lt;/li&gt;
&lt;li&gt;Construct the CRM payload through its documented API and escaping rules. If the target supports only plain text, send plain text. Do not enable a rich-text mode merely to make a single link clickable.&lt;/li&gt;
&lt;li&gt;Read back the test record through the authorized CRM interface. Compare the complete note, link labels and targets against the intended representation. An HTTP success response cannot show whether the CRM removed markup or changed line breaks.&lt;/li&gt;
&lt;li&gt;Record a mapping version and the observed outcome. Keep a rollback path for the adapter change, without sending duplicate notes or overwriting later operator edits. Retry behavior belongs to the operation's delivery design, not to the UTF-16 helper.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Use the separate &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;webhook idempotency guide&lt;/a&gt; for repeated delivery and ambiguous CRM writes. Correct range extraction does not prevent a second note after an uncertain timeout. Conversely, deduplicating updates does not repair an incorrect link label in the first note.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build acceptance cases around meaning
&lt;/h2&gt;

&lt;p&gt;A useful regression set varies the position and kind of source content. Include plain ASCII text, Cyrillic text, an emoji before the entity, several entities in one message and a caption with a link. Add a label whose destination differs from its visible words. These are test fixtures, not real customer conversations or evidence that a deployed connector already passes.&lt;/p&gt;

&lt;p&gt;For each case, write down the selected source field, exact source string, entity coordinates and expected extracted fragment. Then specify the intended CRM representation separately. A plain-text-only target may intentionally drop bold presentation while preserving every word. That is a supported conversion, not missing data, if the product owner approved it and the test records the decision.&lt;/p&gt;

&lt;p&gt;Negative cases should include an out-of-range endpoint, a surrogate-splitting boundary, missing required destination metadata and an unsupported entity kind. Decide whether each produces a plain-text fallback, manual review or a failed mapping job. A fallback must not quietly imply that links were preserved when only labels survived. Keep the status visible to the operator who relies on the note.&lt;/p&gt;

&lt;p&gt;Finally test edits: change the message body so the entity moves, and change a link destination while leaving the label unchanged. Verify that the new representation does not keep stale ranges or an old target. Compare the readback, not just the outgoing JSON. These are proposed integration acceptance tests; the local extraction diagnostic proves only its own artificial cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Close the incident at the boundary actually verified
&lt;/h2&gt;

&lt;p&gt;A precise resolution might say: the mapper used Python indices for Telegram UTF-16 ranges; the new mapper passed the local extraction fixture, while CRM readback remains pending. That is more useful than declaring Telegram fixed or the sales pipeline healthy. If the source body was already changed before mapping, correct that ownership error as well as the arithmetic.&lt;/p&gt;

&lt;p&gt;Monitor mapping failures by reason and adapter version, with only the minimum identifiers needed for investigation. Do not turn error telemetry into a second archive of private enquiries or link credentials. Before enabling production changes, assign responsibility for unsupported shapes and review access. The goal is a faithful, understandable CRM representation, not a promise to reproduce every Telegram presentation feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Telegram Bot API: Message&lt;/a&gt;, source fields and caption metadata, checked on the editorial date.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://core.telegram.org/bots/api#messageentity" rel="noopener noreferrer"&gt;Telegram Bot API: MessageEntity&lt;/a&gt;, UTF-16 coordinates and type-specific fields, checked on the editorial date.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/telegram-message-entities-crm-utf16?locale=en" rel="noopener noreferrer"&gt;Telegram Message Entities: Keep Links and Emoji Intact in Your CRM&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Текст Telegram в CRM: ссылки, эмодзи и смещения UTF-16 [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Thu, 05 Nov 2026 02:16:11 +0000</pubDate>
      <link>https://dev.to/dev24pro/tiekst-telegram-v-crm-ssylki-emodzi-i-smieshchieniia-utf-16-pieriekriostnaia-publikatsiia-4141</link>
      <guid>https://dev.to/dev24pro/tiekst-telegram-v-crm-ssylki-emodzi-i-smieshchieniia-utf-16-pieriekriostnaia-publikatsiia-4141</guid>
      <description>&lt;p&gt;Если обращение из Telegram попало в CRM с неверной ссылкой или обрезанным словом после эмодзи, проверьте преобразование текста до изменения вебхука. Telegram описывает участки текста через смещения в кодовых единицах UTF-16. Диапазоны относятся к исходному тексту или подписи, а не к очищенной копии. Сохраняйте строку вместе с её разметкой, затем отдельно формируйте обычную заметку и проверенные записи о ссылках.&lt;/p&gt;

&lt;p&gt;Разбор предназначен для разработчика собственной интеграции Telegram Bot API с CRM. Это проектное решение и локальная диагностика, не описание проверенной встроенной функции 24go.site. Предполагается, что бот уже получает обращения на законном основании. Для локального примера нужен Python, но не токен бота и не доступ к CRM. Проверка не испытывает действующий коннектор, не отображает недоверенный HTML и не даёт права собирать сведения о посторонних людях.&lt;/p&gt;

&lt;h2&gt;
  
  
  Сначала выберите исходное поле
&lt;/h2&gt;

&lt;p&gt;В &lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;объекте Message&lt;/a&gt; предусмотрены необязательные &lt;code&gt;text&lt;/code&gt; и &lt;code&gt;entities&lt;/code&gt; для текстовых сообщений. Подпись к медиа использует &lt;code&gt;caption&lt;/code&gt; и &lt;code&gt;caption_entities&lt;/code&gt;. Каждый список описывает соответствующую строку. Фотография с подписью не является текстовым сообщением с неожиданно пустым телом: обработчик, который читает только &lt;code&gt;text&lt;/code&gt;, может потерять обращение ещё до разбора форматирования.&lt;/p&gt;

&lt;p&gt;Передавайте эти две пары в один преобразователь как разные варианты входа и записывайте, какая пара выбрана. Если у поддерживаемого сообщения нет ни одного поля, фиксируйте явный результат «нет текста» или «неподдерживаемый формат». Пустая заметка о продаже не должна выглядеть успешным переносом. Остальным видам сообщений нужен собственный разбор схемы; эта инструкция не является полным декодером всех нынешних типов Bot API.&lt;/p&gt;

&lt;p&gt;Не подставляйте вместо текущего тела текст сообщения, на которое ответил человек, пересланный источник или автоматически составленное резюме. Это отдельный контекст. Если он нужен процессу, обозначайте его роль явно. Иначе менеджер может принять старую процитированную цену за текущую просьбу клиента. У каждой строки должен оставаться именно тот список диапазонов, который её описывает.&lt;/p&gt;

&lt;h2&gt;
  
  
  Почему эмодзи сдвигает ссылку
&lt;/h2&gt;

&lt;p&gt;Entity здесь означает типизированный участок строки: например, ссылку, упоминание или выделение. В &lt;a href="https://core.telegram.org/bots/api#messageentity" rel="noopener noreferrer"&gt;контракте MessageEntity&lt;/a&gt; и &lt;code&gt;offset&lt;/code&gt;, и &lt;code&gt;length&lt;/code&gt; измеряются в кодовых единицах UTF-16. Срез строки Python использует позиции кодовых точек Unicode. Для многих обычных букв эти системы совпадают, но различаются для символов, представленных суррогатной парой UTF-16, включая улыбающееся лицо в примере.&lt;/p&gt;

&lt;p&gt;Возьмём искусственную строку &lt;code&gt;😀 offer&lt;/code&gt;. Участок &lt;code&gt;offer&lt;/code&gt; начинается со смещения UTF-16 3 и имеет длину 5. Эмодзи занимает две кодовые единицы, следующий пробел ещё одну. Обычный срез Python с позиции 3 до 8 вернёт &lt;code&gt;ffer&lt;/code&gt;, а не нужное слово. Исключения может не быть: обработчик выдаст правдоподобный, но неправильный фрагмент.&lt;/p&gt;

&lt;p&gt;Из этого не следует правило «любой эмодзи считается за два». Некоторые видимые символы занимают одну кодовую единицу, а отображаемая последовательность эмодзи может состоять из нескольких кодовых точек. То, что читатель видит как один знак, не является надёжной единицей смещения. Используйте указанную кодировку или проверенную библиотечную функцию вместо собственного списка исключений для эмодзи.&lt;/p&gt;

&lt;h2&gt;
  
  
  Разделите исходник и представление в CRM
&lt;/h2&gt;

&lt;p&gt;Сохраняйте принятую строку и соответствующие метаданные вместе, соблюдая правила доступа и сроков хранения вашей интеграции. Производными данными могут быть обычная заметка, записи о ссылках и, при необходимости, безопасное форматированное представление. Исходник нужен для проверки преобразования и воспроизведения ошибки, а не для бессрочного накопления всех разговоров.&lt;/p&gt;

&lt;p&gt;Нормализация является отдельным преобразованием. Удаление начальных пробелов, замена переводов строк или нормализация Unicode могут изменить координаты либо выбранный ими текст. Сначала проверяйте и разрешайте диапазоны на неизменённой строке. Если следующему слою нужна другая строка, вычислите её координаты заново или не передавайте позиции в это представление. Старые смещения нельзя прикреплять к изменённому тексту в расчёте, что они сохранят смысл.&lt;/p&gt;

&lt;p&gt;При обновлении сообщения рассматривайте новое тело и новые метаданные как одну версию. Не переносите старые entities на новый текст. Клиент может изменить и подпись ссылки, и её адрес. Если заменить только видимую заметку, отдельная запись о ссылке останется устаревшей даже при верном расчёте UTF-16. Отдельно согласуйте, должна ли CRM заменять заметку, добавлять редакцию или отправлять изменение на проверку.&lt;/p&gt;

&lt;h2&gt;
  
  
  Видимая подпись и адрес ссылки различаются
&lt;/h2&gt;

&lt;p&gt;Entity типа &lt;code&gt;url&lt;/code&gt; выделяет URL непосредственно в строке. У &lt;code&gt;text_link&lt;/code&gt; в тексте находится видимая подпись, а отдельное необязательное поле &lt;code&gt;url&lt;/code&gt; содержит адрес перехода. Если сохранить только выделенный фрагмент, адрес потеряется. Если показать адрес так, будто это исходная подпись, исчезнет различие между словами человека и целью перехода. Для процесса, которому нужны ссылки, сохраняйте оба значения и тип entity.&lt;/p&gt;

&lt;p&gt;Наличие разметки Telegram не подтверждает безопасность адреса. Подпись «портал компании» может вести на другой сайт. Не загружайте адрес автоматически при создании заметки: обращение к недоверенному URL создаёт отдельную границу сетевой безопасности. Если нужны предпросмотры, используйте отдельно проверенный сервис с проверками назначения, сетевыми ограничениями и понятными правилами обработки данных.&lt;/p&gt;

&lt;p&gt;Для CRM с обычным текстом часто достаточно показать подпись и отдельно обозначенный адрес. Для форматированной заметки экранируйте обычный текст и стройте только поддерживаемую разметку через безопасный механизм отображения. Не склеивайте HTML с клиентским текстом и не воспринимайте оформление как разрешение что-либо выполнять. Корректный диапазон подтверждает расположение фрагмента, но не безопасность его содержимого.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Входные данные&lt;/th&gt;
&lt;th&gt;Что сохранить&lt;/th&gt;
&lt;th&gt;Чего не следует заключать&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Фрагмент исходной строки и тип&lt;/td&gt;
&lt;td&gt;Что адрес безопасно открывать&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text_link&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Подпись и отдельный адрес&lt;/td&gt;
&lt;td&gt;Что подпись описывает настоящий домен&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;text_mention&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Подпись и допустимую ссылку на пользователя, если она нужна&lt;/td&gt;
&lt;td&gt;Что подпись является постоянным username&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bold&lt;/code&gt; или &lt;code&gt;italic&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Исходный текст и необязательное поддерживаемое оформление&lt;/td&gt;
&lt;td&gt;Что выделение меняет приоритет заявки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;code&lt;/code&gt; или &lt;code&gt;pre&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Буквальный текст и поддерживаемые метаданные&lt;/td&gt;
&lt;td&gt;Что команду нужно выполнить&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Неизвестный тип&lt;/td&gt;
&lt;td&gt;Текст и явное решение о неподдерживаемом типе&lt;/td&gt;
&lt;td&gt;Что всё сообщение обязательно удалять&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Согласно API, &lt;code&gt;text_mention&lt;/code&gt; может содержать объект пользователя. Это не доказывает, что отправитель владеет упомянутой учётной записью или что её владелец согласен на создание контакта. Проверка личности и сбор контактных данных остаются отдельными решениями. Не превращайте любое оформленное упоминание в нового человека в CRM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Проверяйте диапазон перед извлечением
&lt;/h2&gt;

&lt;p&gt;Проверяйте целочисленные координаты в пределах представления выбранной строки в UTF-16. Для этого преобразователя отклоняйте отрицательное смещение, неположительную длину, выход за конец и границу внутри суррогатной пары. Различайте некорректные метаданные и ошибку кодирования текста. Автоматический символ замены может скрыть, какой вход сломан; явный результат проверки проще расследовать.&lt;/p&gt;

&lt;p&gt;Следующий код является небольшой диагностикой извлечения, а не полным валидатором Telegram entities. Он намеренно принимает одну строку и один диапазон. Точная проверка типа не позволяет Python принять логические значения за координаты. Строгое декодирование отклоняет границу внутри пары эмодзи. Тесты искусственные: они не отправляют сообщения и не переходят по ссылкам.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;entity_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text must be a string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coordinates must be integers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid range&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-16-le&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;range outside text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-16-le&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;UnicodeDecodeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;range splits a surrogate pair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;

&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;😀 offer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;entity_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;entity_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid range accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-16 extraction checks passed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Сохраните блок в локальный файл Python и выполните в одноразовой среде. Ожидаемая строка терминала: &lt;code&gt;UTF-16 extraction checks passed&lt;/code&gt;. Ошибка утверждения или исключение означают, что эта диагностика не пройдена; не меняйте ожидаемые значения молча. В исследовательских артефактах статьи сохранён фактический локальный запуск. Действующий бот и CRM в этой проверке не участвовали.&lt;/p&gt;

&lt;p&gt;Функция не проверяет типы entities, обязательные поля отдельных типов, правила вложенности, схемы URL и разрешения CRM. Строка с непарным суррогатом может вызвать ошибку уже при кодировании, до декодирования диапазона. В настоящем адаптере направляйте такую ошибку в тот же явный процесс обработки некорректного входа. Короткий вспомогательный пример не является полной границей безопасности.&lt;/p&gt;

&lt;h2&gt;
  
  
  Порядок преобразования в интеграции
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Выберите поддерживаемое тело сообщения и соответствующий список entities. Свяжите задание с источником, областью доступа и версией сообщения. Не применяйте диапазоны подписи к текстовому сообщению и не объединяйте списки разных версий.&lt;/li&gt;
&lt;li&gt;Проверьте строку и метаданные до формирования производных полей. Извлекайте диапазоны в документированной системе координат. Некорректные входы должны быть доступны только через согласованный процесс разбора; не копируйте полные обращения в общие журналы приложения.&lt;/li&gt;
&lt;li&gt;Сформируйте обычную заметку без удаления значимого клиентского текста. Нужные ссылки вынесите в отдельные записи с подписями и адресами. Задайте явный список поддерживаемых типов или правила проверки вместо предположения, что любой новый тип имеет аналог в CRM.&lt;/li&gt;
&lt;li&gt;Подготовьте данные по документированному API и правилам экранирования конкретной CRM. Если цель поддерживает только обычный текст, отправляйте обычный текст. Не включайте форматированный режим ради одной кликабельной ссылки.&lt;/li&gt;
&lt;li&gt;Перечитайте тестовую запись через разрешённый интерфейс CRM. Сравните полную заметку, подписи и адреса с задуманным представлением. Успешный HTTP-ответ не показывает, удалила ли CRM разметку или изменила переводы строк.&lt;/li&gt;
&lt;li&gt;Запишите версию преобразователя и наблюдаемый результат. Подготовьте откат изменения адаптера без дублирующих заметок и перезаписи последующих правок оператора. Повторы относятся к устройству доставки операции, а не к функции извлечения UTF-16.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Для повторных доставок и неоднозначных записей в CRM есть отдельная &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;инструкция по идемпотентности вебхука&lt;/a&gt;. Правильное извлечение диапазона не предотвращает вторую заметку после таймаута с неизвестным исходом. И наоборот, защита от повторов не исправляет неверную подпись ссылки в первой заметке.&lt;/p&gt;

&lt;h2&gt;
  
  
  Проверяйте сохранение смысла
&lt;/h2&gt;

&lt;p&gt;В набор проверок включите обычный латинский текст, кириллицу, эмодзи перед выделенным участком, несколько entities в одном сообщении и подпись к медиа со ссылкой. Отдельно добавьте подпись, которая не совпадает с адресом назначения. Это тестовые данные, а не настоящие клиентские разговоры и не доказательство того, что действующий коннектор уже прошёл испытания.&lt;/p&gt;

&lt;p&gt;Для каждого случая запишите выбранное поле, точную исходную строку, координаты и ожидаемый фрагмент. Затем отдельно определите представление в CRM. Система с обычным текстом может намеренно убрать жирное оформление, сохранив все слова. Если владелец процесса согласовал это решение и оно отражено в проверке, такое преобразование не считается потерей данных.&lt;/p&gt;

&lt;p&gt;Отрицательные сценарии должны включать выход за конец строки, границу внутри суррогатной пары, отсутствие нужного адреса ссылки и неподдерживаемый тип. Решите, что происходит в каждом случае: обычный текст с ограничениями, ручная проверка или ошибка задания. Запасной вариант не должен молча обещать сохранение ссылок, если остались только подписи. Результат должен видеть оператор, который полагается на заметку.&lt;/p&gt;

&lt;p&gt;Наконец, проверьте редактирование: измените тело так, чтобы участок сдвинулся, затем смените адрес ссылки, оставив подпись прежней. Новое представление не должно сохранять старые координаты или старое назначение. Сравнивайте перечитанную запись, а не только отправленный JSON. Это план приёмки интеграции; локальная диагностика извлечения подтверждает лишь собственные искусственные случаи.&lt;/p&gt;

&lt;h2&gt;
  
  
  Завершайте расследование на проверенной границе
&lt;/h2&gt;

&lt;p&gt;Точный результат может звучать так: преобразователь использовал индексы Python вместо смещений UTF-16; новая версия прошла локальный тест извлечения, но чтение результата CRM ещё предстоит. Это полезнее заявления, что Telegram исправлен или вся воронка работает. Если исходную строку меняли до разбора диапазонов, исправьте и порядок преобразований, а не только арифметику.&lt;/p&gt;

&lt;p&gt;Наблюдайте причины ошибок и версии адаптера, сохраняя минимум идентификаторов для расследования. Диагностика не должна становиться вторым архивом частных обращений или секретных параметров ссылок. Перед производственным изменением назначьте ответственного за неподдерживаемые форматы и доступ к проверке. Цель состоит в точном и понятном представлении обращения в CRM, а не в обещании воспроизвести все особенности оформления Telegram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Источники
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Telegram Bot API: Message&lt;/a&gt;: поля текста, подписи и их метаданные; проверено в редакционную дату.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://core.telegram.org/bots/api#messageentity" rel="noopener noreferrer"&gt;Telegram Bot API: MessageEntity&lt;/a&gt;: координаты UTF-16 и поля отдельных типов; проверено в редакционную дату.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/telegram-message-entities-crm-utf16?locale=ru" rel="noopener noreferrer"&gt;Текст Telegram в CRM: ссылки, эмодзи и смещения UTF-16&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram Bot Missing Events: Check allowed_updates Before Resetting Delivery [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Tue, 03 Nov 2026 05:33:15 +0000</pubDate>
      <link>https://dev.to/dev24pro/telegram-bot-missing-events-check-allowedupdates-before-resetting-delivery-cross-post-269e</link>
      <guid>https://dev.to/dev24pro/telegram-bot-missing-events-check-allowedupdates-before-resetting-delivery-cross-post-269e</guid>
      <description>&lt;h1&gt;
  
  
  Telegram Bot Missing Events: Check allowed_updates Before Resetting Delivery
&lt;/h1&gt;

&lt;p&gt;If your Telegram bot receives some events but misses another kind, check its update subscription and application routing before changing transport. &lt;code&gt;allowed_updates&lt;/code&gt; selects update types; it does not grant access to a chat or prove that a CRM worker processed an event. Omitting the parameter preserves the previous setting. An empty list does not literally enable every update type.&lt;/p&gt;

&lt;p&gt;This guide is for the maintainer of a custom Telegram Bot API integration that routes incoming enquiries to a CRM. You need authorized access to its configuration, sanitized diagnostics and a test environment. It does not describe a verified built-in 24go.site feature. The procedure below is a proposed operational test, not a claim that a live bot or CRM was tested while writing this article. No working token is needed to read the checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  First identify what is actually missing
&lt;/h2&gt;

&lt;p&gt;“Messages stopped” can describe several different failures. A text message is normally represented by a &lt;code&gt;message&lt;/code&gt; update; pressing an inline callback button produces &lt;code&gt;callback_query&lt;/code&gt;; a channel post uses &lt;code&gt;channel_post&lt;/code&gt;. Those are different fields on the &lt;a href="https://core.telegram.org/bots/api#update" rel="noopener noreferrer"&gt;Update object&lt;/a&gt;. A handler that only reads &lt;code&gt;update.message&lt;/code&gt; will not process every event delivered in a valid update.&lt;/p&gt;

&lt;p&gt;Write a specific incident statement: a human sent a new text message to the test bot in a private conversation at a known time, but the receiver did not persist a corresponding event. Compare that with “the event was persisted, but no lead appeared.” The second statement points beyond Telegram delivery. Record the event kind, chat type and last observed processing stage, not the complete conversation or a customer phone number.&lt;/p&gt;

&lt;p&gt;Use a unique, non-sensitive marker for a controlled test. Keep it out of campaign automation and stop any downstream production sales action for the test identity through an approved test configuration. A successful diagnostic must not accidentally create a real follow-up sequence. If isolation cannot be established, stop before sending the test and have the integration owner arrange it.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are separate gates between a user action and a lead
&lt;/h2&gt;

&lt;p&gt;Think of the route as eligibility → subscription → transport → receiver → dispatcher → CRM worker. Eligibility means the bot may receive that kind of event in that chat. Subscription means the configured list permits its update type. Transport concerns webhook or polling delivery. The receiver validates and durably accepts the request. The dispatcher chooses an application handler. Finally, a worker performs a permitted business operation.&lt;/p&gt;

&lt;p&gt;Changing one gate does not fix the others. Adding &lt;code&gt;message&lt;/code&gt; to a subscription cannot grant visibility into arbitrary group conversations. Granting a bot more group permissions cannot make a dispatcher handle &lt;code&gt;callback_query&lt;/code&gt;. Returning HTTP success does not show that an event reached durable storage. Seeing a persisted event does not establish that the CRM committed a new record.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://core.telegram.org/bots/faq#what-messages-will-my-bot-get" rel="noopener noreferrer"&gt;Bot FAQ&lt;/a&gt; describes privacy-mode rules for ordinary group messages, including commands addressed to a bot and replies. Use those rules only within their stated scope. The API changelog now also describes newer bot-to-bot and guest-mode capabilities, while older FAQ statements are broader. This guide deliberately uses a human sender and ordinary private messages for its baseline, not another bot, guest messages or a claim that all current bot communication modes behave identically.&lt;/p&gt;

&lt;h2&gt;
  
  
  What allowed_updates does, including the exceptions
&lt;/h2&gt;

&lt;p&gt;The current &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;setWebhook contract&lt;/a&gt; and &lt;a href="https://core.telegram.org/bots/api#getupdates" rel="noopener noreferrer"&gt;getUpdates contract&lt;/a&gt; document three important cases:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Configuration input&lt;/th&gt;
&lt;th&gt;Documented meaning&lt;/th&gt;
&lt;th&gt;Diagnostic consequence&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Parameter omitted&lt;/td&gt;
&lt;td&gt;Reuse the previous setting&lt;/td&gt;
&lt;td&gt;Redeploying without the field is not a reset&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit list such as &lt;code&gt;["message", "callback_query"]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Subscribe to those update types&lt;/td&gt;
&lt;td&gt;Other required types need an explicit decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Empty list &lt;code&gt;[]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;All update types except &lt;code&gt;chat_member&lt;/code&gt;, &lt;code&gt;message_reaction&lt;/code&gt;, &lt;code&gt;message_reaction_count&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Do not describe this as an unrestricted subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are further eligibility requirements for particular types. The Update documentation says &lt;code&gt;chat_member&lt;/code&gt; requires the bot to be an administrator and the type to be explicitly requested. Reaction updates also have their own documented requirements. Do not add all possible types as a generic troubleshooting step: each additional event class needs a handler, access policy and data-retention decision.&lt;/p&gt;

&lt;p&gt;For private chats, &lt;code&gt;my_chat_member&lt;/code&gt; concerns the bot being blocked or unblocked. It is not the same event as a change to another member represented by &lt;code&gt;chat_member&lt;/code&gt;. If your application expects one and subscribes to the other, a transport restart will not repair the mismatch. Match the required business signal to the exact update field before editing the subscription.&lt;/p&gt;

&lt;p&gt;These settings do not retroactively change updates created before the configuration call. Telegram explicitly warns that unwanted updates may still arrive briefly. Your receiver must remain able to reject, ignore or safely store unsupported older events according to policy. A configuration response reporting success is not proof that every subsequent request immediately has only the new shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect without switching delivery modes
&lt;/h2&gt;

&lt;p&gt;For a webhook bot, have the authorized operator inspect &lt;a href="https://core.telegram.org/bots/api#getwebhookinfo" rel="noopener noreferrer"&gt;getWebhookInfo&lt;/a&gt; through the existing safe administration tool. The method reports the webhook URL, pending-update count and optional error details and subscription information. Review the response before sharing it: the URL may reveal internal routing or sensitive path data. Never paste the bot token, full command URL or secret header into a support ticket.&lt;/p&gt;

&lt;p&gt;A nonempty webhook URL establishes that a webhook is configured, not that its current receiver is healthy. A pending count is a point-in-time measure of updates awaiting delivery, not an application backlog metric. A zero count does not prove a user action was eligible, subscribed to, delivered and processed. Optional last-error fields describe an observed error, not the full history of every attempt or a guaranteed current outage.&lt;/p&gt;

&lt;p&gt;If the URL is empty, first establish who owns polling and where its configuration is recorded. Do not launch a second polling consumer from your laptop as a diagnostic. The API documents that &lt;code&gt;getUpdates&lt;/code&gt; does not work while an outgoing webhook is set. Polling also has acknowledgement behavior through &lt;code&gt;offset&lt;/code&gt;; a casual test can interfere with the application's update handling. Transport switching is a separate planned operation, not a harmless way to inspect missing event types.&lt;/p&gt;

&lt;p&gt;Compare three things: the intended subscription in deployment configuration, the request actually used by the current integration to register or poll, and observable state from the authorized management path. A framework can generate that request for you, so checking only a handwritten configuration file may miss the active behavior. Record configuration versions, not secret values.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use observations to choose the next investigation
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation in the isolated test&lt;/th&gt;
&lt;th&gt;Next boundary to inspect&lt;/th&gt;
&lt;th&gt;Avoid concluding&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Private human message absent at ingress&lt;/td&gt;
&lt;td&gt;Bot identity, subscription and transport evidence&lt;/td&gt;
&lt;td&gt;That the CRM discarded it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Message arrives but callback does not&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;callback_query&lt;/code&gt; subscription and callback dispatch&lt;/td&gt;
&lt;td&gt;That all Telegram delivery is broken&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private test works; ordinary group text does not&lt;/td&gt;
&lt;td&gt;Group eligibility, privacy mode and intended interaction&lt;/td&gt;
&lt;td&gt;That broader privileges are automatically necessary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw accepted update contains the expected field; handler does not run&lt;/td&gt;
&lt;td&gt;Application router and supported update schema&lt;/td&gt;
&lt;td&gt;That resetting the webhook will fix code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Handler runs; CRM result is missing&lt;/td&gt;
&lt;td&gt;Worker receipt, business rules and CRM reconciliation&lt;/td&gt;
&lt;td&gt;That Telegram lost the update&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Older unwanted event appears after filter change&lt;/td&gt;
&lt;td&gt;Creation time and documented transition behavior&lt;/td&gt;
&lt;td&gt;That the new subscription is necessarily ignored&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A sanitized ingress record should establish which top-level event field was present and whether durable acceptance succeeded. It need not retain every message body indefinitely. If your only observable signal is “no reply,” add narrow test diagnostics before changing permissions: reply sending is itself a separate operation and can fail even when receipt worked.&lt;/p&gt;

&lt;p&gt;For the authentication boundary, use the separate &lt;a href="https://www.24go.site/blog/telegram-webhook-secret-token-validation?locale=en" rel="noopener noreferrer"&gt;webhook secret validation guide&lt;/a&gt;. For repeated accepted events and uncertain CRM writes, use the &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;idempotency guide&lt;/a&gt;. Those controls remain necessary but answer different questions from an update-type subscription.&lt;/p&gt;

&lt;h2&gt;
  
  
  A controlled change and acceptance procedure
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Record the intended business events and map each to an exact Update field. Identify an owner for every handler. Include existing required types when proposing the replacement list; do not silently remove working flows.&lt;/li&gt;
&lt;li&gt;Save a sanitized baseline: transport owner, active configuration version, relevant subscription evidence and the stages currently observable. Keep private routing and credentials in their approved store, not the incident report.&lt;/li&gt;
&lt;li&gt;Review the proposed change in the integration's normal deployment workflow. Registration parameters include more than the update list. Preserve approved endpoint, certificate and authentication settings rather than improvising a fresh command from a blog example.&lt;/li&gt;
&lt;li&gt;Apply only the approved subscription change through that workflow. Do not set &lt;code&gt;drop_pending_updates&lt;/code&gt; to clear an error: the documented purpose of that option is to drop pending updates, not repair routing.&lt;/li&gt;
&lt;li&gt;Generate a new human private-message test and each additional required interaction in the isolated environment. Check the exact event field, durable receipt, selected handler and downstream test result separately.&lt;/li&gt;
&lt;li&gt;Repeat a previously working event class as a regression test. A newly arriving callback is not a success if the change stopped message processing. Observe old queued events without treating their mere presence as a failed configuration.&lt;/li&gt;
&lt;li&gt;Record results and either retain the configuration or roll back through the same approved path. A rollback is another configuration transition; it does not recover events already discarded or guarantee retroactive delivery of excluded events.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Define success before the change. For example, the private human message should produce an accepted &lt;code&gt;message&lt;/code&gt; receipt, reach the intended test handler and yield the expected test record or explicit no-write decision. A callback test should produce a separate callback receipt and its own authorization result. No live timing or delivery guarantee is inferred from this proposed plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the incident report should say
&lt;/h2&gt;

&lt;p&gt;Close the incident with the narrowest supported finding: which event class was absent, which boundary failed, what configuration changed, and what fresh tests passed. Attach sanitized request metadata and configuration revision references. If only the subscription mismatch was corrected, say that; do not claim the entire sales funnel is healthy.&lt;/p&gt;

&lt;p&gt;If evidence stops at ingress, leave downstream delivery unverified. If no authorized bot-management access exists, record that blocker instead of borrowing credentials or deleting the webhook. If group eligibility remains uncertain, keep the human private-message baseline separate from a future permission review. A quiet bot can be healthy in an inactive chat, and an active webhook can still feed a broken worker. Only a controlled observation across the relevant boundaries resolves the particular missing-event question.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/telegram-bot-missing-update-types?locale=en" rel="noopener noreferrer"&gt;Telegram Bot Missing Events: Check allowed_updates Before Resetting Delivery&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram-бот не получает часть событий: проверка allowed_updates без сброса очереди [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Tue, 03 Nov 2026 05:33:14 +0000</pubDate>
      <link>https://dev.to/dev24pro/telegram-bot-nie-poluchaiet-chast-sobytii-provierka-allowedupdates-biez-sbrosa-ochieriedi-pieriekriostnaia-22dg</link>
      <guid>https://dev.to/dev24pro/telegram-bot-nie-poluchaiet-chast-sobytii-provierka-allowedupdates-biez-sbrosa-ochieriedi-pieriekriostnaia-22dg</guid>
      <description>&lt;h1&gt;
  
  
  Telegram-бот не получает часть событий: проверка allowed_updates без сброса очереди
&lt;/h1&gt;

&lt;p&gt;Если одни события доходят до Telegram-бота, а другие пропали, сначала проверьте подписку на типы обновлений и маршрутизацию в приложении. Параметр &lt;code&gt;allowed_updates&lt;/code&gt; выбирает типы событий, но не даёт доступ к чату и не подтверждает обработку в CRM. Если параметр не передать, сохранится прежняя настройка. Пустой список тоже не означает буквально все существующие типы обновлений.&lt;/p&gt;

&lt;p&gt;Разбор предназначен для владельца собственной интеграции Telegram Bot API с CRM. Нужен разрешённый доступ к её настройкам, обезличенным диагностическим данным и тестовой среде. Это не описание проверенной встроенной функции 24go.site. Ниже предложен эксплуатационный план проверки, а не отчёт об испытании действующего бота или CRM при подготовке статьи. Для чтения плана рабочий токен не требуется.&lt;/p&gt;

&lt;h2&gt;
  
  
  Сначала уточните, чего именно нет
&lt;/h2&gt;

&lt;p&gt;За словами «перестали приходить сообщения» могут стоять разные сбои. Обычное текстовое сообщение представлено обновлением &lt;code&gt;message&lt;/code&gt;, нажатие inline-кнопки с callback — &lt;code&gt;callback_query&lt;/code&gt;, публикация канала - &lt;code&gt;channel_post&lt;/code&gt;. Это разные поля &lt;a href="https://core.telegram.org/bots/api#update" rel="noopener noreferrer"&gt;объекта Update&lt;/a&gt;. Если обработчик читает только &lt;code&gt;update.message&lt;/code&gt;, он не обработает все события, которые сервер корректно доставил приложению.&lt;/p&gt;

&lt;p&gt;Сформулируйте наблюдение точно: человек написал новое текстовое сообщение тестовому боту в личном чате в известное время, но приёмник не сохранил соответствующее событие. Сравните с другим наблюдением: событие сохранено, но лид не появился. Во втором случае поиск уже выходит за границу доставки Telegram. Запишите тип события, тип чата и последний подтверждённый этап обработки, а не всю переписку или телефон клиента.&lt;/p&gt;

&lt;p&gt;Для управляемой проверки используйте уникальную метку без персональных данных. Исключите тестовую учётную запись из производственных продаж через согласованную тестовую конфигурацию. Диагностика не должна случайно запустить настоящую цепочку последующих сообщений. Если изоляция не подтверждена, остановитесь до отправки теста: сначала ответственный за интеграцию должен подготовить безопасную среду.&lt;/p&gt;

&lt;h2&gt;
  
  
  Между действием человека и лидом несколько независимых границ
&lt;/h2&gt;

&lt;p&gt;Путь удобно представить так: допустимость события → подписка → транспорт → приёмник → диспетчер → исполнитель CRM. Допустимость означает, что бот вправе получить этот вид события в этом чате. Подписка разрешает соответствующий тип обновления. Транспорт доставляет его через вебхук или polling. Приёмник проверяет запрос и надёжно сохраняет принятые данные. Диспетчер выбирает прикладной обработчик, а исполнитель делает разрешённую бизнес-операцию.&lt;/p&gt;

&lt;p&gt;Изменение одной границы не исправляет остальные. Добавление &lt;code&gt;message&lt;/code&gt; в список подписки не открывает произвольные групповые разговоры. Дополнительные права в группе не заставят диспетчер обрабатывать &lt;code&gt;callback_query&lt;/code&gt;. Успешный HTTP-ответ не доказывает сохранение события. Сохранённое событие ещё не означает, что CRM зафиксировала новую запись.&lt;/p&gt;

&lt;p&gt;В &lt;a href="https://core.telegram.org/bots/faq#what-messages-will-my-bot-get" rel="noopener noreferrer"&gt;FAQ Telegram&lt;/a&gt; описаны правила privacy mode для обычных групповых сообщений, включая команды, адресованные боту, и ответы. Применяйте их в указанных границах. В актуальном журнале изменений API также описаны новые возможности общения между ботами и guest mode, тогда как старые формулировки FAQ шире. Поэтому исходный тест здесь - сообщение человека в обычном личном чате. Мы не используем другого бота, гостевые сообщения и не утверждаем, что все нынешние режимы общения работают одинаково.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что означает allowed_updates и где исключения
&lt;/h2&gt;

&lt;p&gt;В текущих контрактах &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;setWebhook&lt;/a&gt; и &lt;a href="https://core.telegram.org/bots/api#getupdates" rel="noopener noreferrer"&gt;getUpdates&lt;/a&gt; описаны три важных варианта:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Входная настройка&lt;/th&gt;
&lt;th&gt;Документированный смысл&lt;/th&gt;
&lt;th&gt;Следствие для диагностики&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Параметр отсутствует&lt;/td&gt;
&lt;td&gt;Используется предыдущая настройка&lt;/td&gt;
&lt;td&gt;Повторный деплой без поля не сбрасывает фильтр&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Явный список, например &lt;code&gt;["message", "callback_query"]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Подписка на перечисленные типы&lt;/td&gt;
&lt;td&gt;Остальные необходимые типы требуют отдельного решения&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Пустой список &lt;code&gt;[]&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Все типы, кроме &lt;code&gt;chat_member&lt;/code&gt;, &lt;code&gt;message_reaction&lt;/code&gt;, &lt;code&gt;message_reaction_count&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Нельзя называть такой режим неограниченной подпиской&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;У отдельных типов есть дополнительные условия получения. В описании Update сказано, что для &lt;code&gt;chat_member&lt;/code&gt; бот должен быть администратором, а тип нужно запросить явно. События реакций также имеют собственные документированные условия. Не включайте все возможные типы как универсальный способ ремонта: для каждого нужны обработчик, политика доступа и решение о хранении данных.&lt;/p&gt;

&lt;p&gt;В личных чатах &lt;code&gt;my_chat_member&lt;/code&gt; относится к блокировке или разблокировке бота пользователем. Это не то же самое, что изменение статуса другого участника через &lt;code&gt;chat_member&lt;/code&gt;. Если программа ждёт одно событие, а подписана на другое, перезапуск транспорта не устраняет расхождение. До правки настройки сопоставьте нужный бизнес-сигнал с точным полем Update.&lt;/p&gt;

&lt;p&gt;Новая настройка не меняет задним числом обновления, созданные до вызова метода. Telegram отдельно предупреждает, что нежелательные обновления ещё некоторое время могут приходить. Приёмник должен уметь безопасно отклонять, игнорировать или сохранять такие старые события согласно своей политике. Успешный ответ на изменение конфигурации не доказывает, что каждый следующий запрос немедленно будет иметь только новый состав полей.&lt;/p&gt;

&lt;h2&gt;
  
  
  Посмотрите состояние, не переключая транспорт
&lt;/h2&gt;

&lt;p&gt;Для бота с вебхуком уполномоченный оператор может посмотреть &lt;a href="https://core.telegram.org/bots/api#getwebhookinfo" rel="noopener noreferrer"&gt;getWebhookInfo&lt;/a&gt; через существующий безопасный инструмент администрирования. Метод сообщает URL вебхука, число ожидающих обновлений и необязательные сведения об ошибках и подписке. Перед отправкой результата в поддержку проверьте его: адрес может раскрыть внутреннюю маршрутизацию или чувствительную часть пути. Не вставляйте токен бота, полный URL команды или секретный заголовок в тикет.&lt;/p&gt;

&lt;p&gt;Непустой URL показывает наличие настройки вебхука, но не исправность текущего приёмника. Число ожидающих обновлений — состояние доставки на конкретный момент, а не размер очереди вашего приложения. Ноль не доказывает, что действие пользователя было допустимо, прошло фильтр, доставлено и обработано. Необязательные поля последней ошибки описывают наблюдавшуюся ошибку, а не полную историю всех попыток или обязательную текущую аварию.&lt;/p&gt;

&lt;p&gt;Если URL пустой, сначала выясните, кто управляет polling и где записаны его настройки. Не запускайте второй потребитель обновлений на своём ноутбуке ради проверки. Документация говорит, что &lt;code&gt;getUpdates&lt;/code&gt; не работает при установленном исходящем вебхуке. Кроме того, polling подтверждает обновления с помощью &lt;code&gt;offset&lt;/code&gt;: случайный тест способен вмешаться в обработку приложения. Переключение транспорта — отдельное плановое изменение, а не безвредный просмотр пропавших типов событий.&lt;/p&gt;

&lt;p&gt;Сопоставьте три источника: желаемую подписку в конфигурации деплоя, фактический запрос регистрации или polling, который делает работающая интеграция, и наблюдаемое состояние в разрешённом административном контуре. Фреймворк может сформировать запрос самостоятельно, поэтому одного рукописного файла настроек недостаточно. В отчёт записывайте версии конфигурации, не значения секретов.&lt;/p&gt;

&lt;h2&gt;
  
  
  Как выбрать следующую проверку по наблюдению
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Наблюдение в изолированном тесте&lt;/th&gt;
&lt;th&gt;Следующая граница проверки&lt;/th&gt;
&lt;th&gt;Чего оно ещё не доказывает&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Личное сообщение человека отсутствует на входе&lt;/td&gt;
&lt;td&gt;Идентичность бота, подписка и транспорт&lt;/td&gt;
&lt;td&gt;Что его отбросила CRM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сообщение приходит, callback отсутствует&lt;/td&gt;
&lt;td&gt;Подписка &lt;code&gt;callback_query&lt;/code&gt; и диспетчер callback&lt;/td&gt;
&lt;td&gt;Что вся доставка Telegram сломана&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Личный тест работает, обычный групповой текст нет&lt;/td&gt;
&lt;td&gt;Допустимость в группе, privacy mode и задуманный сценарий&lt;/td&gt;
&lt;td&gt;Что обязательно нужны более широкие права&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;В принятом Update есть нужное поле, обработчик не запущен&lt;/td&gt;
&lt;td&gt;Прикладной маршрутизатор и поддерживаемая схема&lt;/td&gt;
&lt;td&gt;Что сброс вебхука исправит код&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Обработчик запущен, результата CRM нет&lt;/td&gt;
&lt;td&gt;Квитанция исполнителя, бизнес-правила и сверка CRM&lt;/td&gt;
&lt;td&gt;Что Telegram потерял событие&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;После смены фильтра пришло старое ненужное событие&lt;/td&gt;
&lt;td&gt;Время создания и документированный переход&lt;/td&gt;
&lt;td&gt;Что новая подписка обязательно проигнорирована&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Обезличенная запись приёмника должна позволять понять, какое поле события было в верхнем уровне и состоялось ли надёжное принятие. Для этого необязательно бессрочно хранить тела всех сообщений. Если единственный сигнал - отсутствие ответа бота, сначала добавьте узкую тестовую диагностику. Отправка ответа является отдельной операцией и может не сработать даже при успешном приёме.&lt;/p&gt;

&lt;p&gt;За проверку подлинности входящего запроса отвечает отдельная &lt;a href="https://www.24go.site/blog/telegram-webhook-secret-token-validation?locale=ru" rel="noopener noreferrer"&gt;инструкция по webhook secret&lt;/a&gt;. Повторы уже принятых событий и неопределённую запись в CRM разбирает &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;руководство по идемпотентности&lt;/a&gt;. Эти меры остаются нужными, но отвечают на другие вопросы, чем подписка на тип обновления.&lt;/p&gt;

&lt;h2&gt;
  
  
  Порядок управляемого изменения и приёмки
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Запишите необходимые бизнес-события и сопоставьте каждое с точным полем Update. Назначьте ответственного за обработчик. В предложенный список включите уже необходимые типы: нельзя незаметно убрать работающий сценарий.&lt;/li&gt;
&lt;li&gt;Сохраните обезличенное исходное состояние: владельца транспорта, версию активной конфигурации, доступные доказательства подписки и наблюдаемые этапы. Частные адреса и секреты оставьте в предусмотренном хранилище, а не в отчёте об инциденте.&lt;/li&gt;
&lt;li&gt;Проверьте изменение в обычном процессе развёртывания интеграции. Параметры регистрации не ограничиваются списком событий. Сохраните согласованные endpoint, сертификат и аутентификацию, не собирайте новую команду по общему примеру из статьи.&lt;/li&gt;
&lt;li&gt;Примените через этот процесс только согласованную правку подписки. Не включайте &lt;code&gt;drop_pending_updates&lt;/code&gt;, чтобы убрать ошибку: документированное назначение параметра — удаление ожидающих обновлений, а не исправление маршрутизации.&lt;/li&gt;
&lt;li&gt;В изолированной среде создайте новое личное сообщение человека и каждое дополнительное необходимое взаимодействие. Раздельно проверьте поле события, надёжную квитанцию приёмника, выбранный обработчик и тестовый результат следующего этапа.&lt;/li&gt;
&lt;li&gt;Повторите ранее работавший тип события как регрессионный тест. Появление callback не является успехом, если после правки перестали обрабатываться сообщения. Наблюдайте старые события очереди, не считая само их присутствие признаком неисправной настройки.&lt;/li&gt;
&lt;li&gt;Зафиксируйте результат и оставьте настройку либо откатите её тем же согласованным способом. Откат - ещё один переход конфигурации: он не возвращает уже удалённые события и не гарантирует ретроспективную доставку исключённых типов.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Критерий успеха определите до изменения. Например, личное сообщение человека должно дать квитанцию принятого &lt;code&gt;message&lt;/code&gt;, попасть в назначенный тестовый обработчик и привести к ожидаемой тестовой записи либо явному решению не писать данные. Для callback нужна отдельная квитанция и собственный результат проверки прав. Этот предложенный план не содержит гарантии сроков доставки и не выдаётся за выполненную сетевую проверку.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что написать в отчёте об инциденте
&lt;/h2&gt;

&lt;p&gt;Закрывайте задачу самым узким подтверждённым выводом: какой тип отсутствовал, на какой границе был сбой, что поменяли и какие новые тесты прошли. Приложите обезличенные метаданные и ссылки на ревизии конфигурации. Если исправлено только расхождение подписки, так и напишите: это не подтверждение исправности всей воронки продаж.&lt;/p&gt;

&lt;p&gt;Если доказательства заканчиваются на приёмнике, результат последующих этапов остаётся непроверенным. Нет разрешённого доступа к администрированию бота - укажите этот блокер, не заимствуйте чужие учётные данные и не удаляйте вебхук. Если неясна допустимость групповых сообщений, отделите исходный личный тест от будущей проверки прав. В неактивном чате исправный бот может молчать, а действующий вебхук способен передавать события сломанному исполнителю. Конкретный вопрос о пропавшем событии решается наблюдением на нужных границах, а не общим перезапуском.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/telegram-bot-missing-update-types?locale=ru" rel="noopener noreferrer"&gt;Telegram-бот не получает часть событий: проверка allowed_updates без сброса очереди&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>CRM CSV Exports: Quarantine Suspicious Fields Before Handoff [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Fri, 30 Oct 2026 20:08:41 +0000</pubDate>
      <link>https://dev.to/dev24pro/crm-csv-exports-quarantine-suspicious-fields-before-handoff-cross-post-4l0g</link>
      <guid>https://dev.to/dev24pro/crm-csv-exports-quarantine-suspicious-fields-before-handoff-cross-post-4l0g</guid>
      <description>&lt;h2&gt;
  
  
  Why a CRM CSV export needs its own check
&lt;/h2&gt;

&lt;p&gt;Separate machine transfer from spreadsheet viewing before handing an export to a sales representative. User-entered fields can become formulas in the application that opens the file. Under an agreed policy, hold suspicious rows for review while retaining their original values in protected storage. Putting fields inside quotation marks does not establish that formulas are safe.&lt;/p&gt;

&lt;p&gt;OWASP describes CSV Injection as untrusted input entering a CSV and subsequently being interpreted by spreadsheet software. In particular, a cell beginning with &lt;code&gt;=&lt;/code&gt; may be treated as a formula. CWE-1236 documents the weakness and the limitations of mitigation techniques.[1][2]&lt;/p&gt;

&lt;p&gt;A company name, an enquiry comment, or a field copied from another service can contain untrusted text. Passing through a trusted CRM does not change its origin. Permission to download a report does not establish that every value is safe for the next application.&lt;/p&gt;

&lt;p&gt;This article proposes a policy for an export handler you control. It does not describe a verified built-in 24go.site feature, a universal defense for every spreadsheet, or permission to collect other people's contacts. The example uses synthetic records only. It checks handler decisions, does not launch Excel or Calc, and makes no CRM requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agree on who will open the file
&lt;/h2&gt;

&lt;p&gt;An export needs a named consumer. A machine-transfer file must preserve values according to an explicit data contract. A file intended for human viewing needs application-specific behavior checks. Button labels, format descriptions, and storage routes should make that distinction visible.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Agree on&lt;/th&gt;
&lt;th&gt;Treat as an error&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Machine transfer&lt;/td&gt;
&lt;td&gt;Schema, encoding, delimiter, empty-value handling&lt;/td&gt;
&lt;td&gt;Silently changing a phone number or comment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sales representative's review&lt;/td&gt;
&lt;td&gt;Application, version, import settings, exception policy&lt;/td&gt;
&lt;td&gt;Interpreting untrusted text as a formula&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exception investigation&lt;/td&gt;
&lt;td&gt;Owner, access, retention, viewing method&lt;/td&gt;
&lt;td&gt;Moving the original suspect values into an ordinary CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not label a machine-transfer CSV safe to open by double-clicking. It has a different contract with its recipient. JSON with an explicit schema may be a suitable choice when exact text transfer matters, but JSON itself cannot control what the recipient does next. A later conversion into a spreadsheet creates another boundary that needs checking.&lt;/p&gt;

&lt;p&gt;Start a human-readable report with the smallest useful set of columns. If the recipient needs only an enquiry ID, status, and date, omit the full conversation. Reducing fields reduces the amount of untrusted text and disclosed information; it does not replace validation of the values that remain.&lt;/p&gt;

&lt;p&gt;The handoff sits alongside &lt;a href="https://www.24go.site/blog/integratsiya-telegram-lidov-v-crm?locale=en" rel="noopener noreferrer"&gt;Telegram-to-CRM integration&lt;/a&gt;, but export checking addresses a separate task after a record has been stored. The policy here does not verify the product settings or capabilities mentioned in historical blog articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep CSV serialization separate from formula policy
&lt;/h2&gt;

&lt;p&gt;A CSV library handles field and row boundaries. Your content policy decides which values may be handed to a particular consumer. Run these checks in sequence. Reading a file back successfully with the same library verifies the transfer format, not spreadsheet formula behavior.&lt;/p&gt;

&lt;p&gt;Python's documentation describes &lt;code&gt;csv.writer&lt;/code&gt;, delimiter and quoting options, and opening CSV files with &lt;code&gt;newline=''&lt;/code&gt;. By default, &lt;code&gt;csv.reader&lt;/code&gt; returns strings without automatic type conversion. That is Python parser behavior, not a promise about how a spreadsheet will open the same file.[3]&lt;/p&gt;

&lt;p&gt;Do not construct a row using &lt;code&gt;','.join(...)&lt;/code&gt;. Commas, quotation marks, and line breaks inside a comment must remain inside that field. The example uses &lt;code&gt;csv.writer&lt;/code&gt; to quote every field, then &lt;code&gt;csv.reader&lt;/code&gt; to check the round trip. It applies the suspicious-prefix policy before writing; the &lt;code&gt;QUOTE_ALL&lt;/code&gt; option does not replace that policy.&lt;/p&gt;

&lt;p&gt;OWASP lists risky beginnings including &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;@&lt;/code&gt;, tab, carriage return, line feed, and some full-width character variants. It also warns that delimiters and quotes can create a new cell boundary when handled incorrectly.[1] This gives you test cases, not evidence that a particular detector covers every interpretation.&lt;/p&gt;

&lt;p&gt;Do not remove the first character from the original CRM record. A plus can belong to a phone number, a minus can be meaningful text, and an apostrophe can belong to a name. If you choose a viewing transformation, retain the original separately and document what the recipient will see. Apply the change in the presentation layer and test it in the target application.&lt;/p&gt;

&lt;h2&gt;
  
  
  What whole-row quarantine means
&lt;/h2&gt;

&lt;p&gt;Under this proposed policy, a row containing a suspicious field is withheld from the human-view CSV. A separate exception log receives its ID, column name, and reason code. The original value stays in the system under restricted access. The handler does not automatically repair the value or delete the enquiry.&lt;/p&gt;

&lt;p&gt;This is deliberately conservative. A phone number beginning with a plus can hold up an export if you include it among the text columns checked by this rule. That is an expected false positive of the policy. Phone numbers, amounts, and other typed fields need their own agreed contract, not an undocumented exemption.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Example's decision&lt;/th&gt;
&lt;th&gt;Next action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ordinary text&lt;/td&gt;
&lt;td&gt;Include the row&lt;/td&gt;
&lt;td&gt;Check CSV structure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Formula-like prefix after leading spaces&lt;/td&gt;
&lt;td&gt;Hold the entire row&lt;/td&gt;
&lt;td&gt;Review the field through a protected interface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab or line break anywhere in the field&lt;/td&gt;
&lt;td&gt;Hold the entire row&lt;/td&gt;
&lt;td&gt;Confirm whether multiline text is required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unknown status or invalid schema&lt;/td&gt;
&lt;td&gt;Stop processing with an error&lt;/td&gt;
&lt;td&gt;Repair the contract; do not issue a successful file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Comma and quotes inside ordinary text&lt;/td&gt;
&lt;td&gt;Serialize with the CSV library&lt;/td&gt;
&lt;td&gt;Compare the value after reading it back&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a real workflow, show both the included-row count and the held-row count. The recipient must know that the report is incomplete. Do not call it a complete export while records remain under review. Give the exception owner a separate, access-controlled work list.&lt;/p&gt;

&lt;p&gt;The exception log must not become another route for the same problem. Do not put the original suspect comment in a second CSV or paste the entire value into a notification. The example's JSON log contains only a synthetic ID, field name, and reason. Inspecting originals requires an interface that displays them as text without interpreting markup or formulas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the policy check without a spreadsheet
&lt;/h2&gt;

&lt;p&gt;Save the following code as &lt;code&gt;csv_policy_check.py&lt;/code&gt; and run &lt;code&gt;python3 csv_policy_check.py&lt;/code&gt;. It requires Python 3 and its standard library. It creates records in memory, checks included rows and the exception log, writes no export file, and opens no spreadsheet. The value &lt;code&gt;=1+1&lt;/code&gt; is a harmless string used to exercise the rule; no calculation is performed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="n"&gt;PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=+-@＝＋－＠&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STATUSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;leading_format_character&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PREFIXES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;types&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isascii&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STATUSES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quoting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QUOTE_ALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Call tomorrow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Said &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, call later&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  +44123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;break&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＝1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u200b&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＝1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＋1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;－1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＠name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  =1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid row accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;held&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect included IDs &lt;code&gt;1&lt;/code&gt; and &lt;code&gt;3&lt;/code&gt;; the other synthetic records should appear in the held list with reason codes. The assertions also require rejection of an unknown status, a nonnumeric ID, and &lt;code&gt;None&lt;/code&gt;. A traceback or &lt;code&gt;AssertionError&lt;/code&gt; means the check failed; do not count that run as evidence that the policy works.&lt;/p&gt;

&lt;p&gt;The detector intentionally holds any line break in a note and a leading Unicode format character in category &lt;code&gt;Cf&lt;/code&gt; after whitespace. These are additional restrictions proposed by this example. They are not CSV requirements or a complete model of Unicode behavior in spreadsheets. If you add columns, extend validation to every new untrusted field.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Python check cannot establish
&lt;/h2&gt;

&lt;p&gt;The round trip establishes that the two permitted synthetic records survive this CSV dialect unchanged. It does not establish identical handling across every Excel version, Calc, or another application. OWASP explicitly warns that no sanitization strategy is universally safe for every spreadsheet and downstream consumer.[1]&lt;/p&gt;

&lt;p&gt;Before deployment, record the application, its version, regional settings, and import route. In an isolated environment using synthetic data, check the displayed value, cell type, column boundaries, and the absence of calculation for values intended to remain text. Do not use network formulas, commands, or real personal information for these checks.&lt;/p&gt;

&lt;p&gt;Then save and reopen the file through the same route the recipient will use. OWASP notes that Excel can remove quotes or escape characters during saving and reopening.[1] A first-open check is therefore insufficient for a policy that relies on cell transformations. If the required application is unavailable, that stage remains unverified rather than automatically passing.&lt;/p&gt;

&lt;p&gt;Quarantine also needs an operational check: the recipient sees the exceptions count, the owner can find the original enquiry, and a later export neither loses it nor silently removes its restriction. Changing an enquiry's business status must not automatically declare its comment safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduce the policy without losing enquiries
&lt;/h2&gt;

&lt;p&gt;Start with observation in a test environment. Agree on hold reasons with the process owner and establish that records remain in the original system. Decide whether to enable production blocking based on concrete tests and team requirements, not on the existence of one successful script.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Define the columns and permitted types, including separate rules for empty strings, missing values, and negative numbers.&lt;/li&gt;
&lt;li&gt;Assign a consumer to each export and prevent silent switching between human viewing and machine import.&lt;/li&gt;
&lt;li&gt;Retain inclusion and hold examples covering commas, quotes, spaces, control characters, and full-width characters.&lt;/li&gt;
&lt;li&gt;Check that a schema violation fails the whole job. Do not distribute a partly written file as a successful result.&lt;/li&gt;
&lt;li&gt;Agree on protected exception review and the route for exporting a record after a human decision.&lt;/li&gt;
&lt;li&gt;After changing a library, target application, or column set, rerun the affected checks and retain execution conditions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The in-memory approach in this example may not suit large exports. A production handler needs size limits, temporary-storage controls, access restrictions, and atomic delivery of the completed file. Those are implementation responsibilities, not properties of the sample function. Do not begin a download before checking the conditions under which you promise a complete result.&lt;/p&gt;

&lt;p&gt;Start with one existing report: list its untrusted fields, identify the actual recipient, and agree on what happens when the first row is held. If those answers are missing, the export contract is not ready. Consider &lt;a href="https://www.24go.site" rel="noopener noreferrer"&gt;24go.site&lt;/a&gt; for enquiry workflows separately from this check: this article does not establish that the service implements the export mechanism described here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;p&gt;[1] OWASP, CSV Injection: &lt;a href="https://owasp.org/www-community/attacks/CSV_Injection" rel="noopener noreferrer"&gt;https://owasp.org/www-community/attacks/CSV_Injection&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[2] MITRE, CWE-1236: &lt;a href="https://cwe.mitre.org/data/definitions/1236.html" rel="noopener noreferrer"&gt;https://cwe.mitre.org/data/definitions/1236.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[3] Python, csv: &lt;a href="https://docs.python.org/3/library/csv.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/csv.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/crm-csv-export-formula-quarantine?locale=en" rel="noopener noreferrer"&gt;CRM CSV Exports: Quarantine Suspicious Fields Before Handoff&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crm</category>
      <category>безопасность</category>
      <category>экспорт</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>CSV из CRM: как отправлять подозрительные поля на проверку [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Fri, 30 Oct 2026 20:08:39 +0000</pubDate>
      <link>https://dev.to/dev24pro/csv-iz-crm-kak-otpravliat-podozritielnyie-polia-na-provierku-pieriekriostnaia-publikatsiia-mcc</link>
      <guid>https://dev.to/dev24pro/csv-iz-crm-kak-otpravliat-podozritielnyie-polia-na-provierku-pieriekriostnaia-publikatsiia-mcc</guid>
      <description>&lt;h2&gt;
  
  
  Почему CSV из CRM требует отдельной проверки
&lt;/h2&gt;

&lt;p&gt;Перед передачей выгрузки менеджеру разделите машинный обмен и просмотр в электронной таблице. Поля, введённые пользователем, могут оказаться формулами для программы, которая откроет файл. Подозрительные строки лучше задержать для разбора по согласованной политике, сохранив исходные значения в защищённом хранилище. Заключение полей в кавычки само по себе не делает формулы безопасными.&lt;/p&gt;

&lt;p&gt;OWASP описывает CSV Injection как попадание недоверенных данных в CSV с последующей интерпретацией содержимого электронной таблицей. В частности, ячейка, начинающаяся с &lt;code&gt;=&lt;/code&gt;, может быть прочитана как формула. CWE-1236 отдельно описывает эту слабость и ограничения способов её устранения.[1][2]&lt;/p&gt;

&lt;p&gt;Недоверенный текст может оказаться в названии компании, комментарии к обращению или поле, перенесённом из другого сервиса. Данные проходят через доверенную CRM, но происхождение текста от этого не меняется. Право скачать файл тоже не означает, что все его значения безопасны для следующей программы.&lt;/p&gt;

&lt;p&gt;Ниже предложена политика для собственного обработчика экспорта. Это не описание проверенной встроенной функции 24go.site, не универсальная защита всех таблиц и не разрешение собирать чужие контакты. Пример работает только с искусственными записями. Он проверяет решения обработчика, не запускает Excel или Calc и не обращается к CRM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Сначала договоритесь, кто откроет файл
&lt;/h2&gt;

&lt;p&gt;У выгрузки должен быть конкретный потребитель. Файл для переноса между системами требует сохранения значений по контракту. Файл для просмотра человеком требует проверки поведения выбранной программы. Названия кнопок, описание формата и маршрут хранения должны отражать эту разницу.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Назначение&lt;/th&gt;
&lt;th&gt;Что согласовать&lt;/th&gt;
&lt;th&gt;Что считать ошибкой&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Машинный перенос&lt;/td&gt;
&lt;td&gt;Схему, кодировку, разделитель, обработку пустых значений&lt;/td&gt;
&lt;td&gt;Незаметное изменение телефона или комментария&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Просмотр менеджером&lt;/td&gt;
&lt;td&gt;Программу, версию, настройки импорта, правила исключений&lt;/td&gt;
&lt;td&gt;Открытие недоверенного текста как формулы&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Разбор исключений&lt;/td&gt;
&lt;td&gt;Ответственного, доступ, срок хранения, способ просмотра&lt;/td&gt;
&lt;td&gt;Перенос исходных подозрительных значений в обычный CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Не называйте машинный CSV безопасным для открытия двойным щелчком. У него другой договор с получателем. Если требуется точная передача текста, можно выбрать JSON с явно описанной схемой, но сам выбор JSON не управляет тем, что получатель сделает с данными дальше. После преобразования в таблицу проверка понадобится на новой границе.&lt;/p&gt;

&lt;p&gt;Для человеческого отчёта начните с минимального набора колонок. Если менеджеру достаточно номера заявки, статуса и даты, не добавляйте полный текст переписки. Уменьшение состава полей сокращает объём недоверенного текста и раскрываемых сведений; оно не заменяет проверку оставшихся значений.&lt;/p&gt;

&lt;p&gt;Процесс передачи заявок связан с &lt;a href="https://www.24go.site/blog/integratsiya-telegram-lidov-v-crm?locale=ru" rel="noopener noreferrer"&gt;интеграцией Telegram и CRM&lt;/a&gt;, но проверка экспорта решает отдельную задачу после сохранения записи. Приведённая здесь политика не подтверждает настройки и функции продукта, перечисленные в исторических материалах блога.&lt;/p&gt;

&lt;h2&gt;
  
  
  Разделите сериализацию и политику формул
&lt;/h2&gt;

&lt;p&gt;CSV-библиотека отвечает за границы полей и строк. Проверка содержимого отвечает за то, какие данные вы разрешаете передавать конкретному потребителю. Эти проверки выполняются последовательно, и успешное чтение файла обратно библиотекой подтверждает только формат обмена.&lt;/p&gt;

&lt;p&gt;Документация Python описывает &lt;code&gt;csv.writer&lt;/code&gt;, параметры разделителя и кавычек, а также открытие файлов с &lt;code&gt;newline=''&lt;/code&gt;. По умолчанию &lt;code&gt;csv.reader&lt;/code&gt; возвращает строки без автоматического преобразования типов. Это поведение парсера Python, не обещание о том, как тот же файл откроет электронная таблица.[3]&lt;/p&gt;

&lt;p&gt;Не собирайте строку через &lt;code&gt;','.join(...)&lt;/code&gt;. Запятая, кавычка или перенос строки в комментарии должны остаться частью своего поля. В примере используется &lt;code&gt;csv.writer&lt;/code&gt; с кавычками для всех полей, затем &lt;code&gt;csv.reader&lt;/code&gt; проверяет обратное чтение. Политика подозрительных префиксов применяется до записи, а не подменяется параметром &lt;code&gt;QUOTE_ALL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;OWASP перечисляет среди рискованных начал &lt;code&gt;=&lt;/code&gt;, &lt;code&gt;+&lt;/code&gt;, &lt;code&gt;-&lt;/code&gt;, &lt;code&gt;@&lt;/code&gt;, табуляцию, возврат каретки, перевод строки и некоторые полноширинные варианты символов. Там же указано, что разделители и кавычки могут изменить границу ячейки при неправильной обработке.[1] Список помогает составить тесты, но не доказывает полноту конкретного детектора.&lt;/p&gt;

&lt;p&gt;Не удаляйте первый знак из исходной CRM-записи. Плюс может быть частью номера телефона, минус — осмысленного текста, а апостроф — имени. Если выбран способ преобразования для просмотра, храните исходник отдельно и документируйте, какое значение увидит человек. Изменение должно происходить в представлении, с проверкой на целевом приложении.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что означает карантин всей строки
&lt;/h2&gt;

&lt;p&gt;В предложенной политике строка с подозрительным полем не попадает в человеческий CSV. Вместо неё в отдельный журнал исключений записываются идентификатор, название колонки и код причины. Исходное поле остаётся в системе с ограниченным доступом. Обработчик не пытается автоматически исправить значение и не удаляет заявку.&lt;/p&gt;

&lt;p&gt;Такой выбор консервативен. Телефон с плюсом тоже может задержать экспорт, если телефон включён в проверяемые текстовые колонки. Это ожидаемое ложное срабатывание данной политики. Для телефонов, сумм и других типов нужен отдельный согласованный контракт, а не молчаливое исключение из общего правила.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Ситуация&lt;/th&gt;
&lt;th&gt;Решение примера&lt;/th&gt;
&lt;th&gt;Дальнейшее действие&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Обычный текст&lt;/td&gt;
&lt;td&gt;Разрешить строку&lt;/td&gt;
&lt;td&gt;Проверить структуру CSV&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Формульный префикс после начальных пробелов&lt;/td&gt;
&lt;td&gt;Задержать всю строку&lt;/td&gt;
&lt;td&gt;Разобрать поле в защищённом интерфейсе&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Табуляция или перенос строки в любом месте&lt;/td&gt;
&lt;td&gt;Задержать всю строку&lt;/td&gt;
&lt;td&gt;Подтвердить необходимость многострочного текста&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Неизвестный статус или неверная схема&lt;/td&gt;
&lt;td&gt;Остановить обработку с ошибкой&lt;/td&gt;
&lt;td&gt;Исправить контракт, не выдавать успешный файл&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Запятая и кавычки внутри обычного текста&lt;/td&gt;
&lt;td&gt;Сохранить через CSV-библиотеку&lt;/td&gt;
&lt;td&gt;Сверить значение после обратного чтения&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;В реальном процессе полезно показывать два числа: сколько строк включено и сколько задержано. Получатель должен знать, что отчёт неполон. Не называйте файл полной выгрузкой, если часть строк осталась на проверке. Для ответственного за исключения нужен отдельный список работ с контролем доступа.&lt;/p&gt;

&lt;p&gt;Сам журнал не должен становиться вторым каналом проблемы. Не складывайте в его CSV исходный подозрительный комментарий и не отправляйте весь текст в уведомление. В примере JSON-журнал содержит только искусственный ID, имя поля и причину. Просмотр исходных данных потребует интерфейса, который выводит их как текст и не интерпретирует как разметку или формулы.&lt;/p&gt;

&lt;h2&gt;
  
  
  Воспроизводимый тест без электронной таблицы
&lt;/h2&gt;

&lt;p&gt;Сохраните код как &lt;code&gt;csv_policy_check.py&lt;/code&gt; и выполните &lt;code&gt;python3 csv_policy_check.py&lt;/code&gt;. Нужен Python 3 со стандартной библиотекой. Программа создаёт данные только в памяти, проверяет разрешённые строки и журнал исключений. Она не пишет выгрузку на диск и не открывает таблицы. Значение &lt;code&gt;=1+1&lt;/code&gt; служит безопасной строкой для теста правила, вычисления не происходит.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;

&lt;span class="n"&gt;PREFIXES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=+-@＝＋－＠&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;STATUSES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ch&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t\r\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;unicodedata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;leading_format_character&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PREFIXES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;accepted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;types&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isascii&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;isdigit&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STATUSES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;issue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictWriter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fieldnames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;FIELDS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quoting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QUOTE_ALL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeheader&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accepted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;restored&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;accepted&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;

&lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Call tomorrow&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;qualified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Said &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, call later&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  +44123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;line&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;break&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＝1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\u200b&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;7&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＝1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＋1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;－1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;＠name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  =1+1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;formula_prefix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\t&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="se"&gt;\r&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;control_separator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plain&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;new&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;note&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;export_for_review&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid row accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_ids&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;held&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;held&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаются разрешённые ID &lt;code&gt;1&lt;/code&gt; и &lt;code&gt;3&lt;/code&gt;; остальные тестовые записи попадут в журнал с причинами. Проверки также требуют отказа при неизвестном статусе, нецифровом ID и значении &lt;code&gt;None&lt;/code&gt;. Ошибка даёт traceback или &lt;code&gt;AssertionError&lt;/code&gt;; такой запуск нельзя считать подтверждением политики.&lt;/p&gt;

&lt;p&gt;Детектор сознательно задерживает любой перенос строки в комментарии и начальный форматирующий символ категории &lt;code&gt;Cf&lt;/code&gt; после пробелов. Это собственные дополнительные ограничения примера. Они не объявлены требованиями CSV или полным описанием поведения Unicode в таблицах. После изменения набора колонок проверку надо расширить на каждое новое недоверенное поле.&lt;/p&gt;

&lt;h2&gt;
  
  
  Какие проверки остаются за пределами Python
&lt;/h2&gt;

&lt;p&gt;Обратное чтение доказывает сохранение двух разрешённых тестовых записей в данном диалекте CSV. Оно не доказывает, что все версии Excel, Calc и другие программы одинаково обработают файл. OWASP прямо предупреждает об отсутствии универсальной стратегии для всех приложений и последующих потребителей.[1]&lt;/p&gt;

&lt;p&gt;Перед внедрением зафиксируйте программу, её версию, региональные настройки и способ импорта. В изолированной среде на искусственных данных проверьте видимое значение ячейки, её тип, границы колонок и отсутствие вычисления у текста, который должен остаться текстом. Не используйте сетевые формулы, команды или реальные персональные данные для такой проверки.&lt;/p&gt;

&lt;p&gt;Затем сохраните и повторно откройте файл тем же маршрутом, которым пойдёт менеджер. OWASP отмечает, что Excel может убирать кавычки или экранирующие символы при сохранении и повторном открытии.[1] Поэтому проверка первого открытия недостаточна для политики, которая полагается на преобразование ячеек. Если необходимого приложения нет, этот этап остаётся непроверенным, а не автоматически успешным.&lt;/p&gt;

&lt;p&gt;Карантин также требует эксплуатационной проверки: получатель видит число исключений, ответственный может найти исходную заявку, повторный экспорт не теряет её и не снимает ограничение самопроизвольно. Изменение статуса заявки не должно означать автоматическое признание комментария безопасным.&lt;/p&gt;

&lt;h2&gt;
  
  
  Как внедрять без потери заявок
&lt;/h2&gt;

&lt;p&gt;Сначала включите наблюдение в тестовом контуре, согласуйте причины задержки с владельцем процесса и убедитесь, что записи не исчезают из исходной системы. Решение о включении производственной блокировки принимается по конкретным тестам и требованиям команды, не по наличию одного успешного скрипта.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Опишите колонки и разрешённые типы; отдельно укажите правила для пустой строки, отсутствующего значения и отрицательных чисел.&lt;/li&gt;
&lt;li&gt;Назначьте каждой выгрузке потребителя и запретите незаметно менять режим просмотра на машинный импорт.&lt;/li&gt;
&lt;li&gt;Сохраните примеры разрешения и задержки, включая запятые, кавычки, пробелы, управляющие и полноширинные символы.&lt;/li&gt;
&lt;li&gt;Проверьте отказ всего задания при нарушении схемы. Не отправляйте частично записанный файл как успешный результат.&lt;/li&gt;
&lt;li&gt;Согласуйте защищённый разбор исключений и способ повторного экспорта после решения человека.&lt;/li&gt;
&lt;li&gt;После обновления библиотеки, приложения или колонок повторите затронутые проверки и сохраните условия запуска.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Для больших выгрузок запись в память из примера может не подходить. Производственный обработчик должен учитывать ограничения размера, временное хранилище, права доступа и атомарную выдачу готового файла. Это задачи реализации, не свойства приведённой функции. Не начинайте скачивание пользователю до проверки условий, при которых вы обещаете полный результат.&lt;/p&gt;

&lt;p&gt;Начните с одного своего отчёта: перечислите недоверенные поля, найдите его фактического получателя и согласуйте поведение при первой задержанной строке. Если это пока неизвестно, экспортный договор ещё не готов. Обсуждать использование &lt;a href="https://www.24go.site" rel="noopener noreferrer"&gt;24go.site&lt;/a&gt; для работы с обращениями стоит отдельно от этой проверки: статья не подтверждает наличие в сервисе описанного экспортного механизма.&lt;/p&gt;

&lt;h2&gt;
  
  
  Источники
&lt;/h2&gt;

&lt;p&gt;[1] OWASP, CSV Injection: &lt;a href="https://owasp.org/www-community/attacks/CSV_Injection" rel="noopener noreferrer"&gt;https://owasp.org/www-community/attacks/CSV_Injection&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[2] MITRE, CWE-1236: &lt;a href="https://cwe.mitre.org/data/definitions/1236.html" rel="noopener noreferrer"&gt;https://cwe.mitre.org/data/definitions/1236.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;[3] Python, csv: &lt;a href="https://docs.python.org/3/library/csv.html" rel="noopener noreferrer"&gt;https://docs.python.org/3/library/csv.html&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/crm-csv-export-formula-quarantine?locale=ru" rel="noopener noreferrer"&gt;CSV из CRM: как отправлять подозрительные поля на проверку&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crm</category>
      <category>безопасность</category>
      <category>экспорт</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram Group Migration: Keep CRM Routing When the Chat ID Changes [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:36:09 +0000</pubDate>
      <link>https://dev.to/dev24pro/telegram-group-migration-keep-crm-routing-when-the-chat-id-changes-cross-post-16kb</link>
      <guid>https://dev.to/dev24pro/telegram-group-migration-keep-crm-routing-when-the-chat-id-changes-cross-post-16kb</guid>
      <description>&lt;h1&gt;
  
  
  Telegram Group Migration: Keep CRM Routing When the Chat ID Changes
&lt;/h1&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the migration fields in the right direction
&lt;/h2&gt;

&lt;p&gt;The official &lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Message reference&lt;/a&gt; defines two optional fields. They describe the same relationship from different sides:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Received evidence&lt;/th&gt;
&lt;th&gt;Old group ID&lt;/th&gt;
&lt;th&gt;New supergroup ID&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A message with &lt;code&gt;migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;That message's &lt;code&gt;chat.id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Its &lt;code&gt;migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A message with &lt;code&gt;migrate_from_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Its &lt;code&gt;migrate_from_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;That message's &lt;code&gt;chat.id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed request with &lt;code&gt;parameters.migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;chat_id&lt;/code&gt; used in that exact request&lt;/td&gt;
&lt;td&gt;The parameter's value&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third row comes from &lt;a href="https://core.telegram.org/bots/api#responseparameters" rel="noopener noreferrer"&gt;ResponseParameters&lt;/a&gt;, 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate the conversation from its transport address
&lt;/h2&gt;

&lt;p&gt;A fragile schema uses &lt;code&gt;chat_id&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;Use separate concepts in your own data model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;conversation_id&lt;/code&gt;: your stable internal identity for the working conversation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chat_alias&lt;/code&gt;: a Telegram identifier known to refer to that conversation, scoped to your tenant and bot.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;active_chat_id&lt;/code&gt;: the current destination proposed for new operations, subject to current permissions and application policy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;received_chat_id&lt;/code&gt;: the actual ID recorded on a historical event; keep this value unchanged.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;For the wider integration problem, see &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;handling repeated Telegram webhooks&lt;/a&gt;. Event deduplication remains necessary, but it solves a different problem: a migration can arrive as a new update and still concern the existing conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make migration one atomic local decision
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Extract the old and new IDs using the table above. Reject malformed values and self-mappings. Retain which field supplied the relationship.&lt;/li&gt;
&lt;li&gt;In a database transaction, lock or otherwise serialize the scoped alias records involved. Read their current conversation ownership again inside that transaction.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;li&gt;Commit the mapping and any local follow-up job together. Only then acknowledge or finish the corresponding processing step according to your intake contract.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check direction, repeats and conflicts locally
&lt;/h2&gt;

&lt;p&gt;Save this example as &lt;code&gt;migration_check.py&lt;/code&gt; and run &lt;code&gt;python3 migration_check.py&lt;/code&gt;. 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 &lt;code&gt;scope&lt;/code&gt; tuple represents one tenant and one bot. The two message dictionaries are deliberately simplified input fixtures, not recordings from a real group.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no migration field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid pair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old alias needs reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;


&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1009876543210&lt;/span&gt;
&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;aliases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conflict accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other-tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old alias needs reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope crossed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;direction=ok repeat=ok conflict=blocked scope=isolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected final line is &lt;code&gt;direction=ok repeat=ok conflict=blocked scope=isolated&lt;/code&gt;. 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resolve queued work without erasing its history
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;migrate_from_chat_id&lt;/code&gt; 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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose the failure before editing CRM records
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Check next&lt;/th&gt;
&lt;th&gt;Avoid&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Structured migration target in a failed response&lt;/td&gt;
&lt;td&gt;Match it to the stored request and scoped conversation&lt;/td&gt;
&lt;td&gt;Parsing any error text as a new ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New-group traffic creates a second conversation&lt;/td&gt;
&lt;td&gt;Handler ordering and alias lookup&lt;/td&gt;
&lt;td&gt;Deleting the new record before checking ownership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database has the new ID, worker sends to the old one&lt;/td&gt;
&lt;td&gt;Queue resolution and cache invalidation&lt;/td&gt;
&lt;td&gt;Replaying every queued operation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New alias belongs to another conversation&lt;/td&gt;
&lt;td&gt;Tenant/bot scope and previous onboarding&lt;/td&gt;
&lt;td&gt;Automatic conversation merge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New destination still rejects an operation&lt;/td&gt;
&lt;td&gt;Current permissions and the actual error&lt;/td&gt;
&lt;td&gt;Treating migration as a blanket access repair&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the real integration before resuming work
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/telegram-group-migration-crm-chat-id?locale=en" rel="noopener noreferrer"&gt;Telegram Group Migration: Keep CRM Routing When the Chat ID Changes&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Миграция группы Telegram: как сохранить привязку к CRM при смене chat_id [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Wed, 28 Oct 2026 02:36:08 +0000</pubDate>
      <link>https://dev.to/dev24pro/mighratsiia-ghruppy-telegram-kak-sokhranit-priviazku-k-crm-pri-smienie-chatid-pieriekriostnaia-publikatsiia-41g6</link>
      <guid>https://dev.to/dev24pro/mighratsiia-ghruppy-telegram-kak-sokhranit-priviazku-k-crm-pri-smienie-chatid-pieriekriostnaia-publikatsiia-41g6</guid>
      <description>&lt;h1&gt;
  
  
  Миграция группы Telegram: как сохранить привязку к CRM при смене chat_id
&lt;/h1&gt;

&lt;p&gt;После преобразования группы Telegram в супергруппу не считайте новый chat_id новой беседой с клиентом. Зафиксируйте связь старого и нового идентификаторов, сохраните внутренний идентификатор беседы, а перед разрешённой отправкой определяйте её текущий адрес. Если два chat_id уже принадлежат разным беседам, остановите автоматическую обработку и разберите конфликт вместо того, чтобы незаметно объединять карточки.&lt;/p&gt;

&lt;p&gt;Это руководство предназначено разработчику собственной интеграции Telegram Bot API с CRM. Предполагаются бот, законно добавленный в рабочую группу, проверка входящих запросов, постоянное хранилище и тестовое окружение. Описанный подход не является подтверждённой встроенной функцией 24go.site. Для него не нужны сбор участников или нежелательные рассылки. Группа не равна отдельному контакту, а сохранение её истории не даёт нового согласия на обращения к участникам.&lt;/p&gt;

&lt;h2&gt;
  
  
  В какую сторону читать поля миграции
&lt;/h2&gt;

&lt;p&gt;В официальном описании &lt;a href="https://core.telegram.org/bots/api#message" rel="noopener noreferrer"&gt;Message&lt;/a&gt; указаны два необязательных поля. Они показывают одну связь с разных сторон:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Полученное свидетельство&lt;/th&gt;
&lt;th&gt;Старый ID группы&lt;/th&gt;
&lt;th&gt;Новый ID супергруппы&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Сообщение с &lt;code&gt;migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chat.id&lt;/code&gt; этого сообщения&lt;/td&gt;
&lt;td&gt;Значение &lt;code&gt;migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Сообщение с &lt;code&gt;migrate_from_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Значение &lt;code&gt;migrate_from_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chat.id&lt;/code&gt; этого сообщения&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Неуспешный запрос с &lt;code&gt;parameters.migrate_to_chat_id&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;chat_id&lt;/code&gt;, использованный именно в этом запросе&lt;/td&gt;
&lt;td&gt;Значение параметра&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Третья строка относится к &lt;a href="https://core.telegram.org/bots/api#responseparameters" rel="noopener noreferrer"&gt;ResponseParameters&lt;/a&gt;, а не к телу служебного сообщения. Сохраняйте контекст запроса: без исходного адреса ответ с ошибкой не определяет оба конца связи. Не извлекайте старый идентификатор из переведённого описания ошибки. Ветвитесь по документированному структурному полю, а не по конкретной английской фразе или любому HTTP 400.&lt;/p&gt;

&lt;p&gt;Поля необязательны. Обработчик должен выдерживать ситуацию, когда обнаружено только одно направление, эквивалентные сведения пришли повторно или сведения о миграции получены уже после обычного события из новой группы. Не требуйте, чтобы два служебных сообщения пришли вместе или в предполагаемом порядке. Это защитное правило обработки, а не утверждение, что каждая установка обязательно встретит все перечисленные ситуации.&lt;/p&gt;

&lt;p&gt;Храните идентификаторы без сужения до 32-битного целого. Telegram указывает, что у migration ID может быть больше 32 значащих бит, но не больше 52. Знаковое 64-битное целое в БД - прямой вариант хранения. Если пользовательское поле CRM не сохраняет такое целое точно, используйте на этой границе проверенную десятичную строку и проверьте обратное преобразование. Не вычисляйте новый ID, добавляя префикс к старому: используйте значение, переданное Telegram.&lt;/p&gt;

&lt;h2&gt;
  
  
  Отделите беседу от транспортного адреса
&lt;/h2&gt;

&lt;p&gt;Хрупкая схема использует chat_id одновременно как ключ беседы в CRM, адрес уведомлений и историческую принадлежность каждого события. Простая замена тогда либо стирает происхождение старых данных, либо превращает новую группу в новый лид.&lt;/p&gt;

&lt;p&gt;В своей модели разделите четыре понятия:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;conversation_id&lt;/code&gt; - стабильный внутренний идентификатор рабочей беседы.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chat_alias&lt;/code&gt; - идентификатор Telegram, который относится к этой беседе в пределах вашего клиента системы и бота.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;active_chat_id&lt;/code&gt; - текущий адрес для новых операций; его применимость всё равно зависит от актуальных прав и правил приложения.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;received_chat_id&lt;/code&gt; - фактический ID, записанный в историческом событии; его не переписывают.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Область изоляции задаёт ваша интеграция, а не формат Telegram ID. Включайте tenant и bot в ключи поиска, чтобы одна установка не могла случайно привязать свидетельство к беседе другого клиента системы. Уникальное ограничение по области и alias должно запрещать принадлежность одного alias двум внутренним беседам. При нескольких обработчиках нужны транзакционные проверки: словарь в памяти их не заменяет.&lt;/p&gt;

&lt;p&gt;Alias сохраняет непрерывность маршрутизации. Он не доказывает, что два человека в CRM - один человек, что все сделки нужно объединить или что идентификатор сообщения старого чата действителен в новом. Сохраняйте ссылки на сообщения как исходную пару «чат-сообщение». Поведение старых ссылок и ответов проверяйте отдельно, а не переписывайте message_id только на основании миграции чата.&lt;/p&gt;

&lt;p&gt;Общую задачу повторной доставки разбирает материал &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;о повторных вебхуках Telegram&lt;/a&gt;. Дедупликация событий по-прежнему нужна, но решает другой вопрос: миграция может прийти как новый update и при этом относиться к существующей беседе.&lt;/p&gt;

&lt;h2&gt;
  
  
  Принимайте решение о миграции атомарно
&lt;/h2&gt;

&lt;p&gt;Ветку миграции обрабатывайте раньше универсального правила «неизвестный чат - создать беседу в CRM». Иначе служебное сообщение из супергруппы может создать ненужную карточку до того, как обработчик проверит поля миграции.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Проверьте обновление на обычной доверенной границе приёма. Сохраните входящее свидетельство и ключ исходного события. Текст пользователя с каким-либо ID не равен структурному полю миграции Telegram.&lt;/li&gt;
&lt;li&gt;Извлеките старый и новый ID по таблице. Отклоните некорректные значения и случай, когда старый и новый ID одинаковы. Сохраните, какое поле подтвердило связь.&lt;/li&gt;
&lt;li&gt;В транзакции БД заблокируйте затронутые scoped alias или иным способом сериализуйте работу с ними. Заново прочитайте принадлежность обоих alias внутри транзакции.&lt;/li&gt;
&lt;li&gt;Если у старого alias одна беседа, а новый свободен, привяжите новый alias к существующей беседе. Запишите связь миграции и обновите активный адрес беседы.&lt;/li&gt;
&lt;li&gt;Если оба alias уже принадлежат той же беседе, считайте операцию повтором. Сохраните свидетельство, но не создавайте новый объект CRM или новое задание уведомления.&lt;/li&gt;
&lt;li&gt;Если alias принадлежат разным беседам, отправьте операцию на сверку. Не выбирайте более свежую строку, не перезаписывайте внешний ключ и не удаляйте одну беседу автоматически.&lt;/li&gt;
&lt;li&gt;Зафиксируйте привязку и локальное последующее задание вместе. Только после этого подтверждайте соответствующий шаг обработки согласно контракту вашего приёмника.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Если неизвестны оба alias, сохраните неразрешённое свидетельство либо примените отдельное правило подключения, согласованное для этой группы. Не угадывайте владельца среди существующих карточек по названию группы. Если известен только новый alias, старый можно привязать после тех же проверок области и свидетельства; обратный поиск не разрешает перезаписывать конфликт.&lt;/p&gt;

&lt;p&gt;Транзакция вашей БД не охватывает удалённую запись в CRM. Если CRM тоже хранит текущий адрес, обновляйте его отдельной отслеживаемой операцией и сохраняйте её результат. После неоднозначного таймаута сначала установите фактический результат, а не считайте запись неудачной. Таблица alias должна оставаться источником маршрутизации, пока внешняя копия догоняет её по заранее определённому правилу.&lt;/p&gt;

&lt;h2&gt;
  
  
  Локально проверьте направление, повтор и конфликт
&lt;/h2&gt;

&lt;p&gt;Сохраните пример в &lt;code&gt;migration_check.py&lt;/code&gt; и выполните &lt;code&gt;python3 migration_check.py&lt;/code&gt;. Нужна только стандартная библиотека Python; используются искусственные ID и память процесса. Пример не обращается к Telegram или CRM и не создаёт базу данных. Кортеж scope обозначает одного клиента системы и одного бота. Два словаря сообщений - намеренно упрощённые входные данные, а не записи событий реальной группы.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;chat&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no migration field&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invalid pair&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old alias needs reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;after&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;


&lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tenant-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1009876543210&lt;/span&gt;
&lt;span class="n"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_to_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;migrate_from_chat_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;aliases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nf"&gt;pair&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conflict accepted&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1001111111111&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation-B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aliases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;other-tenant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bot-demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;old alias needs reconciliation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;AssertionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;scope crossed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;direction=ok repeat=ok conflict=blocked scope=isolated&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ожидаемая последняя строка: &lt;code&gt;direction=ok repeat=ok conflict=blocked scope=isolated&lt;/code&gt;. Ошибка assertion означает, что проверяемое правило не выдержано; не удаляйте проверку ради нужной строки. Пример намеренно останавливается при неизвестном старом alias. Он не реализует необязательные ветки обратной привязки и первичного подключения, описанные выше.&lt;/p&gt;

&lt;p&gt;Такая проверка устанавливает направление полей и небольшое последовательное правило. Она не проверяет полную схему Telegram update, допустимый диапазон целых чисел, доверенный приём, конкурентные транзакции, восстановление после сбоя, смену активного адреса или внешнюю копию в CRM. Эти проверки добавляют на тех уровнях, где соответствующее поведение действительно реализовано. Небольшой пример позволяет проверить результат, не выдавая учебный словарь за промышленную интеграцию.&lt;/p&gt;

&lt;h2&gt;
  
  
  Обновляйте маршрут задания, а не его прошлое
&lt;/h2&gt;

&lt;p&gt;В очереди могут остаться задания со старым chat_id. Удаление таких заданий теряет работу, а массовая замена исторических payload скрывает исходные обстоятельства. Предпочтительно хранить в задании стабильную ссылку на беседу и определять текущий адрес при исполнении. В квитанции операции записывайте и первоначально запрошенный адрес, и фактически выбранный.&lt;/p&gt;

&lt;p&gt;До отправки проверьте, что у бота остался нужный доступ, а сама операция по-прежнему уместна в этой беседе. Alias миграции не устанавливает актуальное членство или разрешение на публикацию. Не повторяйте автоматически операцию с неизвестным предыдущим результатом доставки: определение адреса не отвечает на вопрос, было ли сообщение уже отправлено.&lt;/p&gt;

&lt;p&gt;Если система проходит цепочки alias, ограничьте число переходов и обнаруживайте циклы. Повтор связи старого ID с новым не должен создавать обратное ребро только потому, что встретилось &lt;code&gt;migrate_from_chat_id&lt;/code&gt;. Оба вида служебного сообщения задают направление от старого к новому. Если цепочка зацикливается или противоречит принадлежности беседы, остановите задание и сохраните свидетельства для оператора. Ограничение числа переходов - ваше защитное правило, а не заявленный лимит миграций Telegram.&lt;/p&gt;

&lt;p&gt;Для закешированных адресов нужен ясный способ сброса. Иначе данные в БД могут быть верными, а обработчик продолжит обращаться по старому адресу. В тестовом окружении оставьте одному обработчику старое значение в кеше и проверьте, как версия миграции или истечение срока кеша заставляет перечитать запись. Успешный поиск в новом процессе не доказывает, что обновился старый кеш.&lt;/p&gt;

&lt;h2&gt;
  
  
  Прежде чем править CRM, определите причину
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Наблюдение&lt;/th&gt;
&lt;th&gt;Следующая проверка&lt;/th&gt;
&lt;th&gt;Чего избегать&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;В ошибке есть структурный migration target&lt;/td&gt;
&lt;td&gt;Связать его с сохранённым запросом и scoped беседой&lt;/td&gt;
&lt;td&gt;Получать новый ID из любого текста ошибки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Трафик новой группы создаёт вторую беседу&lt;/td&gt;
&lt;td&gt;Порядок веток обработчика и поиск alias&lt;/td&gt;
&lt;td&gt;Удалять новую карточку до проверки принадлежности&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;В БД новый ID, обработчик отправляет на старый&lt;/td&gt;
&lt;td&gt;Выбор адреса в очереди и сброс кеша&lt;/td&gt;
&lt;td&gt;Повторять все задания подряд&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Новый alias занят другой беседой&lt;/td&gt;
&lt;td&gt;Область tenant/bot и предыдущее подключение&lt;/td&gt;
&lt;td&gt;Автоматически объединять беседы&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Новый адрес всё равно отклоняет действие&lt;/td&gt;
&lt;td&gt;Актуальные права и фактическая ошибка&lt;/td&gt;
&lt;td&gt;Считать миграцию универсальным восстановлением доступа&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Оператору нужны исходное свидетельство, оба ID, принадлежность бесед и принятое решение, а не доступ ко всей переписке. Записывайте только необходимые для диагностики метаданные согласно своей политике хранения. Не включайте токены бота и лишние персональные данные в технические квитанции.&lt;/p&gt;

&lt;h2&gt;
  
  
  Проверьте настоящую интеграцию до возобновления работы
&lt;/h2&gt;

&lt;p&gt;Сохраните резервную копию затронутых записей маршрутизации и тестируйте в разрешённой группе. Повторите одно и то же свидетельство миграции последовательно и одновременно: решение о принадлежности должно быть одно, без дублирующего локального задания. Остановите обработчик после фиксации транзакции, перезапустите и убедитесь, что незавершённая синхронизация может продолжиться. Создайте явный конфликт владельцев и проверьте, что ни одна беседа не переназначена молча.&lt;/p&gt;

&lt;p&gt;Затем проверьте старое задание из очереди. Его исходный chat_id должен остаться неизменным в истории, текущий адрес должен определяться по принятой связи, а предыдущее состояние доставки должно по-прежнему влиять на решение о запуске. Отдельно проверьте внешнюю копию адреса в CRM, а не только таблицу alias. Наконец, выполните обычное разрешённое взаимодействие в текущей группе и запишите фактический результат: строка локального теста не подтверждает доставку.&lt;/p&gt;

&lt;p&gt;Внедрение готово только после того, как непрерывность, изоляция и обработка ошибок проверены именно в вашем стеке. Если надёжное свидетельство не связывает старый ID с новым, оставьте привязку неразрешённой. Приостановить один маршрут с понятной причиной безопаснее, чем привязать рабочую группу к чужой карточке клиента.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/telegram-group-migration-crm-chat-id?locale=ru" rel="noopener noreferrer"&gt;Миграция группы Telegram: как сохранить привязку к CRM при смене chat_id&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Telegram Bot Webhooks: Validate secret_token Before Queuing CRM Work [cross-post]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:12:41 +0000</pubDate>
      <link>https://dev.to/dev24pro/telegram-bot-webhooks-validate-secrettoken-before-queuing-crm-work-cross-post-3n78</link>
      <guid>https://dev.to/dev24pro/telegram-bot-webhooks-validate-secrettoken-before-queuing-crm-work-cross-post-3n78</guid>
      <description>&lt;h1&gt;
  
  
  Telegram Bot Webhooks: Validate secret_token Before Queuing CRM Work
&lt;/h1&gt;

&lt;p&gt;Check &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; before interpreting a business event or writing a job to a queue. Your system chooses the expected value and supplies it to Telegram as &lt;code&gt;secret_token&lt;/code&gt; when registering the webhook. A request without an accepted value must not start qualification, create a lead, or change a deal. A matching secret, however, neither establishes the user's permissions nor prevents repeated delivery.&lt;/p&gt;

&lt;p&gt;This guide covers the ingress boundary of your own Telegram Bot API → receiver → queue → CRM integration. You need access to the receiver's code, configuration, and a test environment. It does not describe a verified built-in feature of 24go.site. The example uses invented public test values, runs locally, and neither contacts Telegram nor reads real secrets. It tests a receiver decision, not the security of an entire deployed service.&lt;/p&gt;

&lt;h2&gt;
  
  
  What secret_token actually checks
&lt;/h2&gt;

&lt;p&gt;Telegram's &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;setWebhook documentation&lt;/a&gt; defines an optional &lt;code&gt;secret_token&lt;/code&gt;: a string of 1 to 256 characters using &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt;, and &lt;code&gt;-&lt;/code&gt;. When configured, Telegram includes that value in the &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; header of every webhook request. Supplying the parameter during registration is only half the setup. Your receiver must enforce the header check.&lt;/p&gt;

&lt;p&gt;This is a shared secret, not a signature over the message body. The documented mechanism does not include a signed timestamp or a single-use nonce. Someone who obtains the value can present it again; the header alone does not establish the freshness of the JSON or replace secret protection. Protect the connections and keep the value out of logs, traces, screenshots, and error messages.&lt;/p&gt;

&lt;p&gt;Keep the webhook secret distinct from the bot token. The bot token is used to access the Bot API; this guide concerns a separate value for incoming requests. Separating them lets you replace the ingress secret without deliberately exposing the bot token. A leaked bot token remains a separate incident: replacing only the webhook secret does not address every consequence of compromised API access.&lt;/p&gt;

&lt;p&gt;A successful check answers a narrow question: did this request present a secret accepted by this endpoint? It does not establish whether a particular Telegram user may change a particular deal. After accepting the event, you still need to check the actor, their access to the object, and whether the requested transition is allowed. A CRM record identifier in incoming JSON is not, by itself, permission to modify that record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reject before creating a side effect
&lt;/h2&gt;

&lt;p&gt;Give the receiver an explicit sequence so a refusal cannot accidentally create work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Limit request size and read time at the outer layer. You can check a header before application-level JSON parsing, but the HTTP server might already buffer the body. Server and proxy limits are still necessary.&lt;/li&gt;
&lt;li&gt;Check that the method and route match your webhook endpoint. Do not redirect a request containing the secret to an arbitrary location or provide an alternative unprotected route.&lt;/li&gt;
&lt;li&gt;Extract one secret header using the HTTP framework's normal facilities. Header names are case-insensitive; compare the value exactly, without changing its case or trimming whitespace.&lt;/li&gt;
&lt;li&gt;Validate the receiver configuration. An empty expected value is a configuration error, not permission to accept requests without credentials. Prefer detecting this at startup and withholding readiness.&lt;/li&gt;
&lt;li&gt;On mismatch, stop without queuing a job or calling the CRM. Do not return the expected value or comparison details to the caller.&lt;/li&gt;
&lt;li&gt;Only then parse JSON, validate the event structure, and pass the event to durable storage. The successful HTTP acknowledgement must match the storage guarantee your system actually provides.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The status codes below are our example policy, not mandatory Telegram choices: &lt;code&gt;403&lt;/code&gt; for a failed secret check, &lt;code&gt;503&lt;/code&gt; for missing configuration, and &lt;code&gt;202&lt;/code&gt; for the decision “allow the next step.” A real handler must not send that final response before successfully preserving the event. If storage fails, authorization may have succeeded, but the request as a whole has not yet been accepted.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;Telegram documents retries after responses outside the 2XY range&lt;/a&gt;. Rejecting legitimate events because of a configuration mismatch can therefore trigger repeated delivery. Do not hide the problem with an unconditional &lt;code&gt;200&lt;/code&gt;: that can conceal event loss. Restore matching configuration and receiver readiness, then verify that the event flow has recovered. This guide does not assume a precise retry schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  A receiver decision table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Decision before the queue&lt;/th&gt;
&lt;th&gt;What the operator checks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Expected secret is absent&lt;/td&gt;
&lt;td&gt;Do not accept; instance is not ready&lt;/td&gt;
&lt;td&gt;Configuration delivery to every instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Header is missing or empty&lt;/td&gt;
&lt;td&gt;Reject without business actions&lt;/td&gt;
&lt;td&gt;Webhook registration and proxy forwarding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value differs by one character&lt;/td&gt;
&lt;td&gt;Reject; do not try to repair it&lt;/td&gt;
&lt;td&gt;Registered and expected configuration versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Value has extra whitespace&lt;/td&gt;
&lt;td&gt;Reject under this exact-match policy&lt;/td&gt;
&lt;td&gt;Configuration serialization; do not add trimming as a workaround&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret is correct but JSON is invalid&lt;/td&gt;
&lt;td&gt;Authentication passed; event not accepted&lt;/td&gt;
&lt;td&gt;Separate body limits and schema checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret is correct and event was already stored&lt;/td&gt;
&lt;td&gt;Apply deduplication&lt;/td&gt;
&lt;td&gt;Stable event key and existing processing record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret is correct but actor lacks permission&lt;/td&gt;
&lt;td&gt;Do not execute the forbidden action&lt;/td&gt;
&lt;td&gt;Business authorization, not the transport secret&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Define a separate policy for duplicate secret headers. Reject ambiguous input at an agreed layer and test the actual HTTP stack. Servers can expose repeated fields as a list, a combined string, or a selected value. A function that takes one string cannot establish how duplicate headers behave across your network path.&lt;/p&gt;

&lt;h2&gt;
  
  
  A local test without a real secret
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;ingress_check.py&lt;/code&gt; and run &lt;code&gt;python3 ingress_check.py&lt;/code&gt;. It uses only the Python 3 standard library: no packages, bot, network, or CRM. Every value shown is public test data and must not be copied into production configuration. The function receives the already-extracted header value, not raw HTTP bytes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.python.org/3/library/hmac.html#hmac.compare_digest" rel="noopener noreferrer"&gt;Python's documentation&lt;/a&gt; describes &lt;code&gt;hmac.compare_digest&lt;/code&gt; as avoiding content-based short-circuit comparison. String arguments must be ASCII, so the example first checks the type and permitted alphabet. This is not a claim that the entire network service is immune to timing analysis. The documentation separately notes information that differences in argument types and lengths could reveal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z0-9_-]{1,256}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_checks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo_receiver_token_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo_receiver_token_124&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unicode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_я&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;too_long&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;257&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bad_config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;queued&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;queued&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_for_next_step:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run_checks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local execution checked nine cases. &lt;code&gt;match&lt;/code&gt; and &lt;code&gt;repeat&lt;/code&gt; returned &lt;code&gt;202&lt;/code&gt;; six invalid incoming values returned &lt;code&gt;403&lt;/code&gt;; empty configuration returned &lt;code&gt;503&lt;/code&gt;. The final line was &lt;code&gt;accepted_for_next_step: 2&lt;/code&gt;. Here, &lt;code&gt;queued&lt;/code&gt; is only a list counting permission to continue, not a real queue. The repeated value is deliberately accepted again: secret comparison stores no event history. This result shows the boundary of the check, not a problem that rotation solves.&lt;/p&gt;

&lt;p&gt;Use a separate event key and durable storage to handle repeated deliveries. The &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=en" rel="noopener noreferrer"&gt;Telegram-to-CRM idempotency guide&lt;/a&gt; covers that stage. Do not treat the secret and &lt;code&gt;update_id&lt;/code&gt; as interchangeable controls: one limits entry, while the other helps distinguish deliveries of an already accepted event.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the reverse-proxy boundary
&lt;/h2&gt;

&lt;p&gt;A reverse proxy receives the external HTTP request and forwards it to the application. Draw the actual route: public address → TLS termination → proxy or load balancer → specific handler. For each segment, identify who can connect and where the secret is checked. HTTPS on the public address alone says nothing about whether an internal application port is reachable by an unintended caller.&lt;/p&gt;

&lt;p&gt;Check that clients cannot bypass the protective layer and reach the application directly. If only the proxy validates the secret, direct backend access without the same check bypasses that boundary. If the application validates it, the proxy must preserve the incoming value. Do not configure the proxy to insert the correct secret unconditionally into every external request: that would give an arbitrary client a trusted header inside your infrastructure.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.aiogram.dev/en/latest/dispatcher/webhook.html" rel="noopener noreferrer"&gt;official aiogram webhook example&lt;/a&gt; supplies &lt;code&gt;secret_token&lt;/code&gt; both when registering the webhook and when constructing &lt;code&gt;SimpleRequestHandler&lt;/code&gt;. It also shows a web server bound to localhost behind a proxy. These are useful inspection points, not a universal deployment recipe. Container networking or a separate load balancer may require a different bind address and appropriate network restrictions. Verify the behavior of the framework version you actually run.&lt;/p&gt;

&lt;p&gt;In a test deployment, send a request with a deliberately incorrect test value through the public address and confirm that no job appears in storage. Repeat through any accessible internal route within the authorized scope of your infrastructure. With the correct test value, check a different outcome: whether the request reaches the handler that validates and persists the event. These network checks were not performed for this guide. A local function cannot detect a dropped proxy header, an exposed backend port, or a wrongly routed endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rotate without leaving a permanent spare entrance
&lt;/h2&gt;

&lt;p&gt;Rotation means coordinating a replacement of the accepted value and the webhook registration parameter. Treat it as a distributed configuration change: instances can update at different times, and previously sent requests may still be in flight. The cited Telegram contract does not promise an atomic changeover of every delivery to a new configuration.&lt;/p&gt;

&lt;p&gt;For a planned replacement, prepare the new secret in your approved secret store and make the receiver configuration available to every instance. If you choose to accept two values temporarily, specify a bounded overlap window, an owner, and the removal point for the old value. Register the new value through the existing secure bot-management tool and verify fresh test events. After changeover, remove the old value from every receiver and confirm rejection using the old test value. This is an operational proposal, not a built-in automatic Telegram rotation feature.&lt;/p&gt;

&lt;p&gt;If the old secret may have leaked, overlap extends the period in which someone can use it. Work with the incident owner to decide whether to revoke it immediately, accepting a temporary interruption if necessary. Do not switch to “accept anything while we investigate.” Also, do not use &lt;code&gt;drop_pending_updates&lt;/code&gt; merely to make an error disappear. Telegram documents that option as dropping pending updates, not repairing authentication.&lt;/p&gt;

&lt;p&gt;Keep working values out of article commands, shell history, and tickets. Record the configuration version, changeover time, and test outcome, not the secret itself. Separately inspect how your management tool retrieves and passes the value. A general guide cannot establish that it never appears in process arguments or debug logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose refusals and establish readiness
&lt;/h2&gt;

&lt;p&gt;When refusals increase after a change, first establish which layer generated the response: an external protection service, the proxy, the HTTP framework, or your check. A &lt;code&gt;403&lt;/code&gt; by itself does not establish a bad secret_token. Correlate request time and trace identifier without copying sensitive headers. Check the selected endpoint, configuration version on every instance, and header forwarding. Broader CRM permissions will not repair an ingress failure.&lt;/p&gt;

&lt;p&gt;The Bot API's &lt;a href="https://core.telegram.org/bots/api#getwebhookinfo" rel="noopener noreferrer"&gt;getWebhookInfo method&lt;/a&gt; reports webhook state, including the pending-update count and last-error details when present. It is a diagnostic source, not evidence of the secret expected by your application. Review the output before sharing it: webhook addresses and infrastructure details can also be sensitive.&lt;/p&gt;

&lt;p&gt;Start operational monitoring with ingress-refusal counts, configuration failures, and the age of unprocessed events. A failed check must not automatically dump the entire conversation into logs. For an authorized request, separately track durable acceptance and work completion: transport authentication does not prove CRM success. Inspect the logging and metrics pipeline too, since it may capture sensitive headers independently of your handler.&lt;/p&gt;

&lt;p&gt;Readiness for the next stage means an incorrect test secret creates no job through any reachable route, a correct one reaches format validation and storage, missing configuration prevents readiness, and a repeated valid request is handled by separate deduplication. Test user permissions and the actual CRM effect independently. These are observable checks of your own system, not a security guarantee or permission to collect contacts or send unsolicited messages.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was also published on other platforms. Original: &lt;a href="https://www.24go.site/blog/telegram-webhook-secret-token-validation?locale=en" rel="noopener noreferrer"&gt;Telegram Bot Webhooks: Validate secret_token Before Queuing CRM Work&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
    <item>
      <title>Вебхук Telegram-бота: проверка secret_token до очереди и CRM [перекрёстная публикация]</title>
      <dc:creator>ALEKSANDR MICHAILOVICH</dc:creator>
      <pubDate>Tue, 27 Oct 2026 04:12:39 +0000</pubDate>
      <link>https://dev.to/dev24pro/viebkhuk-telegram-bota-provierka-secrettoken-do-ochieriedi-i-crm-pieriekriostnaia-publikatsiia-2pp8</link>
      <guid>https://dev.to/dev24pro/viebkhuk-telegram-bota-provierka-secrettoken-do-ochieriedi-i-crm-pieriekriostnaia-publikatsiia-2pp8</guid>
      <description>&lt;h1&gt;
  
  
  Вебхук Telegram-бота: проверка secret_token до очереди и CRM
&lt;/h1&gt;

&lt;p&gt;Проверяйте &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; до разбора бизнес-события и записи задания в очередь. Ожидаемое значение задаётся на вашей стороне и передаётся Telegram при регистрации вебхука через &lt;code&gt;secret_token&lt;/code&gt;. Запрос без подходящего значения не должен запускать квалификацию, создавать лид или менять сделку. Однако совпадение секрета не подтверждает права пользователя и не защищает от повторной доставки.&lt;/p&gt;

&lt;p&gt;Ниже разобрана входная граница собственной интеграции Telegram Bot API → обработчик → очередь → CRM. Понадобятся доступ к коду приёмника, его настройкам и тестовому окружению. Это не инструкция по настройке проверенной встроенной функции 24go.site. Пример работает локально с вымышленными значениями, не обращается к Telegram и не читает настоящие секреты. Он проверяет решение приёмника, а не безопасность развёрнутого сервиса целиком.&lt;/p&gt;

&lt;h2&gt;
  
  
  Какую проверку даёт secret_token
&lt;/h2&gt;

&lt;p&gt;В &lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;описании setWebhook&lt;/a&gt; Telegram документирует необязательный &lt;code&gt;secret_token&lt;/code&gt;: строку длиной от 1 до 256 символов из &lt;code&gt;A-Z&lt;/code&gt;, &lt;code&gt;a-z&lt;/code&gt;, &lt;code&gt;0-9&lt;/code&gt;, &lt;code&gt;_&lt;/code&gt; и &lt;code&gt;-&lt;/code&gt;. Если параметр задан, Telegram включает его в заголовок &lt;code&gt;X-Telegram-Bot-Api-Secret-Token&lt;/code&gt; каждого webhook-запроса. Передать параметр при регистрации недостаточно: ваш приёмник должен действительно проверять заголовок.&lt;/p&gt;

&lt;p&gt;Это общий секрет между отправителем и приёмником, а не подпись содержимого сообщения. В указанном контракте нет подписанного времени или одноразового nonce. Следовательно, тот, кто получил значение, может предъявить его повторно; сам заголовок не доказывает свежесть JSON и не заменяет защиту ключей. Защищайте соединения и не записывайте значение в журналы, трассировки, скриншоты или сообщения об ошибках.&lt;/p&gt;

&lt;p&gt;Не путайте webhook secret с токеном бота. Токен бота используется для доступа к Bot API; здесь обсуждается отдельное значение для входящих запросов. Разделение позволяет менять входной секрет, не раскрывая намеренно токен бота. Но утечку токена бота нужно разбирать отдельно: замена только webhook secret не устраняет все последствия компрометации доступа к API.&lt;/p&gt;

&lt;p&gt;Успешная проверка отвечает на узкий вопрос: предъявлен ли секрет, который принимает этот endpoint? Она не отвечает на вопрос, имеет ли конкретный Telegram-пользователь право менять данную сделку. После приёма события всё ещё нужно проверить отправителя, его доступ к объекту и допустимость действия. Нельзя считать идентификатор CRM из входящего JSON достаточным разрешением на изменение записи.&lt;/p&gt;

&lt;h2&gt;
  
  
  Поставьте отказ перед побочным эффектом
&lt;/h2&gt;

&lt;p&gt;Задайте последовательность обработки так, чтобы отказ не успевал создать работу:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;На внешнем слое ограничьте размер запроса и время чтения. Заголовок можно проверить до прикладного разбора JSON, но HTTP-сервер всё равно может буферизовать тело. Ограничения на сервере и прокси остаются необходимыми.&lt;/li&gt;
&lt;li&gt;Убедитесь, что метод и маршрут соответствуют вашему webhook endpoint. Не перенаправляйте запрос с секретом на произвольный адрес и не создавайте запасной маршрут без той же проверки.&lt;/li&gt;
&lt;li&gt;Извлеките один заголовок секрета штатными средствами HTTP-фреймворка. Имена HTTP-заголовков регистронезависимы; значение секрета сравнивайте точно, не меняя регистр и не удаляя пробелы.&lt;/li&gt;
&lt;li&gt;Проверьте конфигурацию приёмника. Пустое ожидаемое значение - ошибка настройки, а не разрешение принимать запросы без секрета. Лучше обнаружить её при запуске и не объявлять экземпляр готовым.&lt;/li&gt;
&lt;li&gt;При несовпадении завершите обработку без постановки задания и без вызова CRM. Не возвращайте клиенту ожидаемое значение или подробности сравнения.&lt;/li&gt;
&lt;li&gt;Только после этого разбирайте JSON, проверяйте его структуру и передавайте событие в надёжное хранилище. Успешное HTTP-подтверждение должно соответствовать фактической гарантии сохранения в вашей системе.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Коды ответа ниже - наша примерная политика, а не обязательный выбор Telegram: &lt;code&gt;403&lt;/code&gt; для проверки секрета, завершившейся отказом, &lt;code&gt;503&lt;/code&gt; при отсутствии настройки и &lt;code&gt;202&lt;/code&gt; как решение «разрешить следующий шаг». В реальном обработчике последний код нельзя отправлять до успешного сохранения события. Если хранилище недоступно, авторизация могла пройти, но запрос ещё не принят целиком.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://core.telegram.org/bots/api#setwebhook" rel="noopener noreferrer"&gt;Telegram описывает повтор запросов при ответе не из диапазона 2XY&lt;/a&gt;. Поэтому отказ на настоящем событии из-за ошибки конфигурации может вызвать повторные доставки. Не «чините» это безусловным &lt;code&gt;200&lt;/code&gt;: так можно скрыть потерю события. Восстановите согласованность секрета и готовность приёмника, затем проверьте, что поток событий возобновился. Точные сроки повторов здесь не предполагаются.&lt;/p&gt;

&lt;h2&gt;
  
  
  Таблица решений для приёмника
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Условие&lt;/th&gt;
&lt;th&gt;Решение до очереди&lt;/th&gt;
&lt;th&gt;Что проверять оператору&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Ожидаемый секрет отсутствует&lt;/td&gt;
&lt;td&gt;Не принимать; экземпляр не готов&lt;/td&gt;
&lt;td&gt;Доставку настройки всем экземплярам&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Заголовка нет или значение пустое&lt;/td&gt;
&lt;td&gt;Отклонить без бизнес-действий&lt;/td&gt;
&lt;td&gt;Регистрацию вебхука и передачу заголовка прокси&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Значение отличается на один символ&lt;/td&gt;
&lt;td&gt;Отклонить, не исправлять автоматически&lt;/td&gt;
&lt;td&gt;Согласованность зарегистрированной и ожидаемой версий&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;В значении есть лишний пробел&lt;/td&gt;
&lt;td&gt;Отклонить по выбранной политике точного сравнения&lt;/td&gt;
&lt;td&gt;Сериализацию настройки; не добавлять &lt;code&gt;strip()&lt;/code&gt; как обход&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Секрет верен, JSON некорректен&lt;/td&gt;
&lt;td&gt;Авторизация прошла, событие не принято&lt;/td&gt;
&lt;td&gt;Отдельные проверки формата и лимитов тела&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Секрет верен, событие уже сохранено&lt;/td&gt;
&lt;td&gt;Применить дедупликацию&lt;/td&gt;
&lt;td&gt;Ключ события и существующую запись обработки&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Секрет верен, пользователь не имеет прав&lt;/td&gt;
&lt;td&gt;Не выполнять запрещённое действие&lt;/td&gt;
&lt;td&gt;Бизнес-авторизацию, а не транспортный секрет&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Политику для дублирующихся заголовков задайте отдельно: отклоняйте неоднозначный ввод на согласованном слое и тестируйте именно свой стек. Разные серверы могут представлять повторные поля списком, объединённой строкой или одним выбранным значением. Нельзя утверждать, что пример с одним строковым аргументом проверил поведение дублирующихся заголовков на сетевом пути.&lt;/p&gt;

&lt;h2&gt;
  
  
  Локальная проверка без настоящего секрета
&lt;/h2&gt;

&lt;p&gt;Сохраните код в &lt;code&gt;ingress_check.py&lt;/code&gt; и выполните &lt;code&gt;python3 ingress_check.py&lt;/code&gt;. Нужна только стандартная библиотека Python 3; пакеты, бот, сеть и CRM не используются. Все строки ниже - публичные тестовые данные, их нельзя переносить в рабочую конфигурацию. Функция получает уже извлечённое значение заголовка, а не сырые HTTP-байты.&lt;/p&gt;

&lt;p&gt;В &lt;a href="https://docs.python.org/3/library/hmac.html#hmac.compare_digest" rel="noopener noreferrer"&gt;документации Python&lt;/a&gt; указано, что &lt;code&gt;hmac.compare_digest&lt;/code&gt; позволяет избежать короткого замыкания сравнения по содержимому. Строковые аргументы должны состоять из ASCII-символов, поэтому пример сначала проверяет тип и допустимый алфавит. Это не обещание полной защиты сетевого сервиса от анализа времени: документация отдельно оговаривает возможные утечки при различии типов и длин аргументов.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[A-Za-z0-9_-]{1,256}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;TOKEN&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fullmatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compare_digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_checks&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo_receiver_token_123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;empty&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Demo_receiver_token_124&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;space&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unicode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token_я&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;too_long&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;257&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bad_config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;queued&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;received&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;configured&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;202&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;queued&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;match&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;repeat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;accepted_for_next_step:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queued&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run_checks&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;В локальном запуске примера проверены девять случаев: &lt;code&gt;match&lt;/code&gt; и &lt;code&gt;repeat&lt;/code&gt; дают &lt;code&gt;202&lt;/code&gt;, шесть некорректных входных значений дают &lt;code&gt;403&lt;/code&gt;, пустая конфигурация - &lt;code&gt;503&lt;/code&gt;. Итоговая строка: &lt;code&gt;accepted_for_next_step: 2&lt;/code&gt;. Список &lt;code&gt;queued&lt;/code&gt; здесь лишь считает разрешения на следующий шаг и не является настоящей очередью. Повтор намеренно принимается ещё раз: сравнение секрета не хранит историю событий. Это показывает границу проверки, а не недостаток, который нужно исправлять ротацией.&lt;/p&gt;

&lt;p&gt;Для обработки повторов используйте отдельный ключ события и постоянное хранилище. Этот этап подробнее разобран в &lt;a href="https://www.24go.site/blog/telegram-crm-webhook-idempotency?locale=ru" rel="noopener noreferrer"&gt;руководстве по идемпотентности Telegram → CRM&lt;/a&gt;. Не объединяйте секрет и &lt;code&gt;update_id&lt;/code&gt; в один критерий: первый ограничивает вход, второй помогает различать доставки уже принятого события.&lt;/p&gt;

&lt;h2&gt;
  
  
  Что меняется за обратным прокси
&lt;/h2&gt;

&lt;p&gt;Обратный прокси принимает внешний HTTP-запрос и пересылает его приложению. Составьте схему реального пути: публичный адрес → TLS termination → прокси или балансировщик → конкретный обработчик. Для каждого участка определите, кто может подключиться и где проверяется заголовок. Наличие HTTPS на публичном адресе само по себе ничего не говорит о доступности внутреннего порта.&lt;/p&gt;

&lt;p&gt;Проверьте, что клиенты не могут обойти защитный слой и обратиться к приложению напрямую. Если секрет проверяет только прокси, прямой доступ к backend в обход этой проверки нарушает границу защиты. Если секрет проверяет приложение, прокси должен сохранить входное значение. Не настраивайте прокси так, чтобы он безусловно вставлял правильный секрет в каждый внешний запрос: тогда произвольный клиент получит доверенный заголовок уже внутри вашей инфраструктуры.&lt;/p&gt;

&lt;p&gt;В &lt;a href="https://docs.aiogram.dev/en/latest/dispatcher/webhook.html" rel="noopener noreferrer"&gt;официальном примере aiogram&lt;/a&gt; &lt;code&gt;secret_token&lt;/code&gt; передаётся и при регистрации webhook, и при создании &lt;code&gt;SimpleRequestHandler&lt;/code&gt;. Там же показан web-сервер, привязанный к localhost за прокси. Это полезные точки для проверки, а не универсальная схема развёртывания. Контейнерная сеть или отдельный балансировщик могут потребовать другой адрес привязки и соответствующие сетевые ограничения. Проверьте поведение именно той версии фреймворка, которую используете.&lt;/p&gt;

&lt;p&gt;В тестовом развёртывании отправьте через публичный адрес запрос с заведомо неверным тестовым значением и убедитесь, что в хранилище не появилось задания. Повторите проверку через любой доступный внутренний маршрут, не выходя за разрешённые пределы своей инфраструктуры. Затем проверьте другой результат с правильным тестовым значением: доходит ли запрос до обработчика, который проверяет и сохраняет событие. Эти сетевые проверки здесь не выполнялись. Локальная функция не обнаружит потерю заголовка на прокси, открытый внутренний порт или неверно настроенный маршрут.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ротация без постоянного запасного входа
&lt;/h2&gt;

&lt;p&gt;Ротация означает согласованную замену принимаемого значения и параметра регистрации webhook. Планируйте её как изменение распределённой системы: экземпляры могут обновиться в разное время, а ранее отправленные запросы ещё могут находиться в пути. Приведённый контракт Telegram не обещает атомарного переключения всех доставок на новую конфигурацию.&lt;/p&gt;

&lt;p&gt;Для плановой замены подготовьте новый секрет в разрешённом хранилище и доставьте конфигурацию всем экземплярам приёмника. Если временно принимаются два значения, явно определите ограниченное окно, ответственного за изменение и момент удаления старого значения. Затем зарегистрируйте новое значение через существующий безопасный инструмент управления ботом и проверьте поступление новых тестовых событий. После переключения удалите старое значение на всех экземплярах и подтвердите отказ при запросе со старым тестовым значением. Это эксплуатационная рекомендация, а не встроенная функция автоматической ротации Telegram.&lt;/p&gt;

&lt;p&gt;Если старый секрет мог утечь, окно совместимости продлевает возможность его использования. Вместе с ответственным за инцидент решите, нужно ли немедленно отозвать старое значение, даже если это приведёт к временному перерыву. Не переходите к режиму «принимать всё, пока разбираемся». Не используйте и &lt;code&gt;drop_pending_updates&lt;/code&gt; только для того, чтобы убрать ошибку: в документации эта настройка описана как удаление ожидающих обновлений, а не как исправление авторизации.&lt;/p&gt;

&lt;p&gt;Не вставляйте рабочие значения в команды из статьи, историю shell или тикеты. Записывайте версию конфигурации, время переключения и результат проверки, но не сам секрет. Отдельно проверьте, как инструмент управления получает и передаёт значение. Общее руководство не подтверждает, что секрет не появляется в аргументах процесса или отладочных журналах.&lt;/p&gt;

&lt;h2&gt;
  
  
  Как разбирать отказы и определять готовность
&lt;/h2&gt;

&lt;p&gt;Если после изменения стало больше отказов, сначала выясните, на каком уровне сформирован ответ: внешним защитным сервисом, прокси, HTTP-фреймворком или вашим проверяющим кодом. Один &lt;code&gt;403&lt;/code&gt; сам по себе не доказывает, что неверен secret_token. Сопоставьте время запроса и идентификатор трассировки, не копируя чувствительные заголовки. Проверьте выбранный endpoint, версию настройки на каждом экземпляре и передачу заголовка. Расширение прав CRM не исправит ошибку на входе.&lt;/p&gt;

&lt;p&gt;Метод &lt;code&gt;getWebhookInfo&lt;/code&gt; в &lt;a href="https://core.telegram.org/bots/api#getwebhookinfo" rel="noopener noreferrer"&gt;Bot API&lt;/a&gt; позволяет смотреть состояние вебхука, включая число ожидающих обновлений и сведения о последней ошибке, если они есть. Это источник диагностических данных, а не подтверждение значения секрета, которое ожидает приложение. Прежде чем делиться полным ответом, проверьте его: адрес вебхука и сведения об инфраструктуре тоже могут быть чувствительными.&lt;/p&gt;

&lt;p&gt;Начните мониторинг со счётчиков отказов на входе, ошибок конфигурации и возраста необработанных событий. Отказ проверки не должен приводить к записи всей переписки в журналы. Для разрешённого запроса отдельно отслеживайте надёжное сохранение и выполнение работы: транспортная авторизация не доказывает успешную работу CRM. Проверьте также конвейер журналирования и метрик: он может записывать чувствительные заголовки независимо от обработчика.&lt;/p&gt;

&lt;p&gt;Приёмник готов к следующему этапу, если неверный тестовый секрет не создаёт задание ни через один доступный маршрут, правильный проходит к проверке формата и сохранению, отсутствие настройки блокирует готовность, а повторный правильный запрос обрабатывается отдельной дедупликацией. Права пользователя и фактический результат в CRM проверяются отдельно. Это наблюдаемые проверки собственной системы, а не гарантия безопасности и не разрешение собирать контакты или отправлять нежелательные сообщения.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Эта статья также опубликована на других платформах. Оригинал: &lt;a href="https://www.24go.site/blog/telegram-webhook-secret-token-validation?locale=ru" rel="noopener noreferrer"&gt;Вебхук Telegram-бота: проверка secret_token до очереди и CRM&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>telegram</category>
      <category>marketing</category>
      <category>saastra</category>
      <category>syndicated</category>
    </item>
  </channel>
</rss>
