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 ready.

Webhook Bot (Node.js)

A complete Express server that receives webhook events, verifies signatures, and responds to messages.

import express from 'express';
import crypto from 'crypto';

const app = express();
app.use(express.raw({ type: 'application/json' }));

const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; // whsec_...
const API_KEY = process.env.ROBOT_API_KEY;          // lag_robot_...
const API_BASE = 'https://api.trylag.com/robots';

function verifySignature(payload, headers) {
  const msgId = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signature = headers['webhook-signature'];

  // Decode the whsec_ prefixed secret
  const secretBytes = Buffer.from(
    WEBHOOK_SECRET.replace('whsec_', ''),
    'base64'
  );

  // Construct the signed content
  const signedContent = `${msgId}.${timestamp}.${payload}`;

  // Compute HMAC-SHA256
  const computed = crypto
    .createHmac('sha256', secretBytes)
    .update(signedContent)
    .digest('base64');

  // Compare with provided signatures
  const expected = `v1,${computed}`;
  const signatures = signature.split(' ');

  return signatures.some((sig) => sig === expected);
}

app.post('/webhook', (req, res) => {
  const payload = req.body.toString();

  if (!verifySignature(payload, req.headers)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(payload);
  console.log(`Received event: ${event.type}`);

  switch (event.type) {
    case 'room.message':
      handleMessage(event);
      break;
    case 'member.join':
      handleMemberJoin(event);
      break;
    default:
      console.log(`Unhandled event type: ${event.type}`);
  }

  res.status(200).send('OK');
});

async function handleMessage(event) {
  const { serverId, data } = event;
  const { roomId, content, senderName } = data;

  // Respond to !ping command
  if (content === '!ping') {
    await sendMessage(serverId, roomId, `Pong! Hi ${senderName}`);
  }
}

async function handleMemberJoin(event) {
  const { serverId, data } = event;
  const welcomeRoomId = 'your-welcome-room-id';

  await sendMessage(
    serverId,
    welcomeRoomId,
    `Welcome to the server, ${data.username}!`
  );
}

async function sendMessage(serverId, roomId, content) {
  const url = `${API_BASE}/servers/${serverId}/rooms/${roomId}/messages`;

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Robot ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ content }),
  });

  if (!response.ok) {
    console.error(`Failed to send message: ${response.status}`);
  }
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

SSE Listener

An SSE client that connects to the Lag event stream and processes events in real time.

const API_KEY = process.env.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) {
              // End of event
              if (currentId) lastEventId = currentId;

              try {
                const event = JSON.parse(currentData);
                handleEvent(event);
              } 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));

  // Add your event handling logic here
}

connect();

Long-Poll Client

A simple long-poll loop that fetches pending events.

const API_KEY = process.env.ROBOT_API_KEY;
const POLL_URL = 'https://api.trylag.com/robots/@me/events/poll';

async function pollLoop() {
  while (true) {
    try {
      const response = await fetch(POLL_URL, {
        headers: {
          'Authorization': `Robot ${API_KEY}`,
        },
      });

      if (!response.ok) {
        console.error(`Poll failed: ${response.status}`);
        await sleep(5000);
        continue;
      }

      const { events } = await response.json();

      for (const event of events) {
        handleEvent(event);
      }

      // If we got a full batch, poll again immediately
      if (events.length === 50) {
        continue;
      }
    } catch (err) {
      console.error('Poll error:', err);
      await sleep(5000);
    }
  }
}

function handleEvent(event) {
  console.log(`[${event.type}]`, JSON.stringify(event.data));
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

pollLoop();

Webhook Signature Verification

Node.js

import crypto from 'crypto';

function verifyWebhookSignature(payload, headers, secret) {
  const msgId = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];

  // Reject old timestamps (5 minute tolerance)
  const now = Math.floor(Date.now() / 1000);
  if (Math.abs(now - parseInt(timestamp)) > 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);
}

Python

import hmac
import hashlib
import base64
import time

def verify_webhook_signature(payload, headers, secret):
    msg_id = headers.get('webhook-id')
    timestamp = headers.get('webhook-timestamp')
    signatures = headers.get('webhook-signature')

    # Reject old timestamps (5 minute tolerance)
    now = int(time.time())
    if abs(now - int(timestamp)) > 300:
        return False

    # Decode the whsec_ prefixed secret
    secret_bytes = base64.b64decode(secret.removeprefix('whsec_'))

    # Construct signed content
    signed_content = f'{msg_id}.{timestamp}.{payload}'

    # Compute HMAC-SHA256
    computed = hmac.new(
        secret_bytes,
        signed_content.encode('utf-8'),
        hashlib.sha256
    ).digest()

    expected = f'v1,{base64.b64encode(computed).decode()}'
    return expected in signatures.split(' ')

Sending a Message as a Robot

A standalone example of sending a message to a room.

const API_KEY = 'lag_robot_abc_yoursecretkey';
const SERVER_ID = 'your-server-id';
const ROOM_ID = 'your-room-id';

async function sendRobotMessage(content) {
  const url = `https://api.trylag.com/robots/@me/servers/${SERVER_ID}/rooms/${ROOM_ID}/messages`;

  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Authorization': `Robot ${API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ content }),
  });

  if (!response.ok) {
    const error = await response.json();
    console.error('Failed to send message:', error);
    return null;
  }

  const { message } = await response.json();
  console.log(`Message sent: ${message.id}`);
  return message;
}

// Usage
sendRobotMessage('Hello from my robot!');