Skip to content

Server SDKs/Python

Python

velo-sdk is the server half of Velo for Python: a typed VeloClient over the REST API, an AsyncVeloClient with the same 53 methods awaitable, and nothing outside the standard library. It is built to hold a project API key, which is why it belongs on a server you control.

The package

The distribution on PyPI is velo-sdk and the import package is velo. It requires Python 3.9 or newer and declares no dependencies: the transport is urllib.request from the standard library, so installing it adds one thing to your lockfile and nothing to your audit surface.

  • 53 methods on VeloClient, one per project endpoint, and the same 53 on AsyncVeloClient under identical names and signatures.
  • Responses are frozen dataclasses with snake_case fields that match the wire exactly, so room.empty_timeout_seconds is the empty_timeout_seconds the API returns. Timestamps stay as RFC 3339 strings rather than being parsed into datetime, so nothing is lost or guessed on the way in.
  • Nested request shapes are TypedDicts: TokenPermissions, RoleInput, TemplateSettingsInput, RecordingDestinationConfig and RtmpDestinationConfig. Everything else is a keyword argument.
  • The package ships py.typed, so mypy and pyright read the annotations with no stubs.

This package is the opposite of the mobile and browser packages: it is meant to hold a project API key, because it runs on your server. A key starts with vk_ and grants full control of the project. It ends rooms, removes participants, mints a token for any identity, starts recordings and reads your usage. Never ship it inside a client application, and never hand a project key to a browser or an app binary, because anything you distribute can be read. A client gets in with a room token from POST /v1/tokens, which this package calls as create_token, or by exchanging a room code it was given.

Install

One package, no transitive dependencies to resolve.

bash
pip install velo-sdk

The names differ on purpose, which is the usual place to trip: you install velo-sdk and you import velo.

Constructing a client

The key is the only positional argument. Read it from the environment, never from source, so the value that grants control of your project is never in a diff.

python
import os

from velo import VeloClient

client = VeloClient(os.environ["VELO_API_KEY"])
  • A key that is blank, not a string, or does not start with vk_ raises ValueError at construction rather than failing on the first request. Room tokens and console session tokens are rejected here on purpose: they are not project keys.
  • Surrounding whitespace is trimmed before that check, so a key pasted with a trailing newline still works.
  • AsyncVeloClient takes exactly the same arguments and performs exactly the same validation.

Everything else is keyword only.

python
client = VeloClient(
    os.environ["VELO_API_KEY"],
    base_url="https://api.usevelo.xyz",
    timeout=15.0,
    default_headers={"X-Trace": trace_id},
)
VeloClient and AsyncVeloClient arguments
FieldTypeRequiredDescription
api_keystrYesYour project API key. It becomes the Authorization: Bearer header on every request.
base_urlstrNoDefaults to velo.DEFAULT_BASE_URL, which is https://api.usevelo.xyz. Whitespace and trailing slashes are trimmed, and an empty value raises ValueError. The resolved origin is readable back as client.base_url.
timeoutfloatNoSeconds, applied to every request. velo.DEFAULT_TIMEOUT is 30.0.
default_headersMapping[str, str]NoExtra headers on every request, for a trace id or a gateway token. The SDK's own headers are set after yours.

Every request carries Authorization: Bearer, Accept: application/json and a User-Agent of velo-python. A request with a body adds Content-Type: application/json, and an idempotent call adds Idempotency-Key. Path values are percent encoded with no safe characters, so a room named a/b c becomes a%2Fb%20c rather than a second path segment.

Quickstart

Three steps, and only the third one produces something a client may hold: create the room, mint a room token for one identity, and return the token and its url. The key stays in the process.

python
import os

from velo import VeloClient

client = VeloClient(os.environ["VELO_API_KEY"])

room = client.create_room(
    "consultation-42",
    max_participants=8,
    idempotency_key="consultation-42-create",
)


def velo_token():
    token = client.create_token(
        room=room.name,
        identity="patient-1187",
        name="Amina",
        ttl_seconds=3600,
        role="patient",
    )
    return {"token": token.token, "url": token.url}

