What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
You can verify an ElevenLabs webhook in a Cloudflare Worker without its SDK by using the Worker’s native Web Crypto API—but only if your verifier matches the signing contract for the specific webhook product. Read the request body as raw bytes, validate the signature header and timestamp, verify the HMAC over the documented input, and parse JSON only after verification. ElevenLabs recommends its SDK verifier for general webhooks; the explicit timestamp.raw_body format described below is documented for Custom Channel replies, so do not assume it applies to every event type.
First confirm which ElevenLabs signing format applies
ElevenLabs’ general Webhooks documentation says webhooks use HMAC authentication and recommends checking the ElevenLabs-Signature header with its SDK. It describes the JavaScript constructEvent and Python construct_event helpers as verifying the signature, validating the timestamp, and parsing JSON. That general section does not specify the complete HMAC input construction.
As an Amazon Associate I earn from qualifying purchases.
ElevenLabs’ Custom Channel guide documents a reply signature shaped like t=1753876800,v0=<hex-digest>. In that Custom Channel reply context, the message authenticated with HMAC-SHA256 is {timestamp}.{raw_request_body}. The guide says to verify the raw body before parsing JSON and to reject stale timestamps. Its exact statement is: “The digest is an HMAC-SHA256 signature over {timestamp}.{raw_request_body} using the outbound signing secret.” This is evidence for Custom Channel replies, not proof that every ElevenLabs webhook has the same header fields or signing input. Check the documentation or SDK behavior for the event type you are integrating before deploying a custom verifier.
Choose between the SDK and a custom Worker verifier
| Route | When it fits | Trade-off |
|---|---|---|
| ElevenLabs SDK helper | Your runtime and dependency policy allow the SDK, and its helper supports the webhook type you receive. | This is the documented default for general webhooks; the helper handles signature verification, timestamp validation, and JSON parsing. Confirm the helper’s compatibility with your product and version. |
| Worker Web Crypto | You need to avoid the SDK and have a confirmed signing contract for the specific webhook. | Cloudflare Workers provides the cryptographic primitives, but you own raw-body handling, header parsing, freshness policy, compatibility checks, and maintenance if the provider changes its format. |
Cloudflare documents HMAC and SHA-256 support in Workers’ Web Crypto API, along with HMAC key import and HMAC verification. Its guidance warns against naive string comparison of message authentication codes. Using crypto.subtle.verify() avoids implementing a normal string comparison yourself.
#1 Best Overall
- POWERFUL SECURITY KEY: The Security Key C NFC is the essential physical passkey for protecting your digital life from phishing attacks. It ensures only you can access your accounts.
- WORKS WITH 1000+ ACCOUNTS: Compatible with Google, Microsoft, and Apple. A single Security Key C NFC secures 100 of your favorite accounts, including email, password managers, and more.
- FAST & CONVENIENT LOGIN: Plug in your Security Key C NFC via USB-C and tap it, or tap it against your phone (NFC) to authenticate. No batteries, no internet connection, and no extra fees required.
- TRUSTED PASSKEY TECHNOLOGY: Uses the latest passkey standards (FIDO2/WebAuthn & FIDO U2F) but does not support One-Time Passwords. For complex needs, check out the YubiKey 5 Series.
- BUILT TO LAST: Made from tough, waterproof, and crush-resistant materials. Manufactured in Sweden and programmed in the USA with the highest security standards.
Implement verification without changing the signed bytes
The example below is for a product whose confirmed contract matches the Custom Channel format: a decimal Unix timestamp in seconds, a hexadecimal v0 digest, and HMAC-SHA256 over the UTF-8 bytes of timestamp.raw_body. It uses a 300-second freshness window as an application policy example—not as a universal ElevenLabs requirement. Choose an appropriate tolerance for your deployment and clock-skew conditions.
Set ELEVENLABS_WEBHOOK_SECRET as a protected Worker secret binding, not a source-code constant. For example, configure it with wrangler secret put ELEVENLABS_WEBHOOK_SECRET for the relevant Worker environment. The binding value should be the signing secret for this webhook, not a general API key.
Rank #2
- Ultra-Compact FIDO2 Security Key - Plug-and-stay or carry on a keychain. This USB-A hardware security key offers portable, always-on protection for desktop and mobile use. (Item Size: 0.75 X 0.74 IN x 0.25 IN)
- USB-A Hardware Key for All Devices - Works with USB-A ports on PC, Mac, Android, and other laptop/notebook device. Enables secure, cross-platform login with FIDO2.0 passkey support.
- FIDO Certified Security Key - Meets FIDO and FIDO2 standards. Works with Google, Microsoft, GitHub, Dropbox, and more. Please check service compatibility before purchase.
- Passwordless Login with Passkey - Supports passkey login via WebAuthn and CTAP2. Enjoy password-free sign-ins where supported. Not all websites or services currently support passkeys.
- Advanced Multi-Factor Authentication - Offers 200 FIDO2 passkey slots and 50 OATH-TOTP slots. Strong, flexible 2FA/MFA support across various apps and authentication platforms.
const MAX_AGE_SECONDS = 300; // Application policy; choose for your deployment.
const MAX_FUTURE_SKEW_SECONDS = 60; // Application policy; choose for your deployment.
function hexToBytes(hex) {
if (hex.length === 0 || hex.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(hex)) {
throw new Error("Invalid signature encoding");
}
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < bytes.length; i++) {
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return bytes;
}
function parseSignature(header) {
// Reject repeated fields rather than choosing one ambiguously.
const fields = header.split(",");
const timestamps = [];
const digests = [];
for (const field of fields) {
const separator = field.indexOf("=");
if (separator < 1) throw new Error("Malformed signature header");
const name = field.slice(0, separator).trim();
const value = field.slice(separator + 1).trim();
if (name === "t") timestamps.push(value);
if (name === "v0") digests.push(value);
}
if (timestamps.length !== 1 || digests.length !== 1) {
throw new Error("Missing or duplicate signature fields");
}
if (!/^[0-9]+$/.test(timestamps[0])) throw new Error("Invalid timestamp");
const timestamp = Number(timestamps[0]);
if (!Number.isSafeInteger(timestamp)) throw new Error("Invalid timestamp");
return { timestampText: timestamps[0], timestamp, mac: hexToBytes(digests[0]) };
}
async function verifyElevenLabs(request, env) {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const header = request.headers.get("ElevenLabs-Signature");
if (!header) return new Response("Unauthorized", { status: 401 });
let parsed;
try {
parsed = parseSignature(header);
} catch {
return new Response("Unauthorized", { status: 401 });
}
const now = Math.floor(Date.now() / 1000);
if (parsed.timestamp < now - MAX_AGE_SECONDS ||
parsed.timestamp > now + MAX_FUTURE_SKEW_SECONDS) {
return new Response("Unauthorized", { status: 401 });
}
// Read once, before JSON parsing or any transformation of the body.
const rawBody = new Uint8Array(await request.arrayBuffer());
const prefix = new TextEncoder().encode(`${parsed.timestampText}.`);
const signedMessage = new Uint8Array(prefix.length + rawBody.length);
signedMessage.set(prefix, 0);
signedMessage.set(rawBody, prefix.length);
const secretBytes = new TextEncoder().encode(env.ELEVENLABS_WEBHOOK_SECRET);
const key = await crypto.subtle.importKey(
"raw",
secretBytes,
{ name: "HMAC", hash: "SHA-256" },
false,
["verify"]
);
let valid = false;
try {
valid = await crypto.subtle.verify("HMAC", key, parsed.mac, signedMessage);
} catch {
valid = false;
}
if (!valid) return new Response("Unauthorized", { status: 401 });
let event;
try {
event = JSON.parse(new TextDecoder().decode(rawBody));
} catch {
return new Response("Invalid JSON", { status: 400 });
}
// Validate the expected event shape, then enqueue or process idempotently.
// Return 200 promptly once the authenticated delivery is safely accepted.
return new Response("OK", { status: 200 });
}
Adapt the parser to the documented header exactly
The parser deliberately rejects missing, repeated, malformed, or non-numeric timestamp and digest fields. If the webhook’s official contract permits multiple signature versions or defines different formatting, implement that contract explicitly rather than silently accepting extra forms. The digest is decoded from hexadecimal into bytes: passing the ASCII characters of the hex string to verify() would verify the wrong value.
Keep the body byte-for-byte intact
Do not call request.json() and serialize the resulting object again before verification. JSON can be represented with different whitespace, escaping, or byte encodings while expressing the same data; a MAC authenticates bytes, not semantic JSON equivalence. The code reads the body once into a byte array and joins those exact bytes to the timestamp prefix shown in the applicable contract.
Rank #3
- Security Key : Protect your online accounts against unauthorized access by using FIDO2 and U2F authentication with T120. It's the world's most protective security key that works with windows, Mac OS, Linux as well as Chrome, Firefox, Edge and many other major browsers.
- Certified with the new FIDO2 standard, T120 provides the benefit of fast login and strong protection against phishing, account takeover as well as many other online attactks.
- Works with : Bank of America, Github, Google, Microsoft, DUO, Twitter, Facebook, Dropbox, Apple, ebay, BINANCE, mor and more.
- Fits USB-C port : Insert the T120 security key into the USB-C port of each service and log in conveniently with one touch
- For the driver download and user guide, please visit TrustKey Solutions Home support page.
Set freshness as your own explicit policy
A valid MAC can still be replayed if an old signed request is accepted indefinitely. The Custom Channel guide says to reject stale timestamps, and ElevenLabs’ general webhook page says its SDK helpers validate timestamps, but the material does not establish one universal freshness window for all webhook types. The sample’s five-minute age and one-minute future-skew values are local policy examples. Consider expected delivery delays and clock synchronization when choosing limits.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Handle authenticated deliveries safely
Authentication establishes that a request matches a secret and signed payload; it does not make event processing automatically safe. Validate the authenticated JSON against the event type you expect, then make side effects idempotent. ElevenLabs notes that retry bodies can be identical to the original and recommends deduplication using event_timestamp and event-specific identifiers such as conversation_id.
Rank #4
- USB-C or tap via NFC for easy authentication on any compatible device. No drivers needed; optional Kensington software available for advanced management features.
- Works across Windows, macOS, iOS, Android, ChromeOS, and supports Passkeys and Apple ID.
- Slim, keychain-ready form for easy carry and on-the-go authentication
- IP68-rated for dependable performance
- FIDO CTAP 2.1 for enhanced security features (e.g. resident credentials, Passkey support) and backwards compatibility with CTAP 2. FIDO2 L2 certified security for phishing resistant protection against identity theft and unauthorized access.
Acknowledge promptly after you have safely accepted the authenticated event—often by placing work on a queue—rather than holding the webhook request open for slow downstream work. ElevenLabs says to return HTTP 200 promptly after signature validation; repeated failures can eventually disable a webhook. Its general documentation says disablement occurs at 10 or more consecutive failures if no delivery has ever succeeded or the last successful delivery was more than seven days ago.
As described in ElevenLabs’ general Webhooks documentation checked on 2026-10-05, retries are disabled by default, can be enabled per webhook, and currently apply only to post_call_transcription webhooks. For retryable failures, the documented schedule is up to five retries: immediate, then 30 seconds, 2 minutes, 8 minutes, and 30 minutes, with up to 10% random jitter. The documented retryable responses are 5xx, 429, and 408; 4xx responses are not retried. These settings and policies can change, so confirm the current behavior for your webhook in your ElevenLabs account and current documentation before depending on it.
Test the verifier against the provider’s contract
Before production, confirm the exact signing input, secret, header encoding, timestamp units, and event type with the documentation or SDK implementation for the webhook you receive. Test both valid and invalid cases using signed fixtures or known-good deliveries, including:
- A valid signature over the exact raw body and timestamp.
- A one-byte body change, altered timestamp, malformed hex digest, absent header, and duplicate header fields.
- A valid signature with a timestamp outside your configured freshness policy.
- Valid signed JSON with an unexpected event shape, so authentication and application-level validation remain distinct.
The general page currently lists post_call_transcription, voice_removal_notice, voice_removal_notice_withdrawn, and voice_removed as supported event types; treat that list as changeable and verify the events enabled for your account. Never include the webhook secret in logs or error responses.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.
Recommended Free Tools




