Benvo Python library
The official Python library for the Benvo API. Typed resources, automatic pagination, idempotent writes, retries with backoff, webhook verification and request signing, using only the Python standard library.
- Python 3.9 or later, no dependencies
- Type hints included (
py.typed) - Full reference: developers.benvo.io
Install
pip install benvo
Quickstart
import os
from benvo import Benvo
client = Benvo(os.environ["BENVO_API_KEY"])
ping = client.ping()
print(ping["account"]["name"], ping["key"]["scopes"])
person = client.people.create(first_name="Sophie", last_name="Tan", email="sophie.tan@example.com")
print(person["id"], person.last_response["status"])
Objects come back as dictionaries (typed with TypedDict), so person["id"] works and they serialise with json.dumps. Each one also has last_response with status, request_id, idempotent_replayed and headers; benvo.response_of(obj) reads it with a type hint.
Keep the key on your server. The API refuses requests from browsers.
Options
client = Benvo(
api_key,
base_url="https://api.benvo.io",
timeout=30,
max_retries=2,
signing_secret=os.environ.get("BENVO_SIGNING_SECRET"),
)
Every method also takes request_options={"timeout": 5, "max_retries": 0, "headers": {...}}, and writes take idempotency_key.
Resources
| Resource | Methods |
|---|---|
client.ping() |
Check the key, its account and scopes |
client.people |
list, retrieve, create, update |
client.members |
list, retrieve |
client.membership_tiers |
list |
client.leads |
list, create |
client.reservations |
list, retrieve, create, update, cancel |
client.availability |
check |
client.events |
list, retrieve |
client.rsvps |
list(event_id), create(event_id, ...), update(event_id, rsvp_id, ...) |
client.orders |
list, retrieve |
client.invoices |
list, retrieve |
client.payments |
list, retrieve |
client.menus |
list, retrieve |
client.products |
list |
client.meta |
ip_ranges (no key needed) |
Writes take the body as keyword arguments, as a dictionary, or both:
availability = client.availability.check(date="2026-11-12", party_size=4)
slot = next((s for s in availability["slots"] if s["available"]), None)
if slot:
reservation = client.reservations.create(
date=slot["date"],
time=slot["time"],
party_size=4,
guest={"name": "Sophie Tan", "email": "sophie.tan@example.com"},
)
client.reservations.update(reservation["id"], party_size=5)
client.rsvps.create("evt_8Hk2QmZ4", {"member_id": "mem_3fJ9xL2p", "guests": [{"name": "Rachel Lim"}]})
On update, a value of None is sent as null and clears the field.
Ids are opaque strings with a prefix per object (per_, mem_, res_ and so on). Store them as strings and never parse them. Money is always {"amount": 24860, "currency": "SGD"} with amount in minor units. Every object carries livemode.
Contact details and sensitive fields are only present when the key has the matching scope (people:read_contact, people:read_sensitive, orders:read_spend), so read them with .get().
Pagination
list() returns an iterator that fetches the next page with starting_after until has_more is false.
for reservation in client.reservations.list(date_from="2026-11-01", date_to="2026-11-30", limit=100):
print(reservation["id"], reservation["status"])
One page at a time:
page = client.people.list(limit=25).page()
print(len(page["data"]), page["has_more"], page["next_cursor"])
following = client.people.list(limit=25, starting_after=page["next_cursor"]).page()
Also available: .pages() and .to_list(max_items). Timestamp filters such as updated_since accept a datetime (time zone aware values are sent in UTC) or an ISO 8601 string; date filters accept a date.
Idempotency
Every create, update, cancel and RSVP write sends an Idempotency-Key. When you do not pass one, the library generates a uuid v4 and keeps the same key across its own retries, so a retried write never runs twice.
Pass your own key to make a write safe across restarts of your process:
client.leads.create(
email="marcus.wong@example.com",
message="Dinner for 30 in December",
idempotency_key=f"enquiry-{form_submission_id}",
)
A replayed result has last_response["idempotent_replayed"] == True. Reusing a key with a different body fails with idempotency_key_reused.
Errors
Every API error is a BenvoError subclass with status, type, code, param, request_id, headers and raw.
| Exception | When |
|---|---|
BenvoAuthenticationError |
401: missing, invalid, expired or revoked key |
BenvoPermissionError |
403: missing scope, module not enabled, account inactive, consent required |
BenvoNotFoundError |
404: no such record in this account |
BenvoInvalidRequestError |
400, 409, 413, 422: invalid parameters or a conflict such as slot_unavailable |
BenvoRateLimitError |
429 after the retries; retry_after holds the seconds to wait |
BenvoApiError |
5xx after the retries, or a response that is not JSON |
BenvoConnectionError |
The API could not be reached; timed_out is true for timeouts |
from benvo import BenvoInvalidRequestError
try:
client.reservations.create(date="2026-11-12", time="19:00", party_size=4, guest={"name": "Sophie Tan", "email": "sophie.tan@example.com"})
except BenvoInvalidRequestError as error:
if error.code != "slot_unavailable":
raise
print("That time was just taken.")
Quote request_id when you contact Benvo support.
Retries
The library retries up to max_retries times (default 2) on:
- 429 rate limits
- 409
idempotency_in_progress - 5xx responses
- network errors and timeouts
It waits for Retry-After when the API sends it (up to 60 seconds) and otherwise backs off exponentially from 0.5 seconds up to 8 seconds with jitter. Set max_retries=0 to turn retries off.
Webhooks
Benvo signs every webhook with Standard Webhooks headers. Verify the raw body, before any JSON parsing:
import os
from flask import Flask, request
from benvo import BenvoWebhookVerificationError, webhooks
app = Flask(__name__)
@app.post("/benvo/webhooks")
def benvo_webhook():
try:
event = webhooks.verify(request.get_data(), request.headers, os.environ["BENVO_WEBHOOK_SECRET"])
except BenvoWebhookVerificationError:
return "", 400
if event["type"] == "reservation.created":
print("New booking", event["data"]["object"]["id"])
return "", 204
webhooks.verify(payload, headers, secret, tolerance=300) checks the webhook-id, webhook-timestamp and webhook-signature headers, refuses timestamps more than tolerance seconds away from now, compares signatures in constant time and returns the parsed event. headers can be any mapping (header names in any case) or a list of pairs. Store event["id"] and skip ids you have already processed.
Endpoints set to thin payloads receive only id and object in data.object; fetch the full object with your key.
For your own tests, webhooks.sign(body, secret, msg_id, timestamp=None) returns signed headers.
Request signing
Keys marked to require signatures must sign every request. Pass the key's signing secret and the library adds the header:
client = Benvo(api_key, signing_secret=os.environ["BENVO_SIGNING_SECRET"])
Benvo-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "t.METHOD.path.body">
path is the path with its query string exactly as sent (for example /v1/people?limit=25) and body is the exact JSON sent, or empty for GET. A new signature is made for each retry. Requests older than 5 minutes are refused, so keep your server clock in sync. To sign by hand, use benvo.sign_request(secret, method, path, body, timestamp=None).
Test keys and live keys
| Key | Works on | livemode |
|---|---|---|
bvk_test_... |
Your sandbox account only, with fake data. Nothing reaches real guests | False |
bvk_live_... |
The venue account it was issued for, after the venue owner approves it | True |
The same code runs against both. client.livemode tells you which kind of key the client holds, and every object and webhook event carries livemode. Build and test with a test key, then swap in the live key.
Development
Run from packages/sdk-python:
python3 -m unittest discover -s tests # offline test suite
python3 scripts/build_dist.py # dist/benvo-1.0.0.tar.gz and dist/benvo-1.0.0-py3-none-any.whl, standard library only
python3 -m build # the same archives with the official builder, when the build package is installed
Licence
MIT. Copyright Coviola Group LLC.
Metadata
Release files for benvo 1.0.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 | Provenance | Actions |
|---|---|---|---|---|
| benvo-1.0.0.tar.gz | 21.4 kB | Without provenance | Details for benvo-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| benvo-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for benvo-1.0.0-py3-none-any.whl |
Total release size: 41.3 kB
Release files / benvo-1.0.0.tar.gz
| Download URL | benvo-1.0.0.tar.gz |
|---|---|
| Size | 21.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
2ae698e1e70be300fc9b62542d900606d27d0d43eaaa13fd902f90fe325b2a7c
|
|
BLAKE2b-256 checksum How to use checksums |
e7c8f3e93a1839aaac1c22846d8a79b5747e3a1a0eead0427a302ff7c4020167
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / benvo-1.0.0-py3-none-any.whl
| Download URL | benvo-1.0.0-py3-none-any.whl |
|---|---|
| Size | 19.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9970750717e83a9db1b04390d3930faa9393ae5fbe2b16fd5608c0ff81b7443d
|
|
BLAKE2b-256 checksum How to use checksums |
2606cbf3117a629f869b6dff1dd4df04cf4d21545fb95fe9ea6e54bcc657b181
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Security for benvo 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.