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 "", 204

Deliveries 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())