Examples
These examples demonstrate common robot integration patterns. All examples assume you have already created a robot and have your API key (and webhook secret if applicable) ready.
Hello World
The shortest possible “send a message” example using the SDKs.
import { LagClient } from '@lagapp/sdk';
const client = new LagClient({ token: process.env.LAG_ROBOT_API_KEY! });
const me = await client.identity();
await client.servers.rooms.messages.send(me.serverId!, 'room_xyz', {
content: 'Hello from my robot!',
}); import os
from lagclient import Client
with Client(token=os.environ['LAG_ROBOT_API_KEY']) as client:
me = client.identity()
client.servers.rooms.messages.send(
me.server_id,
'room_xyz',
content='Hello from my robot!',
)Echo Bot (Async / Long-Poll)
A complete bot that polls for events, looks for !echo <text> commands, and replies in the same room. Event polling currently uses raw HTTP because the SDKs don’t yet wrap the event endpoints, but the reply path goes through the SDK.
import { LagClient } from '@lagapp/sdk';
const API_KEY = process.env.LAG_ROBOT_API_KEY!;
const POLL_URL = 'https://api.trylag.com/robots/@me/events/poll';
const client = new LagClient({ token: API_KEY });
const me = await client.identity();
const serverId = me.serverId!;
async function pollLoop() {
while (true) {
try {
const res = await fetch(POLL_URL, {
headers: { Authorization: `Robot ${API_KEY}` },
});
if (!res.ok) {
console.error(`Poll failed: ${res.status}`);
await sleep(5000);
continue;
}
const events = (await res.json()) as Array<{
eventType: string;
payload: { type: string; data: Record<string, unknown> };
}>;
for (const event of events) {
await handleEvent(event);
}
} catch (err) {
console.error('Poll error:', err);
await sleep(5000);
}
}
}
async function handleEvent(event: { eventType: string; payload: any }) {
if (event.eventType !== 'room.message') return;
const { roomId, content } = event.payload.data;
if (typeof content !== 'string' || !content.startsWith('!echo ')) return;
const reply = content.slice(6);
await client.servers.rooms.messages.send(serverId, roomId, {
content: `🔁 ${reply}`,
});
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
pollLoop(); import asyncio
import os
import httpx
from lagclient import AsyncClient
API_KEY = os.environ['LAG_ROBOT_API_KEY']
POLL_URL = 'https://api.trylag.com/robots/@me/events/poll'
async def main() -> None:
async with AsyncClient(token=API_KEY) as client:
me = await client.identity()
server_id = me.server_id
async with httpx.AsyncClient(timeout=60) as http:
while True:
try:
res = await http.get(
POLL_URL,
headers={'Authorization': f'Robot {API_KEY}'},
)
res.raise_for_status()
for event in res.json():
await handle_event(client, server_id, event)
except Exception as e:
print(f'Poll error: {e}')
await asyncio.sleep(5)
async def handle_event(client: AsyncClient, server_id: str, event: dict) -> None:
if event['eventType'] != 'room.message':
return
data = event['payload']['data']
content = data.get('content', '')
if not isinstance(content, str) or not content.startswith('!echo '):
return
reply = content[len('!echo '):]
await client.servers.rooms.messages.send(
server_id,
data['roomId'],
content=f'U0001F501 {reply}',
)
if __name__ == '__main__':
asyncio.run(main())Welcome Bot
Greets new members in a designated welcome room. Same pattern: poll loop in raw HTTP, reply via the SDK.
import { LagClient } from '@lagapp/sdk';
const API_KEY = process.env.LAG_ROBOT_API_KEY!;
const WELCOME_ROOM_ID = 'room_welcome_xyz';
const POLL_URL = 'https://api.trylag.com/robots/@me/events/poll';
const client = new LagClient({ token: API_KEY });
const me = await client.identity();
const serverId = me.serverId!;
while (true) {
const res = await fetch(POLL_URL, {
headers: { Authorization: `Robot ${API_KEY}` },
});
if (!res.ok) {
await new Promise((r) => setTimeout(r, 5000));
continue;
}
const events = await res.json();
for (const event of events) {
if (event.eventType !== 'member.join') continue;
const { username } = event.payload.data;
await client.servers.rooms.messages.send(serverId, WELCOME_ROOM_ID, {
content: `👋 Welcome to the server, **${username}**!`,
});
}
} import asyncio
import os
import httpx
from lagclient import AsyncClient
API_KEY = os.environ['LAG_ROBOT_API_KEY']
WELCOME_ROOM_ID = 'room_welcome_xyz'
POLL_URL = 'https://api.trylag.com/robots/@me/events/poll'
async def main() -> None:
async with AsyncClient(token=API_KEY) as client:
me = await client.identity()
server_id = me.server_id
async with httpx.AsyncClient(timeout=60) as http:
while True:
try:
res = await http.get(
POLL_URL,
headers={'Authorization': f'Robot {API_KEY}'},
)
res.raise_for_status()
for event in res.json():
if event['eventType'] != 'member.join':
continue
username = event['payload']['data']['username']
await client.servers.rooms.messages.send(
server_id,
WELCOME_ROOM_ID,
content=f'U0001F44B Welcome to the server, **{username}**!',
)
except Exception as e:
print(f'Poll error: {e}')
await asyncio.sleep(5)
if __name__ == '__main__':
asyncio.run(main())Webhook Receiver
A complete server that receives webhook events, verifies signatures, and replies via the SDK. Webhook receivers must run a public HTTPS endpoint - the SDK is used for the reply path, not for receiving.
import express from 'express';
import crypto from 'node:crypto';
import { LagClient } from '@lagapp/sdk';
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!; // whsec_...
const API_KEY = process.env.LAG_ROBOT_API_KEY!; // lag_robot_...
const app = express();
app.use(express.raw({ type: 'application/json' }));
const client = new LagClient({ token: API_KEY });
function verifySignature(payload: string, headers: express.Request['headers']): boolean {
const msgId = headers['webhook-id'] as string;
const timestamp = headers['webhook-timestamp'] as string;
const signature = headers['webhook-signature'] as string;
if (!msgId || !timestamp || !signature) return false;
const secretBytes = Buffer.from(WEBHOOK_SECRET.replace('whsec_', ''), 'base64');
const signedContent = `${msgId}.${timestamp}.${payload}`;
const computed = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64');
const expected = `v1,${computed}`;
return signature.split(' ').some((sig) => sig === expected);
}
app.post('/webhook', async (req, res) => {
const payload = req.body.toString();
if (!verifySignature(payload, req.headers)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload);
if (event.type === 'room.message' && event.data.content === '!ping') {
await client.servers.rooms.messages.send(event.serverId, event.data.roomId, {
content: `Pong! Hi ${event.data.senderName}`,
});
}
res.status(200).send('OK');
});
app.listen(3000, () => console.log('Webhook server listening on port 3000')); import base64
import hmac
import hashlib
import os
from fastapi import FastAPI, Request, HTTPException
from lagclient import Client
WEBHOOK_SECRET = os.environ['WEBHOOK_SECRET'] # whsec_...
API_KEY = os.environ['LAG_ROBOT_API_KEY'] # lag_robot_...
app = FastAPI()
client = Client(token=API_KEY)
def verify_signature(payload: bytes, headers) -> bool:
msg_id = headers.get('webhook-id')
timestamp = headers.get('webhook-timestamp')
signature = headers.get('webhook-signature')
if not (msg_id and timestamp and signature):
return False
secret_bytes = base64.b64decode(WEBHOOK_SECRET.removeprefix('whsec_'))
signed_content = f'{msg_id}.{timestamp}.{payload.decode()}'.encode()
computed = hmac.new(secret_bytes, signed_content, hashlib.sha256).digest()
expected = f'v1,{base64.b64encode(computed).decode()}'
return expected in signature.split(' ')
@app.post('/webhook')
async def webhook(request: Request) -> dict:
payload = await request.body()
if not verify_signature(payload, request.headers):
raise HTTPException(status_code=401, detail='Invalid signature')
event = await request.json()
if event['type'] == 'room.message' and event['data'].get('content') == '!ping':
client.servers.rooms.messages.send(
event['serverId'],
event['data']['roomId'],
content=f"Pong! Hi {event['data']['senderName']}",
)
return {'ok': True}SSE Listener (Raw HTTP)
An SSE client that connects to the Lag event stream and processes events in real time. Event delivery is not yet wrapped by the SDKs.
const API_KEY = process.env.LAG_ROBOT_API_KEY;
const SSE_URL = 'https://api.trylag.com/robots/@me/events/sse';
let lastEventId = null;
function connect() {
const headers = {
'Authorization': `Robot ${API_KEY}`,
'Accept': 'text/event-stream',
};
if (lastEventId) headers['Last-Event-ID'] = lastEventId;
fetch(SSE_URL, { headers })
.then((response) => {
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
function read() {
reader.read().then(({ done, value }) => {
if (done) {
console.log('SSE connection closed, reconnecting...');
setTimeout(connect, 1000);
return;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('
');
buffer = lines.pop() || '';
let currentId = null;
let currentData = null;
for (const line of lines) {
if (line.startsWith('id: ')) currentId = line.slice(4);
else if (line.startsWith('data: ')) currentData = line.slice(6);
else if (line === '' && currentData) {
if (currentId) lastEventId = currentId;
try {
handleEvent(JSON.parse(currentData));
} catch (err) {
console.error('Failed to parse event:', err);
}
currentId = null;
currentData = null;
}
}
read();
});
}
read();
})
.catch((err) => {
console.error('SSE connection error:', err);
setTimeout(connect, 5000);
});
}
function handleEvent(event) {
console.log(`[${event.type}]`, JSON.stringify(event.data));
}
connect(); Webhook Signature Verification (Standalone)
If you need to verify a webhook signature outside an HTTP framework, the same logic applies. Both implementations follow the Standard Webhooks spec.
import crypto from 'node:crypto';
function verifyWebhookSignature(
payload: string,
headers: Record<string, string>,
secret: string,
): boolean {
const msgId = headers['webhook-id'];
const timestamp = headers['webhook-timestamp'];
const signatures = headers['webhook-signature'];
if (!msgId || !timestamp || !signatures) return false;
// Reject old timestamps (5 minute tolerance)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp, 10)) > 300) return false;
const secretBytes = Buffer.from(secret.replace('whsec_', ''), 'base64');
const signedContent = `${msgId}.${timestamp}.${payload}`;
const computed = crypto.createHmac('sha256', secretBytes).update(signedContent).digest('base64');
const expected = `v1,${computed}`;
return signatures.split(' ').some((sig) => sig === expected);
} import base64
import hashlib
import hmac
import time
def verify_webhook_signature(payload: str, headers: dict, secret: str) -> bool:
msg_id = headers.get('webhook-id')
timestamp = headers.get('webhook-timestamp')
signatures = headers.get('webhook-signature')
if not (msg_id and timestamp and signatures):
return False
# Reject old timestamps (5 minute tolerance)
now = int(time.time())
if abs(now - int(timestamp)) > 300:
return False
secret_bytes = base64.b64decode(secret.removeprefix('whsec_'))
signed_content = f'{msg_id}.{timestamp}.{payload}'.encode()
computed = hmac.new(secret_bytes, signed_content, hashlib.sha256).digest()
expected = f'v1,{base64.b64encode(computed).decode()}'
return expected in signatures.split(' ')Reading Message History
Walk every message in a room using the SDK pagination helpers.
import { LagClient } from '@lagapp/sdk';
const client = new LagClient({ token: process.env.LAG_ROBOT_API_KEY! });
const me = await client.identity();
const serverId = me.serverId!;
const roomId = 'room_xyz';
let total = 0;
for await (const page of client.servers.rooms.messages.iter(serverId, roomId, { limit: 100 })) {
for (const msg of page.items) {
console.log(`${msg.createdAt} ${msg.username}: ${msg.content}`);
total += 1;
}
}
console.log(`Read ${total} messages`); import os
from lagclient import Client
with Client(token=os.environ['LAG_ROBOT_API_KEY']) as client:
me = client.identity()
server_id = me.server_id
room_id = 'room_xyz'
total = 0
for page in client.servers.rooms.messages.iter(server_id, room_id, limit=100):
for msg in page.items:
print(f'{msg.created_at} {msg.username}: {msg.content}')
total += 1
print(f'Read {total} messages')Listing Members
const members = await client.servers.members.list(serverId);
for (const m of members) {
const name = m.displayName ?? m.username;
const status = m.status ?? 'offline';
console.log(`${name} (${m.role}) - ${status}`);
} members = client.servers.members.list(server_id)
for m in members:
name = m.display_name or m.username
status = m.status or 'offline'
print(f'{name} ({m.role}) - {status}')