Signing & verification
Every webhook Teler delivers is signed with HMAC-SHA256 using the shared secret configured on your Voice App or SIP Trunk. Verify the signature on every request: it’s the only way to know the call really came from Teler.
What Teler sends
Section titled “What Teler sends”Each webhook request includes two extra headers:
| Header | Example | Description |
|---|---|---|
X-Teler-Timestamp | 1714201234 | Unix epoch seconds when the event was signed. |
X-Teler-Signature | 8a92f1c4... | The HMAC-SHA256 digest as a bare hex string. |
The signature is computed as:
HMAC-SHA256(secret, "<X-Teler-Timestamp>.<raw_body>")Where:
<X-Teler-Timestamp>is the value of the timestamp header as a string.<raw_body>is the raw request body bytes, exactly as Teler sent them, with no parsing and no re-serializing.secretis the value you set when creating the Voice App or SIP Trunk.
Verify the signature
Section titled “Verify the signature”import hmacimport hashlibimport timefrom flask import Flask, request, abort
WEBHOOK_SECRET = "<your secret>"TOLERANCE_SECONDS = 300 # reject events older than 5 minutes
app = Flask(__name__)
def verify_teler_signature(raw_body: bytes, timestamp: str, signature: str) -> bool: # 1. Reject stale events (replay protection) if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS: return False
# 2. Compute the expected signature msg = f"{timestamp}.".encode() + raw_body expected = hmac.new(WEBHOOK_SECRET.encode(), msg, hashlib.sha256).hexdigest()
# 3. Constant-time compare against the raw header value return hmac.compare_digest(signature, expected)
@app.post("/webhook")def webhook(): raw_body = request.get_data() ts = request.headers.get("X-Teler-Timestamp", "") sig = request.headers.get("X-Teler-Signature", "")
if not verify_teler_signature(raw_body, ts, sig): abort(401)
event = request.get_json() # ... enqueue event for async processing ... return "", 200import express from "express";import crypto from "node:crypto";
const WEBHOOK_SECRET = "<your secret>";const TOLERANCE_SECONDS = 300; // reject events older than 5 minutes
const app = express();
// IMPORTANT: capture the raw body; Express's json parser would discard itapp.use(express.json({ verify: (req: any, _res, buf) => { req.rawBody = buf; },}));
function verifyTelerSignature(rawBody: Buffer, timestamp: string, signature: string): boolean { // 1. Reject stale events (replay protection) if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_SECONDS) { return false; }
// 2. Compute the expected signature const msg = Buffer.concat([Buffer.from(`${timestamp}.`), rawBody]); const expected = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(msg) .digest("hex");
// 3. Constant-time compare against the raw header value if (signature.length !== expected.length) return false; return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));}
app.post("/webhook", (req: any, res) => { const ts = req.header("X-Teler-Timestamp") ?? ""; const sig = req.header("X-Teler-Signature") ?? "";
if (!verifyTelerSignature(req.rawBody, ts, sig)) { return res.sendStatus(401); }
// ... enqueue req.body for async processing ... res.sendStatus(200);});Common mistakes
Section titled “Common mistakes”| Mistake | Fix |
|---|---|
| Using parsed JSON instead of the raw body | The signature is over the bytes as sent. Re-serializing changes whitespace and invalidates the signature. |
| Skipping the timestamp tolerance check | Without it, a captured request can be replayed indefinitely. 5 minutes is a sensible window. |
Using == instead of constant-time compare | Leaks signature bytes via timing. Always use hmac.compare_digest (Python) or crypto.timingSafeEqual (Node). |
| Verifying after parsing/processing | Verify first: reject before any mutation. |
Rotating the secret
Section titled “Rotating the secret”Rotating the webhook secret in the dashboard replaces the secret immediately: there is no dual-secret grace window. Deploy the new secret to your endpoint at the same time you rotate it, so verification never breaks. This needs brief coordination, but no old secret is accepted once the new one is set.
Next steps
Section titled “Next steps” Webhook overview Delivery contract, retries, ordering, idempotency.
Call events The four call lifecycle events you'll receive.