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}')