SapanCalDeveloper Docs

Python SDK

Install and use the bentocal package for Python, sync or asyncio: typed methods, retries and webhook verification.

The bentocal package on PyPI wraps every API endpoint in a method. It takes the region from your API key, retries rate limits and provider hiccups for you, raises errors with a stable code, and checks webhook signatures in one call.

  • Python 3.11+, with a sync client and an asyncio client
  • One dependency: httpx
  • Responses are TypedDicts generated from the API's OpenAPI document, checked under mypy strict
Verify before publishing: the package isn't on PyPI yet. Remove this callout once pip install bentocal works.
Your API key is a secret, so use the SDK on your server only, never in an app you ship to people's devices.

Install

Bash
pip install bentocal

Or with uv: uv add bentocal.

Create a client

The client reads BENTOCAL_API_KEY from the environment, or takes the key as its first argument. The key already says which region your application lives in, so there's no base URL to set. See Authentication and API keys.

Python
from bentocal import BentoCal

client = BentoCal()  # reads BENTOCAL_API_KEY

Create one client and reuse it: it holds a connection pool. Call client.close() when you're done, or use it as a context manager (with BentoCal() as client:).

Fields are keyword arguments with the API's own names (user_id, slot_duration_minutes), so the API reference applies as written. Responses are dicts with the same keys as the API's JSON.

asyncio

AsyncBentoCal has the same methods; await them:

Python
from bentocal import AsyncBentoCal

async with AsyncBentoCal() as client:
    user = await client.users.get("user_123")

Outside async with, close it with await client.aclose().

Connect a calendar

Create a connect session for one of your users and send them to its url:

Python
session = client.connect_sessions.create(
    user_id="user_123",  # your own ID for the user
    redirect_uri="https://app.example.com/calendar/connected",
)
# Redirect your user to 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. Read the session to get the connection:

Python
# GET /calendar/connected?connect_session_id=cs_...&status=success
session = client.connect_sessions.get(connect_session_id)
if session["status"] == "completed":
    print("Connected", session["connection"]["email"])

To relink a connection whose token stopped working, pass its connection_id when you create the session. See Connecting calendars.

Find a time

start and end take a timezone-aware datetime or an ISO 8601 string. A naive datetime (no tzinfo) is rejected, because it doesn't say which moment it means.

Python
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"])

For raw busy times instead of slots:

Python
busy = client.free_busy.query(
    user_ids=["user_123"],
    start=datetime(2026, 11, 2, tzinfo=sydney),
    end=datetime(2026, 11, 3, tzinfo=sydney),
)

See Free/busy and availability.

Book it

Events are keyed by your own ID, so the same call creates the event and later updates it, and a retry never makes a duplicate:

Python
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")

See Writing events for every field.

Verify webhooks

Pass the raw request body, before any JSON parsing, plus the request headers. The client reads the signing secret from BENTOCAL_WEBHOOK_SECRET, or you can pass secret=. verify raises WebhookVerificationError if the signature is missing, wrong or more than five minutes old. During a secret rotation both the old and the new secret are accepted.

Python
from bentocal import WebhookVerificationError

@app.post("/webhooks/bentocal")  # Flask
def bentocal_webhook():
    try:
        event = client.webhooks.verify(request.get_data(), request.headers)
    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

The raw body and headers in other frameworks:

FrameworkBodyHeaders
Flaskrequest.get_data()request.headers
Djangorequest.bodyrequest.headers
FastAPI / Starletteawait request.body()request.headers

Deliveries are retried, so use event["id"] to skip ones you've already handled. See Webhooks for the event types.

To test a handler locally, sign your own payload with bentocal.sign_webhook(body, "whsec_test") and send it in the BentoCal-Signature header.

Errors

API errors raise BentoCalApiError. Branch on its code, which is stable; the message is for people.

Python
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":
        ...  # Send the user through a new connect session with connection_id
    raise

BentoCalApiError has status, code, message, request_id (the same ID as in the console's logs; quote it to support), details (validation issues) and retry_after. Network failures and timeouts raise BentoCalConnectionError. Both extend BentoCalError. See Errors for every code.

Retries and timeouts

Network errors, 429 and 502–504 are retried twice with exponential backoff. When the API sends Retry-After, the SDK waits that long, up to 60 seconds; a longer wait raises instead. Each attempt times out after 30 seconds. See Limits and rate limits.

Python
client = BentoCal(max_retries=4, timeout=10)  # seconds

Configuration

ArgumentEnvironment variableDefault
api_keyBENTOCAL_API_KEYRequired
webhook_secretBENTOCAL_WEBHOOK_SECRETNone
base_urlBENTOCAL_BASE_URLhttps://api.bentocal.com/{region}/v1, region from the key
timeout30 seconds
max_retries2
http_clientA new httpx.Client / httpx.AsyncClient

All methods

MethodEndpoint
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

The package is MIT licensed.