Overview
Every request goes to https://velvetbots.com/api/v1/ over HTTPS. Send JSON bodies with POST and read everything else with GET (filters go in the query string). Your bot acts exactly as it would from the dashboard: it must be online to do anything in Second Life, and group actions follow your Manage groups settings.
The API never moves L$. You can read a bot’s balance, but sending L$ stays on the dashboard’s Money Control page.
Keys & authentication
- Open Dashboard → Developer API and create a key (up to 10).
- Give it a name, tick only the permissions it needs, and pick All my bots or just some.
- Copy the key straight away — it’s shown once. We only keep a fingerprint of it.
Send it on every request in the X-Velvet-Key header. In LSL:
HTTP_CUSTOM_HEADER, "X-Velvet-Key", API_KEY| Permission | Lets the key |
|---|---|
| Send IMs | send instant messages from the bot |
| Local chat | make the bot speak in local chat, on any channel |
| Groups | list groups and roles, invite, eject, send group chat and notices |
| Movement | walk, teleport, sit and stand |
| Events | read what the bot heard and saw, and use a webhook |
| AI replies | reserved for VBL rules that use the AI Brain |
Any key can read its bots’ status, look up names and fire VBL rules. Revoke a key on the Developer page and it stops working on the next request.
Responses & errors
Every reply is one flat JSON object with ok. On an error it also has code and message, with a matching HTTP status:
{"ok": false, "code": "bot_offline", "message": "The bot must be online to send an IM."}| Status | Common codes |
|---|---|
| 400 | bad_json, bad_uuid, bad_message, message_too_long, bad_channel, bad_position, bad_role |
| 401 | bad_key — missing, wrong or revoked key |
| 403 | no_permission, invite_off, group_chat_off, premium_required |
| 404 | not_found (not your bot, or outside the key’s bots), bad_group |
| 409 | bot_offline |
| 429 | rate_limited |
llJsonGetValue(body, ["ok"]) returns the constant JSON_TRUE or JSON_FALSE, not the text "true". Compare with JSON_TRUE. For long replies (events), add HTTP_BODY_MAXLENGTH, 16384 to your request.Rate limits
| Basic bot | Premium bot | |
|---|---|---|
| Requests per bot per minute | 30 | 120 |
| Event polls per bot per minute | 120 | 240 |
Requests per key per minute that name no bot (/bots, /key, /events) | 60 (event polls 120) | |
Second Life also throttles IMs and group invites from one avatar, so spread bulk sends out.
Bots & status
{bot} in a path is the bot’s ID (like h1n1), not its Second Life name. Copy it from Dashboard › Developer API › Your bot IDs, or from the top of the bot’s own page.
GET /api/v1/key
GET /api/v1/bots
id, name, uuid, tier, online, region.GET /api/v1/bots/{bot}
online, state, region, sitting_on (what it’s sitting on now), seat (its saved sit spot) and balance (its L$, read-only; null until known).IM & local chat
POST /api/v1/bots/{bot}/im · permission: Send IMs
{"to": "<avatar uuid>", "message": "Your rent is due tomorrow"} — up to 1000 bytes.POST /api/v1/bots/{bot}/say · permission: Local chat
{"message": "Welcome!", "channel": 0} — channel is optional (0 = public chat); any other channel is for your own scripts to hear.Groups
Only groups switched on in Manage groups exist for the API. Basic bots keep their 3-group limit.
GET /api/v1/bots/{bot}/groups · permission: Groups
id, name, and whether invite, group_chat and can_notice are on.GET /api/v1/bots/{bot}/groups/{group}/roles · permission: Groups
id, name).POST /api/v1/bots/{bot}/groups/{group}/invite · permission: Groups
{"to": "<avatar uuid>", "role": "<role id, optional>"} — needs Invite ticked in Manage groups. No role = Everyone.POST /api/v1/bots/{bot}/groups/{group}/eject · permission: Groups
{"to": "<avatar uuid>"} — the bot’s role needs the Eject ability in that group.POST /api/v1/bots/{bot}/groups/{group}/notice · permission: Groups
{"subject": "Party!", "message": "Saturday 2pm SLT"} — Premium bots, one notice per group every 30 minutes, subject + message up to 510 characters.POST /api/v1/bots/{bot}/groups/{group}/chat · permission: Groups
{"message": "Hello everyone"} — needs Group chat ticked in Manage groups.Movement
POST /api/v1/bots/{bot}/walk · permission: Movement
{"x": 128, "y": 64, "z": 22, "run": false, "region": "optional"} — walks in its region, or into a neighbouring one.POST /api/v1/bots/{bot}/teleport · permission: Movement
{"region": "Region Name", "x": 128, "y": 128, "z": 25} — no region = same region.POST /api/v1/bots/{bot}/sit · permission: Movement
{"target": "<object uuid>"}POST /api/v1/bots/{bot}/stand · permission: Movement
Name to UUID
POST /api/v1/bots/{bot}/lookup
{"name": "jane.doe"} — searches Second Life through your (online) bot. Usually answers in the same call with ready: true and candidates (uuid, name); if not, poll GET /api/v1/bots/{bot}/lookup a few seconds later.Events
Once a key with Events covers a bot, VelvetBots keeps what that bot hears and sees for 7 days (up to 1000 per bot): IMs, local chat, group chat (groups with Group chat on), L$ payments it receives, and coming online / going offline.
GET /api/v1/events?since={last_id}&limit=25&types=im,payment · permission: Events
GET /api/v1/bots/{bot}/events?since={last_id} · permission: Events
{"ok": true, "last_id": 1042, "more": false, "events": [
{"id": 1041, "bot": "abc1", "type": "im", "at": "2026-10-18T12:00:00+00:00",
"from": "<uuid>", "name": "Jane Doe", "message": "Is unit 4 free?"},
{"id": 1042, "bot": "abc1", "type": "payment", "at": "...",
"from": "<uuid>", "name": "Jane Doe", "amount": 250, "txn": "...", "description": ""}
]}- Types:
im,local,group(addsgroup),payment,online(addsregion),offline. - Start without
since: you get the latest few and the currentlast_id. Then always pass the lastlast_idyou got. more: truemeans there are more waiting — ask again straight away.limitis 1–100 (default 25).- Poll every 10–15 seconds from LSL. A server can use a webhook instead.
Webhooks
Optional, for your own server. On the Developer page, add an HTTPS URL to a key with Events. Every event that key covers is POSTed there as the same JSON object as above, with these headers:
X-Velvet-Event | the event type (ping for the Send test button) |
X-Velvet-Event-Id | the event id — use it to ignore repeats |
X-Velvet-Signature | t=<unix time>,v1=<hex HMAC-SHA256 of "t.body" with your signing secret> |
- Answer with any 2xx within 5 seconds. Anything else is retried 3 more times (after 5 s, 30 s and 2 min).
- After 50 events in a row that couldn’t be delivered, the webhook pauses and the Developer page shows why. Press Save to turn it back on. Events stay pollable meanwhile.
- The URL must be a public HTTPS address. Redirects aren’t followed.
# Python: verify a webhook before trusting it
import hashlib, hmac, time
def verified(secret: str, raw_body: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, sig = int(parts["t"]), parts["v1"]
if abs(time.time() - t) > 300: # older than 5 minutes: reject
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)Fire a VBL rule
POST /api/v1/bots/{bot}/vbl/fire
{"event": "door", "avatar": "<uuid, optional>", "text": "optional"} — runs the bot’s VBL rules that start with when api "door". The rules’ own actions decide what happens.LSL example
IM whoever touches the object, from your bot:
string API_KEY = "vbk_..."; // Dashboard > Developer API
string BOT_ID = "your_bot_id";
default
{
touch_start(integer n)
{
llHTTPRequest("https://velvetbots.com/api/v1/bots/" + BOT_ID + "/im",
[HTTP_METHOD, "POST", HTTP_MIMETYPE, "application/json",
HTTP_CUSTOM_HEADER, "X-Velvet-Key", API_KEY],
llList2Json(JSON_OBJECT, ["to", (string)llDetectedKey(0),
"message", "Thanks for visiting!"]));
}
http_response(key req, integer status, list meta, string body)
{
// "ok" is a JSON boolean: compare with JSON_TRUE, never with "true".
if (llJsonGetValue(body, ["ok"]) != JSON_TRUE)
llOwnerSay("Failed: " + llJsonGetValue(body, ["message"]));
}
}LSL Starter Pack
Free ready-made scripts: group invites for renters and tenants, an IM relay, an events poller, a local chat listener, a group notice sender, a VBL rule trigger and a status sign. Get it from the Developer page (it’s delivered to your Second Life inventory) or on the Second Life Marketplace. Paste your key and bot ID (Dashboard › Developer API › Your bot IDs) into a script and it’s ready.