Skip to content

Commit 65885ab

Browse files
committed
don't delete my webhooks bro
1 parent 5848021 commit 65885ab

2 files changed

Lines changed: 137 additions & 1 deletion

File tree

.fernignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ src/elevenlabs/conversational_ai/conversation.py
66
src/elevenlabs/conversational_ai/default_audio_interface.py
77
src/elevenlabs/realtime_tts.py
88
src/elevenlabs/play.py
9-
src/elevenlabs/webhooks.py
9+
src/elevenlabs/webhooks_custom.py
1010

1111
# Ignore CI files
1212
.github/

src/elevenlabs/webhooks_custom.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import json
2+
import hmac
3+
import hashlib
4+
import time
5+
from typing import Any, Dict
6+
from .errors import BadRequestError
7+
from .webhooks.client import WebhooksClient as AutogeneratedWebhooksClient, AsyncWebhooksClient as AutogeneratedAsyncWebhooksClient
8+
9+
10+
class WebhooksClient(AutogeneratedWebhooksClient):
11+
"""
12+
A client to handle ElevenLabs webhook-related functionality
13+
Extends the autogenerated client to include custom webhook methods
14+
"""
15+
16+
def construct_event(self, rawBody: str, sig_header: str, secret: str) -> Dict:
17+
"""
18+
Constructs a webhook event object from a payload and signature.
19+
Verifies the webhook signature to ensure the event came from ElevenLabs.
20+
21+
Args:
22+
rawBody: The webhook request body. Must be the raw body, not a JSON object
23+
sig_header: The signature header from the request
24+
secret: Your webhook secret
25+
26+
Returns:
27+
The verified webhook event
28+
29+
Raises:
30+
BadRequestError: If the signature is invalid or missing
31+
"""
32+
33+
if not sig_header:
34+
raise BadRequestError(body="Missing signature header")
35+
36+
if not secret:
37+
raise BadRequestError(body="Webhook secret not configured")
38+
39+
headers = sig_header.split(',')
40+
timestamp = None
41+
signature = None
42+
43+
for header in headers:
44+
if header.startswith('t='):
45+
timestamp = header[2:]
46+
elif header.startswith('v0='):
47+
signature = header
48+
49+
if not timestamp or not signature:
50+
raise BadRequestError(body="No signature hash found with expected scheme v0")
51+
52+
# Validate timestamp
53+
req_timestamp = int(timestamp) * 1000
54+
tolerance = int(time.time() * 1000) - 30 * 60 * 1000
55+
if req_timestamp < tolerance:
56+
raise BadRequestError(body="Timestamp outside the tolerance zone")
57+
58+
# Validate hash
59+
message = f"{timestamp}.{rawBody}"
60+
61+
digest = "v0=" + hmac.new(
62+
secret.encode('utf-8'),
63+
message.encode('utf-8'),
64+
hashlib.sha256
65+
).hexdigest()
66+
67+
if signature != digest:
68+
raise BadRequestError(
69+
body="Signature hash does not match the expected signature hash for payload"
70+
)
71+
72+
return json.loads(rawBody)
73+
74+
75+
class AsyncWebhooksClient(AutogeneratedAsyncWebhooksClient):
76+
"""
77+
Async version of WebhooksClient that extends the autogenerated async client
78+
"""
79+
80+
def construct_event(self, rawBody: str, sig_header: str, secret: str) -> Dict:
81+
"""
82+
Constructs a webhook event object from a payload and signature.
83+
Verifies the webhook signature to ensure the event came from ElevenLabs.
84+
85+
Args:
86+
rawBody: The webhook request body. Must be the raw body, not a JSON object
87+
sig_header: The signature header from the request
88+
secret: Your webhook secret
89+
90+
Returns:
91+
The verified webhook event
92+
93+
Raises:
94+
BadRequestError: If the signature is invalid or missing
95+
"""
96+
97+
if not sig_header:
98+
raise BadRequestError(body="Missing signature header")
99+
100+
if not secret:
101+
raise BadRequestError(body="Webhook secret not configured")
102+
103+
headers = sig_header.split(',')
104+
timestamp = None
105+
signature = None
106+
107+
for header in headers:
108+
if header.startswith('t='):
109+
timestamp = header[2:]
110+
elif header.startswith('v0='):
111+
signature = header
112+
113+
if not timestamp or not signature:
114+
raise BadRequestError(body="No signature hash found with expected scheme v0")
115+
116+
# Validate timestamp
117+
req_timestamp = int(timestamp) * 1000
118+
tolerance = int(time.time() * 1000) - 30 * 60 * 1000
119+
if req_timestamp < tolerance:
120+
raise BadRequestError(body="Timestamp outside the tolerance zone")
121+
122+
# Validate hash
123+
message = f"{timestamp}.{rawBody}"
124+
125+
digest = "v0=" + hmac.new(
126+
secret.encode('utf-8'),
127+
message.encode('utf-8'),
128+
hashlib.sha256
129+
).hexdigest()
130+
131+
if signature != digest:
132+
raise BadRequestError(
133+
body="Signature hash does not match the expected signature hash for payload"
134+
)
135+
136+
return json.loads(rawBody)

0 commit comments

Comments
 (0)