The same three steps in both server SDKs. Only token and url leave the server.

create_token returns a Token carrying token, url, expires_at, and, when the room has a template, role and publish_profile. Send the first two to the browser or the phone and nothing else.

create_token arguments
FieldTypeRequiredDescription
roomstrYesThe room name, unnamespaced, as you created it. Keyword only, like every argument here.
identitystrYesWho the token admits. It is the identity every other call refers to.
nameOptional[str]NoThe display name other participants see.
metadataOptional[str]NoAn opaque string carried on the participant. It is not read by Velo.
ttl_secondsOptional[int]NoHow long the token stays valid. Left out of the body when None, which takes the server default.
roleOptional[str]NoA role from the template the room uses. It fixes publish sources, the subscribe graph, permissions, priority and capacity.
permissionsOptional[TokenPermissions]NoA TypedDict of can_publish, can_subscribe and can_publish_data. Only the keys you set are sent.
idempotency_keyOptional[str]NoSent as the Idempotency-Key header when it is not None.

The async client

AsyncVeloClient exposes the same method names and the same signatures as VeloClient, awaitable. Swapping one for the other is an await and nothing else, and the test suite asserts that the two surfaces stay identical signature for signature.

python
import asyncio
import os

from velo import AsyncVeloClient


async def main() -> None:
    client = AsyncVeloClient(os.environ["VELO_API_KEY"])

    room = await client.create_room("consultation-42", max_participants=8)
    token = await client.create_token(
        room=room.name,
        identity="patient-1187",
        role="patient",
    )
    print(token.token, token.url, token.expires_at)


asyncio.run(main())

Every async method delegates to the sync client through asyncio.to_thread, so it is a thread offload rather than native async sockets. The standard library has no async HTTP client and this package takes no third party dependencies, so that is the honest trade: your event loop is never blocked, but each call occupies a worker thread for its duration. If you need native async I/O, wrap VeloClient yourself with the HTTP client you already use.

What it covers

The same 53 calls the Go SDK exposes, in Python naming. This is a map, not a reference: every method maps to one endpoint, and the full request and response shapes live in the REST API reference.

  • Rooms and tokens: create_room, list_rooms, get_room, delete_room, create_token.
  • Participants and moderation: list_participants, get_participant, remove_participant, mute_track, send_data, change_participant_role, list_role_changes.
  • Sessions: list_sessions, get_session, list_room_sessions.
  • Recordings and broadcasts: start_recording, list_recordings, list_room_recordings, get_recording, stop_recording, start_broadcast, list_broadcasts, get_broadcast, stop_broadcast.
  • Ingress: create_ingress, list_ingresses, get_ingress, delete_ingress.
  • Templates, roles and destinations: create_template, list_templates, get_template, update_template, delete_template, get_template_settings, update_template_settings, put_role, get_role, delete_role, create_destination, list_destinations, get_destination, update_destination, delete_destination.
  • Room codes: create_room_code, list_room_codes, delete_room_code. This is how a client joins without ever seeing a key.
  • Webhooks: create_webhook_endpoint, list_webhook_endpoints, delete_webhook_endpoint, list_webhook_deliveries, retry_webhook_delivery.
  • Billing: get_usage and get_plan. The from query parameter the API takes is spelled from_ in Python, because from is a keyword: client.get_usage(from_="2026-07-01", to="2026-07-31").

send_data accepts bytes, bytearray, memoryview or str, and base64 encodes it for the wire, so pass your payload as it is. A response with no body, such as a 204 from a delete, comes back as None.

Pagination

List calls return a Page, a frozen dataclass carrying items, limit and offset as the server applied them, and has_more. It is iterable and has a length, so you can loop the page itself and reach for page.items only when you want the list.

python
page = client.list_sessions(room="consultation-42", active=True, limit=100)

