Repeated Telegram Webhooks: Avoiding a Second CRM Lead

6 min read

Separate event retries from new enquiries, persist incoming updates, and handle an ambiguous CRM create timeout without blindly creating another lead.

Repeated Telegram Webhooks: Avoiding a Second CRM Lead

If the same Telegram event arrives twice, your integration should not create a second CRM lead. A stable event identifier and a processing record help prevent that. But checking whether an ID has been seen is not enough: if the CRM creates a lead and its response is lost, your handler cannot tell whether it is safe to send another create request.

This is a design guide for developers connecting Telegram Bot API → their own handler → a CRM. It does not describe a verified built-in feature of 24go.site. It assumes your bot already receives the relevant enquiries legitimately, and that you have a database and a test CRM. The design does not grant permission to collect other people's contact details or send unsolicited messages.

Give events, contacts, and deals different identities

These objects need different keys and rules for handling repeats.

ObjectWhat its key protectsExample key in your integration
Incoming eventProcessing the same delivery twicesource + your tenant + bot + update_id
ContactLinking multiple enquiries to one persontenant + source + user identifier, when available
DealSeparating a new enquiry from an existing conversationinternal enquiry ID under an agreed business rule
In the Telegram Bot API, update_id uniquely identifies an update. Telegram explicitly documents its use for ignoring repeated updates. The additional components above are a proposed isolation scheme for your system, not Telegram's key format.

Two different messages from one person are not duplicate deliveries of the same event. They might extend one deal or start different deals; agree on the rule with the process owner. Do not turn “one delivery, one processing action” into “one person, one deal forever.”

Receiver: persist first, acknowledge second

Idempotency means that repeating an operation does not produce an additional effect. Store incoming updates durably rather than relying only on process memory.

  1. Authenticate the request and validate its expected structure. Telegram's setWebhook supports secret_token; when configured, Telegram sends it in X-Telegram-Bot-Api-Secret-Token. Check it before queuing work. Do not log the token's value.
  2. Build the event key. Do not use request arrival time, which changes on redelivery.
  3. Store the event and set its state to pending in one transaction. Put a unique constraint on the composite key. If jobs live in a separate table, write both records in the same transaction to avoid losing work between writes.
  4. Return a successful HTTP response after the commit succeeds. If the key already exists, acknowledge delivery without creating a new job. An existing pending record must remain available to the worker.
  5. If durable storage fails, do not acknowledge success. Your handler should not claim to have accepted an event it has not stored.
Hookdeck's idempotency guide describes unique constraints and processing history as idempotency strategies. The database constraint matters: separate “check whether it exists” and “insert” operations do not provide the same atomic protection against simultaneous requests.

Worker: a timeout does not mean creation failed

Before attempting creation, persist an operation_id that identifies the business operation. Keep it unchanged across worker retries. Prevent two workers from executing the same operation at once with atomic job claiming, an ownership deadline, and a recovery procedure for abandoned jobs.

Choose the next step based on capabilities verified in your CRM's own documentation:

  • Idempotent creation is supported. Reuse the same operation key on each retry within its documented validity period. Do not assume every CRM accepts an Idempotency-Key header.
  • A unique external ID and atomic upsert are supported. Map the operation to that ID. Here, upsert means creating or updating by key; an ordinary search followed by creation does not provide equivalent protection against a race.
  • Neither mechanism is available. After an ambiguous timeout, move the job to needs_reconciliation. Check the CRM using an external operation reference, request logs, or manual inspection. If you cannot establish the result, do not blindly submit another create request.
After confirmed success, store crm_id and mark the job done. If the process dies after creating the lead but before saving the result, recovery must use the same idempotency mechanism or reconciliation. A local event table alone does not guarantee exactly-once effects in a remote CRM.

Decide what repeated and new messages should do

SituationAction
Same event key, pending recordAcknowledge delivery; continue the original job
Same event key, done recordDo not repeat the CRM action
Different update_id, same contactApply the rule for continuing or opening a deal
Timeout after sending a create requestRetry with a supported stable key or reconcile; do not generate a new random key
Late stage-change eventCheck its version or whether the transition is valid rather than overwriting unconditionally
Do not transfer one provider's guarantees to another. For example, Stripe documents event retries and the absence of guaranteed delivery order. This illustrates why delivery contracts need checking; it is not the specification for your CRM or Telegram integration.

Test failure paths before enabling the integration

Use anonymized test enquiries and check more than the happy path:

  1. Deliver the same key twice, then concurrently in two requests. Storage should contain one job, and the CRM effect should not be duplicated.
  2. Stop the worker after pending is persisted. The event should still be available for processing after restart.
  3. Simulate a CRM record being created while its response is lost. Recovery should use the original key or move the job to needs_reconciliation, not blindly create again.
  4. Send a different message from the same test contact. Confirm that event deduplication does not discard it.
  5. Test authentication failure and database failure. A forged request should not enter the queue, and an unsaved event should not receive a successful acknowledgement.
In the local SQLite check accompanying this article, inserting the same key twice sequentially left one row; inserting a different key left two. This checks a storage constraint using artificial data. It is not a CRM test, a concurrent HTTP test, or a test of the entire recovery design.

For operation, monitor the age of pending jobs, errors, and the number of jobs requiring needs_reconciliation. Retain the mapping event_key → operation_id → crm_id. Avoid placing full conversations and tokens in technical logs unnecessarily. Align key retention with actual redelivery periods and data requirements: after a key is deleted, an old event can look new again.

Readiness is not just seeing a test lead appear once. Confirm that retries do not add another effect, unfinished work can recover, and an ambiguous response does not turn into a duplicate deal.