Skip to content

Commit f83ef85

Browse files
committed
extend autogenerated webhooks
1 parent 53650b8 commit f83ef85

3 files changed

Lines changed: 141 additions & 75 deletions

File tree

src/elevenlabs/client.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
BaseElevenLabs, AsyncBaseElevenLabs
88
from .environment import ElevenLabsEnvironment
99
from .realtime_tts import RealtimeTextToSpeechClient
10-
from .webhooks import WebhooksClient
10+
from .webhooks_custom import WebhooksClient, AsyncWebhooksClient
1111

1212

1313
# this is used as the default value for optional parameters
@@ -27,7 +27,7 @@ class ElevenLabs(BaseElevenLabs):
2727
2828
- environment: ElevenLabsEnvironment. The environment to use for requests from the client. from .environment import ElevenLabsEnvironment
2929
30-
Defaults to ElevenLabsEnvironment.PRODUCTION
30+
Defaults to ElevenLabsEnvironment.PRODUCTION
3131
3232
- api_key: typing.Optional[str].
3333
@@ -60,7 +60,7 @@ def __init__(
6060
httpx_client=httpx_client
6161
)
6262
self.text_to_speech = RealtimeTextToSpeechClient(client_wrapper=self._client_wrapper)
63-
self.webhooks = WebhooksClient()
63+
self.webhooks = WebhooksClient(client_wrapper=self._client_wrapper)
6464

6565

6666
class AsyncElevenLabs(AsyncBaseElevenLabs):
@@ -72,7 +72,7 @@ class AsyncElevenLabs(AsyncBaseElevenLabs):
7272
7373
- environment: ElevenLabsEnvironment. The environment to use for requests from the client. from .environment import ElevenLabsEnvironment
7474
75-
Defaults to ElevenLabsEnvironment.PRODUCTION
75+
Defaults to ElevenLabsEnvironment.PRODUCTION
7676
7777
- api_key: typing.Optional[str].
7878
@@ -107,4 +107,4 @@ def __init__(
107107
timeout=timeout,
108108
httpx_client=httpx_client
109109
)
110-
self.webhooks = WebhooksClient()
110+
self.webhooks = AsyncWebhooksClient(client_wrapper=self._client_wrapper)

src/elevenlabs/webhooks.py

Lines changed: 0 additions & 70 deletions
This file was deleted.

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)