The official Python SDK for the Lag API.
lagclient is a hand-written REST client covering the public Lag API: users,
friends, DMs, servers, rooms, room messages, events, and image uploads. It
does not include the WebSocket protocol or the voice client - those are
out of scope for this package.
Ships both a synchronous Client (backed by httpx.Client) and an
asynchronous AsyncClient (backed by httpx.AsyncClient), sharing a single
Pydantic v2 model layer.
- Typed response models with autocomplete and validation.
- Typed exception hierarchy with automatic retries on transient failures.
- Cursor pagination helpers (sync and async iterators).
- Multipart image upload from path, bytes, or file-like.
- Zero required dependencies beyond
httpxandpydantic.
pip install lagclientRequires Python 3.9+.
import os
from lagclient import Client
with Client(token=os.environ["LAG_TOKEN"]) as client:
who = client.identity()
print(f"Hello, {who.display_name}")
for server in client.servers.list():
print(f"- {server.name} ({server.member_count} members)")import asyncio
import os
from lagclient import AsyncClient
async def main() -> None:
async with AsyncClient(token=os.environ["LAG_TOKEN"]) as client:
who = await client.identity()
print(f"Hello, {who.display_name}")
for server in await client.servers.list():
print(f"- {server.name}")
asyncio.run(main())The SDK accepts two credential types and auto-detects which one you passed:
- A Personal Access Token (
lag_pat_*) - for scripts, CI, and any code acting on behalf of a real user. Create one in the Lag web app under settings, or via thelagCLI withlag auth login. Sent asAuthorization: Bearer <token>. - A Robot API key (
lag_robot_*) - for bots and integrations that act as their own server-scoped identity. Create one when you create a robot on a server. Sent asAuthorization: Robot <key>.
Both types are passed via the same token parameter:
from lagclient import Client
# As a user:
user_client = Client(token="lag_pat_...")
# As a robot:
bot_client = Client(token="lag_robot_abcd1234_...")The SDK does not implement OAuth flows, refresh tokens, or browser-based login - obtain a token elsewhere and pass it in.
Robot keys are scoped to a single server and have a fixed permission set.
The SDK switches the auth scheme to Robot automatically and routes
server-scoped actions to the robot endpoints under the hood, so the same
resource methods work for both users and robots:
from lagclient import Client
with Client(token="lag_robot_abcd1234_...") as bot:
me = bot.identity() # GET /robots/@me/info
print(me.display_name, me.permissions)
# Same API as a user, but routed to /robots/@me/servers/...
bot.servers.rooms.messages.send(
me.server_id, "room_id", content="Hello from a robot"
)
page = bot.servers.rooms.messages.list(me.server_id, "room_id")
for msg in page["messages"]:
...Methods supported with a robot key:
client.identity()client.servers.rooms.list(server_id)client.servers.members.list(server_id)client.servers.rooms.messages.list(server_id, room_id, ...)client.servers.rooms.messages.send(server_id, room_id, ...)client.servers.rooms.messages.edit(server_id, room_id, message_id, ...)client.servers.rooms.messages.delete(server_id, room_id, message_id)
Methods that have no robot equivalent (e.g. users.me(), friends, DMs,
events, image uploads) raise LagInvalidTokenError upfront when called
with a robot key, or return 401/403 from the API.
client = Client(
token="lag_pat_...",
base_url="https://api.trylag.com", # default
timeout_seconds=30.0, # default
max_retries=2, # default
user_agent="my-app/1.0", # optional override
extra_headers={"X-My-Header": "v"}, # optional extra headers
# http_client=httpx.Client(...) # bring your own
)max_retries controls how many times the client retries on a transient
failure (5xx, 429, network error). Backoff is exponential with jitter, capped
at ~8s. The server's Retry-After header is honored on 429.
Both clients are also usable without a context manager:
client = Client(token="lag_pat_...")
try:
client.users.me()
finally:
client.close()Every resource hangs off the client instance. The async client has the exact
same tree, just with await in front of each call.
| Attribute | What it covers |
|---|---|
client.system |
/health, /version, /system-status, /config |
client.users |
/users/me, /users/me/avatar, /users/:id, /users/search, Steam helpers |
client.friends |
list, requests, send/accept/decline, remove, block |
client.dms |
conversations + messages with cursor pagination |
client.servers |
servers CRUD, icon upload, leave |
client.servers.invites |
create / list / revoke / preview / join |
client.servers.members |
kick, ban, mute (and lists of active bans/mutes) |
client.servers.roles |
role CRUD and assignment |
client.servers.rooms |
voice rooms |
client.servers.rooms.messages |
room chat: list/send/edit/delete with cursor pagination |
client.events |
server events: list/create/get/update/cancel/RSVP |
client.events.guests |
host-side guest moderation |
client.events.templates |
recurring event templates |
client.images |
multipart upload, metadata, status, delete |
DM and room message endpoints return {messages, hasMore, nextCursor}. You
can walk pages yourself:
cursor = None
while True:
page = client.dms.list_messages("c1", limit=50, cursor=cursor)
for msg in page["messages"]:
handle(msg)
if not page["hasMore"] or page["nextCursor"] is None:
break
cursor = page["nextCursor"]Or use the built-in iterator helpers:
# Sync
for page in client.dms.iter_messages("c1", limit=50):
for msg in page.items:
handle(msg)
# Async
async for page in async_client.dms.iter_messages("c1", limit=50):
for msg in page.items:
handle(msg)The same pattern works for room messages via
client.servers.rooms.messages.iter(server_id, room_id).
# From a file path:
client.images.upload("./avatar.png", purpose="avatar")
# From raw bytes:
with open("./avatar.png", "rb") as f:
client.images.upload(
f.read(),
purpose="avatar",
filename="avatar.png",
content_type="image/png",
)
# From a file-like:
with open("./cover.jpg", "rb") as f:
client.images.upload(f, purpose="event_cover", filename="cover.jpg")The maximum upload size is 25 MiB.
Every non-2xx response becomes a typed exception you can catch precisely:
from lagclient import (
LagAPIError,
LagAuthError,
LagPermissionError,
LagNotFoundError,
LagConflictError,
LagRateLimitError,
LagServerError,
LagConnectionError,
LagInvalidTokenError,
)
try:
client.servers.get("does-not-exist")
except LagNotFoundError:
print("not there")
except LagRateLimitError as err:
print(f"slow down - retry after {err.retry_after_seconds}s")
except LagAPIError as err:
print(f"{err.status}: {err}")Network failures (DNS, refused, timeouts before any response) are raised as
LagConnectionError. Everything else is a subclass of LagAPIError.
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
pytest
mypy src/lagclient
ruff check src tests
python -m build # produces wheel + sdistTests use respx to mock httpx transport - no real API is required. Every
resource has its own test file in tests/.
- The Lag API itself -
product/apps/api/in the Lag monorepo. @lag/sdkfor TypeScript / Node - sibling package insdks/node/.- The
lagCLI incli/- also MIT licensed.
MIT. See LICENSE.