Skip to content

Commit 8e46c17

Browse files
authored
Merge pull request #9 from browser-use/feat/webhooks
feat: Add Webhook Utility Functions, Improve Docs
2 parents 655a660 + 7ddcd16 commit 8e46c17

File tree

4 files changed

+460
-137
lines changed

4 files changed

+460
-137
lines changed

README.md

Lines changed: 108 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -26,65 +26,6 @@ result.done_output
2626

2727
> The full API of this library can be found in [api.md](api.md).
2828
29-
## Async usage
30-
31-
Simply import `AsyncBrowserUse` instead of `BrowserUse` and use `await` with each API call:
32-
33-
```python
34-
import os
35-
import asyncio
36-
from browser_use_sdk import AsyncBrowserUse
37-
38-
client = AsyncBrowserUse(
39-
api_key=os.environ.get("BROWSER_USE_API_KEY"), # This is the default and can be omitted
40-
)
41-
42-
43-
async def main() -> None:
44-
task = await client.tasks.run(
45-
task="Search for the top 10 Hacker News posts and return the title and url.",
46-
)
47-
print(task.done_output)
48-
49-
50-
asyncio.run(main())
51-
```
52-
53-
Functionality between the synchronous and asynchronous clients is otherwise identical.
54-
55-
### With aiohttp
56-
57-
By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
58-
59-
You can enable this by installing `aiohttp`:
60-
61-
```sh
62-
# install from PyPI
63-
pip install browser-use-sdk[aiohttp]
64-
```
65-
66-
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
67-
68-
```python
69-
import asyncio
70-
from browser_use_sdk import DefaultAioHttpClient
71-
from browser_use_sdk import AsyncBrowserUse
72-
73-
74-
async def main() -> None:
75-
async with AsyncBrowserUse(
76-
api_key="My API Key",
77-
http_client=DefaultAioHttpClient(),
78-
) as client:
79-
task = await client.tasks.run(
80-
task="Search for the top 10 Hacker News posts and return the title and url.",
81-
)
82-
print(task.done_output)
83-
84-
85-
asyncio.run(main())
86-
```
87-
8829
## Structured Output with Pydantic
8930

9031
Browser Use Python SDK provides first class support for Pydantic models.
@@ -115,6 +56,10 @@ asyncio.run(main())
11556

11657
## Streaming Updates with Async Iterators
11758

59+
> When presenting a long running task you might want to show updates as they happen.
60+
61+
Browser Use SDK exposes a `.stream` method that lets you subscribe to a sync or an async generator that automatically polls Browser Use Cloud servers and emits a new event when an update happens (e.g., live url becomes available, agent takes a new step, or agent completes the task).
62+
11863
```py
11964
class HackerNewsPost(BaseModel):
12065
title: str
@@ -132,7 +77,7 @@ async def main() -> None:
13277
structured_output_json=SearchResult,
13378
)
13479

135-
async for update in client.tasks.stream(structured_task.id, structured_output_json=SearchResult):
80+
async for update in client.tasks.stream(task.id, structured_output_json=SearchResult):
13681
if len(update.steps) > 0:
13782
last_step = update.steps[-1]
13883
print(f"{update.status}: {last_step.url} ({last_step.next_goal})")
@@ -152,6 +97,109 @@ async def main() -> None:
15297
asyncio.run(main())
15398
```
15499

100+
## Verifying Webhook Events
101+
102+
> You can configure Browser Use Cloud to emit Webhook events and process them easily with Browser Use Python SDK.
103+
104+
Browser Use SDK lets you easily verify the signature and structure of the payload you receive in the webhook.
105+
106+
```py
107+
import uvicorn
108+
import os
109+
from browser_use_sdk.lib.webhooks import Webhook, verify_webhook_event_signature
110+
111+
from fastapi import FastAPI, Request, HTTPException
112+
113+
app = FastAPI()
114+
115+
SECRET_KEY = os.environ['SECRET_KEY']
116+
117+
@app.post('/webhook')
118+
async def webhook(request: Request):
119+
body = await request.json()
120+
121+
timestamp = request.headers.get('X-Browser-Use-Timestamp')
122+
signature = request.headers.get('X-Browser-Use-Signature')
123+
124+
verified_webhook: Webhook = verify_webhook_event_signature(
125+
body=body,
126+
timestamp=timestamp,
127+
secret=SECRET_KEY,
128+
expected_signature=signature,
129+
)
130+
131+
if verified_webhook is not None:
132+
print('Webhook received:', verified_webhook)
133+
else:
134+
print('Invalid webhook received')
135+
136+
return {'status': 'success', 'message': 'Webhook received'}
137+
138+
if __name__ == '__main__':
139+
uvicorn.run(app, host='0.0.0.0', port=8080)
140+
```
141+
142+
## Async usage
143+
144+
Simply import `AsyncBrowserUse` instead of `BrowserUse` and use `await` with each API call:
145+
146+
```python
147+
import os
148+
import asyncio
149+
from browser_use_sdk import AsyncBrowserUse
150+
151+
client = AsyncBrowserUse(
152+
api_key=os.environ.get("BROWSER_USE_API_KEY"), # This is the default and can be omitted
153+
)
154+
155+
156+
async def main() -> None:
157+
task = await client.tasks.run(
158+
task="Search for the top 10 Hacker News posts and return the title and url.",
159+
)
160+
print(task.done_output)
161+
162+
163+
asyncio.run(main())
164+
```
165+
166+
Functionality between the synchronous and asynchronous clients is otherwise identical.
167+
168+
### With aiohttp
169+
170+
By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
171+
172+
You can enable this by installing `aiohttp`:
173+
174+
```sh
175+
# install from PyPI
176+
pip install browser-use-sdk[aiohttp]
177+
```
178+
179+
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
180+
181+
```python
182+
import asyncio
183+
from browser_use_sdk import DefaultAioHttpClient
184+
from browser_use_sdk import AsyncBrowserUse
185+
186+
187+
async def main() -> None:
188+
async with AsyncBrowserUse(
189+
api_key="My API Key",
190+
http_client=DefaultAioHttpClient(),
191+
) as client:
192+
task = await client.tasks.run(
193+
task="Search for the top 10 Hacker News posts and return the title and url.",
194+
)
195+
print(task.done_output)
196+
197+
198+
asyncio.run(main())
199+
```
200+
201+
## Advanced
202+
155203
## Handling errors
156204