while True:
    for session in page:
        print(session.participant_identity, session.duration_seconds)
    if not page.has_more:
        break
    page = client.list_sessions(
        room="consultation-42",
        active=True,
        limit=page.limit,
        offset=page.offset + page.limit,
    )
  • A filter left as None is dropped from the query string entirely, so an unset limit takes the server default rather than sending a literal zero.
  • Booleans are serialised as true and false, so active=True reaches the API as active=true.
  • list_destinations is the one exception to the shape: the endpoint returns a plain array, so it returns list[Destination].

Idempotency

Calls that create or trigger something accept idempotency_key=, sent as the Idempotency-Key header. Replaying the same key with the same body replays the original response instead of acting twice; replaying it with a different body is rejected with idempotency_key_reuse. Leaving it as None means the header is not sent at all.

python
room = client.create_room(
    "consultation-42",
    idempotency_key="consultation-42-create",
)

recording = client.stop_recording(egress_id, idempotency_key=f"stop-{egress_id}")
  • It is available on create_room, create_token, mute_track, send_data, change_participant_role, start_recording, stop_recording, start_broadcast, stop_broadcast, create_template, create_destination, create_room_code, create_webhook_endpoint and retry_webhook_delivery.
  • Nothing else takes one. create_ingress, the update calls and put_role have no idempotency parameter, because they are already defined by the state they set.
  • The async client takes the same keyword in the same position, because its signatures are the sync ones.

Errors

Every API failure arrives in one envelope, and the package raises a class you can catch rather than returning a response to inspect. Everything it raises descends from VeloError.

json402 Payment Required
{
  "error": {
    "code": "quota_exceeded",
    "message": "monthly participant minutes exhausted"
  }
}
python
from velo import (
    VeloAPIError,
    VeloConnectionError,
    VeloPermissionDeniedError,
    VeloQuotaExceededError,
    VeloRateLimitedError,
)

try:
    recording = client.start_recording("consultation-42", type="room_composite")
except VeloQuotaExceededError as exc:
    show_upgrade_prompt(exc.code, exc.message)
except VeloRateLimitedError:
    retry_after_backoff()
except VeloPermissionDeniedError as exc:
    disable_control(exc.permission)
except VeloAPIError as exc:
    report(exc.status, exc.code, exc.field, exc.message)
except VeloConnectionError as exc:
    log.warning("velo never answered: %s", exc.cause)
VeloError
The base class for everything this package raises. Catch it to catch all of them.
VeloAPIError
Any non-2xx response. It carries status, code, message, field and the parsed body.
VeloQuotaExceededError
Any 402, such as quota_exceeded, room_limit_reached, insufficient_balance or feature_not_in_plan. A plan limit is an expected state, not a bug, so it gets its own class.
VeloPermissionDeniedError
Any 403. Its permission property aliases field, which names the grant that was missing, so a UI can disable that one control.
VeloNotFoundError
Any 404.
VeloRateLimitedError
Any 429. Back off and retry rather than surfacing it.
VeloConnectionError
The request never produced an HTTP response, or the response body was not valid JSON. It carries message and the original exception as cause.
  • The four status classes all extend VeloAPIError, so order your except clauses narrowest first, as the example above does.
  • Status is what selects the class, not the code, so a new 402 code still arrives as VeloQuotaExceededError.
  • When a response is not a Velo envelope, code becomes http_{status} and message holds the first 512 characters of the body.

A quota failure is an expected state: the project is live and out of minutes. Branch on it first, tell the person what happened, and leave the retry loop for VeloRateLimitedError. Not every 402 is about minutes: recording and live streaming are not on the free plan, so on a free project start_recording, start_broadcast, create_ingress and create_destination raise VeloQuotaExceededError with the code feature_not_in_plan and the refused feature in field. The list, get, stop and delete calls are never refused for that reason. Full status meanings are in Authentication.

Where to go next

  • The REST API is the full surface every method here maps onto, request body by request body.
  • Webhooks is how your Python service hears about a room ending or a recording completing without polling list_sessions.
  • The Web SDK is what receives the token this package mints. The Android, iOS, Flutter and React Native packages take the same token.
  • The Go SDK is the same surface for a Go backend, method for method.

Was this page useful?

Edit this page on GitHub