BentoCal for Python
The official client for the BentoCal API: connect your users' Google and Microsoft calendars, find when they're free, and write events, from one API.
- Sync and asyncio clients
- Fully typed: responses are
TypedDicts generated from the API's OpenAPI document - Retries rate limits and provider hiccups for you
- Webhook signature verification built in
- One dependency (
httpx), Python 3.11+
pip install bentocal
Quickstart
from bentocal import BentoCal
client = BentoCal() # reads BENTOCAL_API_KEY
Your API key is in the console under Application → API keys. It already says which region your application lives in, so there's nothing else to configure.
1. Connect a calendar
Create a connect session for one of your users and send them to its url:
session = client.connect_sessions.create(
user_id="user_123", # your own id for the user
redirect_uri="https://app.example.com/calendar/connected",
)
return redirect(session["url"])
They sign in to Google or Microsoft and come back to your redirect_uri with connect_session_id and status in the query string:
# GET /calendar/connected?connect_session_id=cs_...&status=success
session = client.connect_sessions.get(request.args["connect_session_id"])
if session["status"] == "completed":
print("Connected", session["connection"]["email"])
2. Find a time
from datetime import datetime
from zoneinfo import ZoneInfo
sydney = ZoneInfo("Australia/Sydney")
result = client.availability.query(
participants=[{"user_id": "user_123"}, {"user_id": "user_456"}],
periods=[{"start": datetime(2026, 11, 2, 9, tzinfo=sydney), "end": datetime(2026, 11, 2, 17, tzinfo=sydney)}],
slot_duration_minutes=30,
)
for slot in result["slots"]:
print(slot["start"], slot["user_ids"])
start and end take a timezone-aware datetime or an ISO 8601 string. For raw busy blocks instead of slots, use client.free_busy.query(user_ids=[...], start=..., end=...).
3. Book it
Events are keyed by your own id, so the same call creates and later updates the event, and a retry never makes a duplicate:
slot = result["slots"][0]
client.events.upsert("user_123", "booking_789", summary="Intro call", start=slot["start"], end=slot["end"], time_zone="Australia/Sydney")
client.events.delete("user_123", "booking_789")
4. Handle webhooks
BentoCal tells you when a connection is created, needs relinking or is revoked, and when a calendar changes. Verify the signature against the raw request body:
from bentocal import WebhookVerificationError
# Flask
@app.post("/webhooks/bentocal")
def bentocal_webhook():
try:
event = client.webhooks.verify(request.get_data(), request.headers) # secret from BENTOCAL_WEBHOOK_SECRET
except WebhookVerificationError:
return "", 400
if event["type"] == "connection.relink_required":
ask_user_to_reconnect(event["data"]["user_id"], event["data"]["connection_id"])
elif event["type"] == "calendar.changed":
resync(event["data"]["user_id"], event["data"]["calendar_id"])
return "", 204
In Django pass request.body and request.headers; in FastAPI, await request.body() and request.headers. Verification fails if the signature is missing, wrong or older than five minutes. Deliveries are retried, so use event["id"] to skip ones you've already handled. During a signing-secret rotation both the old and new secret are accepted.
To test your handler, sign your own payload with bentocal.sign_webhook(body, "whsec_test") and send it as the BentoCal-Signature header.
Async
from bentocal import AsyncBentoCal
async with AsyncBentoCal() as client:
user = await client.users.get("user_123")
Every method is the same, awaited.
All methods
| Method | API |
|---|---|
connect_sessions.create(user_id=, redirect_uri=, provider=, connection_id=) |
POST /connect_sessions |
connect_sessions.get(id) |
GET /connect_sessions/{id} |
users.get(user_id) |
GET /users/{user_id} |
users.update(user_id, default_connection_id=) |
PATCH /users/{user_id} |
users.delete(user_id) |
DELETE /users/{user_id} |
connections.get(id) |
GET /connections/{id} |
connections.delete(id) |
DELETE /connections/{id} |
connections.list_calendars(id) |
GET /connections/{id}/calendars |
free_busy.query(user_ids=, start=, end=, calendar_ids=) |
POST /free_busy |
availability.query(participants=, periods=, slot_duration_minutes=, ...) |
POST /availability |
events.upsert(user_id, event_id, summary=, start=, end=, ...) |
PUT /users/{user_id}/events/{event_id} |
events.delete(user_id, event_id) |
DELETE /users/{user_id}/events/{event_id} |
webhooks.verify(payload, headers, secret=) |
checks a webhook locally |
Arguments and response keys use the API's names, so the API reference applies as written.
Errors
API errors raise BentoCalApiError. Branch on code, which is stable; the message is for people.
from bentocal import BentoCalApiError
try:
client.events.upsert(user_id, booking_id, summary="Intro call", start=start, end=end)
except BentoCalApiError as err:
if err.code == "relink_required":
# The user revoked access or changed their password: send them through a new connect
# session with connection_id to relink.
...
raise
BentoCalApiError has status, code, message, request_id (quote it to support), details (validation issues) and retry_after. Network failures and timeouts raise BentoCalConnectionError. Both extend BentoCalError.
Retries and timeouts
Network errors, 429 and 502–504 are retried twice with exponential backoff, waiting as long as the API's Retry-After asks (up to 60 seconds; longer waits raise instead). Each attempt times out after 30 seconds.
client = BentoCal(max_retries=4, timeout=10)
Configuration
| Argument | Environment variable | Default |
|---|---|---|
api_key |
BENTOCAL_API_KEY |
required |
webhook_secret |
BENTOCAL_WEBHOOK_SECRET |
none |
base_url |
BENTOCAL_BASE_URL |
https://api.bentocal.com/{region}/v1, region from the key |
timeout |
30 seconds |
|
max_retries |
2 |
|
http_client |
a new httpx.Client / httpx.AsyncClient |
License
MIT
Metadata
Release files for bentocal 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| bentocal-0.1.0.tar.gz | 46.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| bentocal-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 64.3 kB
Release files / bentocal-0.1.0.tar.gz
| Download URL | bentocal-0.1.0.tar.gz |
|---|---|
| Size | 46.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
c36eb225da63c0a51b9a1f96e15103c25f813d1bb69d0e0bd25862de574d4041
|
|
BLAKE2b-256 checksum How to use checksums |
0db13cb68daedd62a9d0ad301228edf49e358c0a6eab69f8dc89855979170d0e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / bentocal-0.1.0-py3-none-any.whl
| Download URL | bentocal-0.1.0-py3-none-any.whl |
|---|---|
| Size | 18.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
370b61a9ffe9b482f58e3c1a64b71a65d6b93ea28000c2baa0eecf61b8fed7cf
|
|
BLAKE2b-256 checksum How to use checksums |
15b9441ed0f92c29b21b46762c373cd1e1ce5bb01ab90f7ba878e445934d4a48
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|