Skip to main content

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)

Source distribution for benvo 1.0.0
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)

Table of built distributions (wheels) for benvo 1.0.0
File Interpreter ABI Platform Actions
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.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files No provenance

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