Python
The official client for Python, sync and async.
The vojo package is the official Python client. It needs Python 3.10 or newer, and its only dependency is httpx.
Install
The package isn't published yet. Until it is, ask your Vojo contact for a copy.
Make your first call
import os
from vojo import Vojo
client = Vojo(os.environ["VOJO_KEY"])
organisation = client.organisation.retrieve()
print(organisation.name)Leave the key out and the client reads VOJO_SECRET_KEY.
Call the API
Methods follow the API's paths, and arguments match the API's field names. The one exception is from, a reserved word in Python, which is from_. Each reference page shows its method. Most methods return the resource itself, and statistics and issues keep data and meta.
site = client.sites.create(name="Leeds Trinity")
screen = client.screens.create(name="Lobby North", site_id=site.id, size_inches=55)
same = client.screens.retrieve(screen.id)
# Fields you leave out are unchanged. None sends null.
client.screens.update(screen.id, name="Lobby South", notes=None)Page through a list
Looping walks every item across every page, fetching more as it goes.
for screen in client.screens.list(has_harvester=True):
print(screen.name)first_page gives you one page, if you want to page yourself.
page = client.screens.list(limit=100).first_page()
print(len(page.data), page.has_more, page.next_cursor)Handle errors
import vojo
try:
client.screens.retrieve("scr_missing")
except vojo.NotFoundError as error:
print(error.code, error.request_id)
except vojo.RateLimitError as error:
print("wait", error.retry_after)
except vojo.APIError as error:
print(error.status, error.code)| Error | When |
|---|---|
AuthenticationError |
401 |
PermissionDeniedError |
402 or 403 |
NotFoundError |
404 |
ValidationError |
400, 413 or 422 |
ConflictError |
409 |
RateLimitError |
429: see retry_after |
ServerError |
5xx |
APIConnectionError, APITimeoutError |
No response |
PaginationError |
The API's paging data couldn't be followed safely |
WebhookVerificationError |
A webhook delivery failed verification: see kind |
Every error derives from VojoError. API errors carry status, code, problem (the full problem response), request_id and headers. Always handle a code you haven't seen, because new ones can be added.
Verify webhooks
Verifying needs only the endpoint's signing secret, not an API key. Pass the raw request body, exactly as you received it. This example uses Flask.
import os
from flask import Flask, request
from vojo import WebhookVerificationError, webhooks
app = Flask(__name__)
signing_secret = os.environ["VOJO_WEBHOOK_SECRET"]
@app.post("/vojo")
def receive():
try:
event = webhooks.construct_event(
request.get_data(), request.headers.get(webhooks.SIGNATURE_HEADER), signing_secret
)
except WebhookVerificationError as error:
return error.kind, 400
if event.type == "verification.score.final":
print(event.payload.playId)
return "", 204Deliveries can repeat, so deduplicate on event.id, which stays the same across retries. While you switch to a new secret after a roll, pass both: webhooks.construct_event(body, header, [new_secret, old_secret]).
To test your own handler, sign a payload with webhooks.generate_test_header(body, secret).
Retries and idempotency
Failed requests are retried twice when that's safe: connection failures, 429, and 5xx on operations that can't apply twice.
Every create is sent with an Idempotency-Key, reused across its retries. To retry across your own restarts, supply your own.
site = client.sites.create(name="Leeds Trinity", idempotency_key=f"site-{your_record_id}")Configure the client
client = Vojo(
key,
organisation="org_123", # OAuth access tokens only.
version="2026-10-07", # Default: the version this release was built against.
timeout=30.0, # In seconds. Default 60.
max_retries=5, # Default 2.
base_url="http://127.0.0.1:4020", # For example, the mock server.
)
# For one call, or a group of calls.
client.screens.list(request_options={"timeout": 5.0})
client.with_options(max_retries=0).screens.retrieve(screen_id)A function that returns the current token also works in place of the key, for OAuth tokens that refresh.
Results carry the HTTP details on last_response, with status, headers, request_id, meta and idempotent_replayed. Check wrapper.last_response.meta after you wrap a creative, and quote request_id when you contact Vojo.
Use it with async
AsyncVojo has the same methods.
import asyncio
from vojo import AsyncVojo
async def main():
async with AsyncVojo() as client:
async for screen in client.screens.list():
print(screen.name)
asyncio.run(main())