Skip to main content

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)

Source distribution for bentocal 0.1.0
File Size Uploaded
bentocal-0.1.0.tar.gz 46.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for bentocal 0.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page