157205
When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `browser_use_sdk.APIConnectionError` is raised.
@@ -247,8 +295,6 @@ On timeout, an `APITimeoutError` is thrown.
247295

248296
Note that requests that time out are [retried twice by default](#retries).
249297

250-
## Advanced
251-
252298
### Logging
253299

254300
We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module.
@@ -294,58 +340,6 @@ These methods return an [`APIResponse`](https://github.com/browser-use/browser-u
294340

295341
The async client returns an [`AsyncAPIResponse`](https://github.com/browser-use/browser-use-python/tree/main/src/browser_use_sdk/_response.py) with the same structure, the only difference being `await`able methods for reading the response content.
296342

297-
#### `.with_streaming_response`
298-
299-
The above interface eagerly reads the full response body when you make the request, which may not always be what you want.
300-
301-
To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods.
302-
303-
```python
304-
with client.tasks.with_streaming_response.create(
305-
task="Search for the top 10 Hacker News posts and return the title and url.",
306-
) as response:
307-
print(response.headers.get("X-My-Header"))
308-
309-
for line in response.iter_lines():
310-
print(line)
311-
```
312-
313-
The context manager is required so that the response will reliably be closed.
314-
315-
### Making custom/undocumented requests
316-
317-
This library is typed for convenient access to the documented API.
318-
319-
If you need to access undocumented endpoints, params, or response properties, the library can still be used.
320-
321-
#### Undocumented endpoints
322-
323-
To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other
324-
http verbs. Options on the client will be respected (such as retries) when making this request.
325-
326-
```py
327-
import httpx
328-
329-
response = client.post(
330-
"/foo",
331-
cast_to=httpx.Response,
332-
body={"my_param": True},
333-
)
334-
335-
print(response.headers.get("x-foo"))
336-
```
337-
338-
#### Undocumented request params
339-
340-
If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request
341-
options.
342-
343-
#### Undocumented response properties
344-
345-
To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You
346-
can also get all the extra fields on the Pydantic model as a dict with
347-
[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra).
348-
349343
### Configuring the HTTP client
350344

351345
You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including:
@@ -388,29 +382,6 @@ with BrowserUse() as client:
388382
# HTTP client is now closed
389383
```
390384

391-
## Versioning
392-
393-
This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
394-
395-
1. Changes that only affect static types, without breaking runtime behavior.
396-
2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
397-
3. Changes that we do not expect to impact the vast majority of users in practice.
398-
399-
We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
400-
401-
We are keen for your feedback; please open an [issue](https://www.github.com/browser-use/browser-use-python/issues) with questions, bugs, or suggestions.
402-
403-
### Determining the installed version
404-
405-
If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version.
406-
407-
You can determine the version that is being used at runtime with:
408-
409-
```py
410-
import browser_use_sdk
411-
print(browser_use_sdk.__version__)
412-
```
413-
414385
## Requirements
415386

416387
Python 3.8 or higher.

examples/webhooks.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env -S rye run python
2+
3+
from typing import Any, Dict, Tuple
4+
from datetime import datetime
5+
6+
from browser_use_sdk.lib.webhooks import (
7+
Webhook,
8+
WebhookAgentTaskStatusUpdate,
9+
WebhookAgentTaskStatusUpdatePayload,
10+
create_webhook_signature,
11+
verify_webhook_event_signature,
12+
)
13+
14+
SECRET = "your-webhook-secret-key"
15+
16+
17+
def mock_webhook_event() -> Tuple[Dict[str, Any], str, str]:
18+
"""Mock a webhook event."""
19+
20+
timestamp = datetime.fromisoformat("2023-01-01T00:00:00").isoformat()
21+
22+
payload = WebhookAgentTaskStatusUpdatePayload(
23+
session_id="sess_123",
24+
task_id="task_123",
25+
status="started",
26+
metadata={"progress": 25},
27+
)
28+
29+
signature = create_webhook_signature(
30+
payload=payload.model_dump(),
31+
timestamp=timestamp,
32+
secret=SECRET,
33+
)
34+
35+
evt: Webhook = WebhookAgentTaskStatusUpdate(
36+
type="agent.task.status_update",
37+
timestamp=datetime.fromisoformat("2023-01-01T00:00:00"),
38+
payload=payload,
39+
)
40+
41+
return evt.model_dump(), signature, timestamp
42+
43+
44+
def main() -> None:
45+
"""Demonstrate webhook functionality."""
46+
47+
# NOTE: You'd get the evt and signature from the webhook request body and headers!
48+
evt, signature, timestamp = mock_webhook_event()
49+
50+
verified_webhook = verify_webhook_event_signature(
51+
body=evt,
52+
expected_signature=signature,
53+
timestamp=timestamp,
54+
secret=SECRET,
55+
)
56+
57+
if verified_webhook is None:
58+
print("✗ Webhook signature verification failed")
59+
else:
60+
print("✓ Webhook signature verified successfully")
61+
print(f" Event type: {verified_webhook.type}")
62+
63+
64+
if __name__ == "__main__":
65+
main()

0 commit comments

Comments
 (0)