Telegram Bot Webhooks: Validate secret_token Before Queuing CRM Work
Reject requests with the wrong webhook secret_token, check the proxy boundary, and plan rotation. Includes a local Python test without a bot or CRM.
Telegram Bot Webhooks: Validate secret_token Before Queuing CRM Work
Check X-Telegram-Bot-Api-Secret-Token before interpreting a business event or writing a job to a queue. Your system chooses the expected value and supplies it to Telegram as secret_token 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.
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.
What secret_token actually checks
Telegram's setWebhook documentation defines an optional secret_token: a string of 1 to 256 characters using A-Z, a-z, 0-9, _, and -. When configured, Telegram includes that value in the X-Telegram-Bot-Api-Secret-Token header of every webhook request. Supplying the parameter during registration is only half the setup. Your receiver must enforce the header check.
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.
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.
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.
Reject before creating a side effect
Give the receiver an explicit sequence so a refusal cannot accidentally create work:
- 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.
- 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.
- 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.
- 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.
- On mismatch, stop without queuing a job or calling the CRM. Do not return the expected value or comparison details to the caller.
- 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.
403 for a failed secret check, 503 for missing configuration, and 202 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.
Telegram documents retries after responses outside the 2XY range. Rejecting legitimate events because of a configuration mismatch can therefore trigger repeated delivery. Do not hide the problem with an unconditional 200: 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.
A receiver decision table
| Condition | Decision before the queue | What the operator checks |
|---|---|---|
| Expected secret is absent | Do not accept; instance is not ready | Configuration delivery to every instance |
| Header is missing or empty | Reject without business actions | Webhook registration and proxy forwarding |
| Value differs by one character | Reject; do not try to repair it | Registered and expected configuration versions |
| Value has extra whitespace | Reject under this exact-match policy | Configuration serialization; do not add trimming as a workaround |
| Secret is correct but JSON is invalid | Authentication passed; event not accepted | Separate body limits and schema checks |
| Secret is correct and event was already stored | Apply deduplication | Stable event key and existing processing record |
| Secret is correct but actor lacks permission | Do not execute the forbidden action | Business authorization, not the transport secret |
A local test without a real secret
Save this as ingress_check.py and run python3 ingress_check.py. 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.
Python's documentation describes hmac.compare_digest 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.
import hmac
import re
TOKEN = re.compile(r"[A-Za-z0-9_-]{1,256}")
def decision(received, expected):
if not isinstance(expected, str) or not TOKEN.fullmatch(expected):
return 503
if not isinstance(received, str) or not TOKEN.fullmatch(received):
return 403
return 202 if hmac.compare_digest(received, expected) else 403
def run_checks():
expected = "Demo_receiver_token_123"
cases = [
("match", expected, expected, 202),
("missing", None, expected, 403),
("empty", "", expected, 403),
("wrong", "Demo_receiver_token_124", expected, 403),
("space", expected + " ", expected, 403),
("unicode", "token_я", expected, 403),
("too_long", "A" * 257, expected, 403),
("bad_config", expected, "", 503),
("repeat", expected, expected, 202),
]
queued = []
for label, received, configured, want in cases:
status = decision(received, configured)
assert status == want, (label, status, want)
if status == 202:
queued.append(label)
print(f"{label}: {status}")
assert queued == ["match", "repeat"]
print("accepted_for_next_step:", len(queued))
if __name__ == "__main__":
run_checks()
The local execution checked nine cases. match and repeat returned 202; six invalid incoming values returned 403; empty configuration returned 503. The final line was accepted_for_next_step: 2. Here, queued 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.
Use a separate event key and durable storage to handle repeated deliveries. The Telegram-to-CRM idempotency guide covers that stage. Do not treat the secret and update_id as interchangeable controls: one limits entry, while the other helps distinguish deliveries of an already accepted event.
Check the reverse-proxy boundary
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.
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.
The official aiogram webhook example supplies secret_token both when registering the webhook and when constructing SimpleRequestHandler. 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.
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.
Rotate without leaving a permanent spare entrance
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.
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.
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 drop_pending_updates merely to make an error disappear. Telegram documents that option as dropping pending updates, not repairing authentication.
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.
Diagnose refusals and establish readiness
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 403 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.
The Bot API's getWebhookInfo method 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.
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.
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.