lagclient
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.
Install
pip install lagclient
Requires Python 3.9+.
Quickstart
Synchronous
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)")
Asynchronous
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())
Authentication
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.
Robots
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.
Configuration
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()
Resources
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 |
Pagination
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).
Image upload
# 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.
Error handling
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.
Local development
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 + sdist
Tests use respx to mock httpx transport - no real API is required. Every
resource has its own test file in tests/.
Related
- 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.
License
MIT. See LICENSE.
Metadata
Release files for lagclient 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lagclient-0.2.0.tar.gz | 31.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lagclient-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 68.3 kB
Release files / lagclient-0.2.0.tar.gz
| Download URL | lagclient-0.2.0.tar.gz |
|---|---|
| Size | 31.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
85a9b82e7eac38372aa170921b0f4bbd0555cf714fe17f7f37b4e21f62bf2ad9
|
|
BLAKE2b-256 checksum How to use checksums |
c56119454ab76530d0fa4a8e6cc2787c227fee5eafaed3614ab60cbc4714c8a8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 10, 2026.
Transparency logRelease files / lagclient-0.2.0-py3-none-any.whl
| Download URL | lagclient-0.2.0-py3-none-any.whl |
|---|---|
| Size | 36.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
db33599e51f041918aeb68ac44d7aa7dff74e31d6acc27dd17bb37000d3af8e0
|
|
BLAKE2b-256 checksum How to use checksums |
87bfb5daba7e898692229e03de5bce389025ea6ed44e82eeaea984d5420b39a0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 10, 2026.
Transparency log