Header format
When a webhook has authentication enabled, every delivery includes a TelegraMD-Signature header.
v2 format (current):
TelegraMD-Signature: t=1748476800,sha256=base64EncodedHMAC==
v1 format (legacy):
TelegraMD-Signature: sha256=base64EncodedHMAC==
The presence of t= at the start of the header value identifies v2.
How v2 signatures are computed
signedContent = "{t}.{rawJsonBody}"
signature = base64( HMAC-SHA256(signingSecret, signedContent) )
header = "t={t},sha256={signature}"
Where:
tis a Unix timestamp in seconds (integer)rawJsonBodyis the JSON string signed server-side (JSON.stringify(payload)) before the request is sent — in practice this matches the request body byte-for-byte, but it comes from a separate serialization step rather than being read back off the wire. Verify against the raw body you received, before your own JSON parser touches itsigningSecretis the 64-character hex secret shown at webhook creation or secret rotation
Replay protection
t gives your server the information it needs to reject replayed deliveries — TelegraMD does not enforce a freshness window on your behalf, since TelegraMD is the sender, not the receiver, of these webhooks. Your server must check it independently, as shown below.
Recommended: reject any request where now - t > 5 minutes (300 seconds).
Recommended check:
import time, hmac, hashlib, base64
def verify(secret, body_bytes, signature_header):
# Parse header
parts = dict(p.split("=", 1) for p in signature_header.split(","))
t = int(parts["t"])
sig = parts["sha256"]
# Replay guard
if abs(time.time() - t) > 300:
return False
# Recompute
signed = f"{t}.{body_bytes.decode('utf-8')}"
expected = base64.b64encode(
hmac.new(secret.encode(), signed.encode(), hashlib.sha256).digest()
).decode()
return hmac.compare_digest(sig, expected)Critical: Always use
hmac.compare_digest(or equivalent constant-time compare) to prevent timing attacks. Never use==to compare the signature strings.
How v1 signatures are computed (legacy)
signedContent = rawJsonBody
signature = base64( HMAC-SHA256(signingSecret, signedContent) )
header = "sha256={signature}"
v1 has no timestamp and therefore no replay protection. Migrate to v2 to gain replay resistance.
Secret rotation
Use POST /v2/webhooks/{webhook}/actions/rotateSecret to generate a new signing secret.
Update your verification code immediately after rotating — there is no grace period for signature verification. From the moment you rotate, every delivery is signed with the new secret only. If your server is still checking signatures against the old secret, verification will fail on every delivery until you update it.
The 24-hour window that follows rotation applies to one thing only: undoing the rotation. If you rotated by mistake or aren't ready yet, call POST /v2/webhooks/{webhook}/actions/revertSecret within 24 hours to restore the previous secret. After 24 hours, the previous secret is discarded and can no longer be recovered.
The new secret is returned once in the API response and never stored in plaintext after that. If you lose it, rotate again.
v1 → v2 signature migration checklist
- Detect the header format:
t=prefix → v2;sha256=only → v1 - For v2: extract
tand validate the replay window before verifying the HMAC - Always read
body_bytesbefore your JSON parser — parsers may not preserve key order - Use timing-safe comparison for the HMAC values
- After updating your consumer, call the upgrade endpoint to switch the webhook to v2 payloads
