Securing webhooks
Copy page
Anyone who discovers your webhook URL can POST fake events to it. A signing secret lets you verify that events really come from 23 Telecom.
How signing works
Section titled “How signing works”Set a signing secret in the portal (Settings → Webhooks) or via
PUT /user/webhooks. Every webhook request then includes:
| Header | Value |
|---|---|
X-Webhook-Timestamp | Unix timestamp (seconds) |
X-Webhook-Signature | sha256={hex} |
Verification:
expected = HMAC-SHA256(your_secret, "{timestamp}.{raw_body}")compare: "sha256=" + expected == X-Webhook-SignatureThree rules:
- Compute over the raw request body — before any JSON parsing or re-serialization.
- Use constant-time comparison (
hmac.compare_digest,crypto.timingSafeEqual,hash_equals) — never==on signatures. - Reject stale timestamps (e.g. older than 5 minutes) to limit replay windows.
Verification code in Python, Node.js and PHP is included in the delivery webhook examples.
Managing the secret
Section titled “Managing the secret”The secret is write-only: GET /user/webhooks returns only
signing_secret_set: true|false, never the value.
| Operation | PUT body |
|---|---|
| Preserve existing secret | Omit signing_secret, or send signing_secret: null |
| Set / rotate secret | { "signing_secret": "your-new-secret" } |
| Clear secret | { "clear_signing_secret": true } |
Invalid (400 INVALID_SECRET) | signing_secret: "" or longer than 64 chars |
Defense in depth
Section titled “Defense in depth”- HTTPS only — webhook URLs must be TLS; plain HTTP exposes payloads and signatures in transit.
- Don’t reflect errors verbosely — a
401with no body is enough for a failed signature. - Idempotency by
message_id— replayed or retried events must not double-process. - Secret hygiene — generate 32+ random characters, store alongside your other secrets, rotate on staff changes.