Guide
Bots & command sync
A creator running four tools should still have one botin their chat, not four. This page is how your app connects to that shared bot, publishes its commands into it without stepping on anyone else's, and — if the creator makes it a moderator — acts on chat. Every endpoint, field and limit below is read out of the live API.
The one thing to understand first
A Velora bot is not a second account that a creator logs into and shares a password with. It is a bot identity owned by their existing account, which they authorize into a channel with a specific permission set they can revoke at any time — and which multiple apps can connect to at once.
That last part matters most for integrators. A creator running your tool alongside two others does not end up with three bots in their chat. They have one bot, and each app publishes its own commands into it. Velora tracks which app owns which command, so nothing collides silently and disconnecting one app never disturbs another.
| Concern | How Velora handles it |
|---|---|
| Second account for the bot | Not needed. The bot is an identity owned by the user's account. |
| Sharing credentials with a tool | Never. Apps connect through OAuth; no password is exchanged. |
| Getting a bot out of a channel | The channel owner revokes it themselves. No support ticket. |
| Limiting what a bot may do | Four explicit permissions, granted per channel. |
| Several tools, one chat | One shared bot. Commands are namespaced per app. |
| Two apps claiming the same command | Deterministic override with automatic restore. See below. |
The four moving parts
1. The bot identity
A bot has its own unique username, display name and avatar, and is owned by exactly one user account. A user may own unlimited bots, but only one of them can have Velora Bot Studio (VBS) features — the built-in commands, timers, variables and event triggers — enabled at a time. Bots without VBS are just identities your app drives.
2. Channel permission — moderator role
A bot moderates by being granted the moderator role in a channel, exactly like a human moderator, by the broadcaster. There is no separate bot-permission system. Covered in full under Moderating chat below.
3. The app connection
When a creator connects your app to one of their bots, Velora records the link between your client_id and that bot, along with who connected it, whether it is active, when it was last used and how many commands you currently have synced. Several apps can hold an active connection to the same bot at the same time. Each connection is independent — disconnecting yours leaves the others alone.
4. Synced commands
Your app publishes its command list to Velora so the bot's profile page can show viewers what the bot actually responds to. Commands are stored per (bot, app) pair, so the platform always knows which tool owns which trigger.
Connecting your app to a bot
All endpoints below are called with the user's OAuth token, so they run in your app's context automatically — you never pass your client_id in the body.
/api/integrations/oauth/bot/availableevery bot this user owns and could connectShow this list and let the creator choose. It includes their VBS bot and any bots created by other tools — picking an existing one is how they end up with a single shared bot rather than one per app.
/api/integrations/oauth/bot/currentthe bot currently connected to your app for this userCall this on startup. If it returns a connection, you are already set up and should not prompt the creator again.
/api/integrations/oauth/bot/selectconnect an existing bot{ "botId": "b3f1c2a4-..." }/api/integrations/oauth/bot/createcreate a new bot and connect it{
"botName": "CoolBot", // required, 3-30 chars, [a-zA-Z0-9_] only
"description": "Does cool things", // optional, max 500 chars
"avatarUrl": "https://..." // optional; see avatar upload below
}Returns 409 if the name is taken. Always check first with GET /api/integrations/oauth/bot/check-name/:botName.
Prefer “select” over “create”. Offer the existing-bot list first and treat creating a new one as the fallback. A creator with four tools should still have one bot in their chat.
/api/integrations/oauth/bot/connectionslist this user's bot connections/api/integrations/oauth/bot/disconnectdisconnect your app from its botDisconnecting removes your synced commands and restores any built-in commands your app had overridden. It does not delete the bot and does not affect other apps.
Command sync
/api/integrations/oauth/bot/commands/syncpublish your full command listRequires the bot:commands scope. This is a full replacementfor your app's commands on that bot: send the complete list every time. Triggers you omit are removed. Other apps' commands are untouched.
POST /api/integrations/oauth/bot/commands/sync
Authorization: Bearer <user access token>
{
"botId": "b3f1c2a4-...",
"commands": [
{
"trigger": "uptime",
"description": "How long the stream has been live",
"aliases": ["up"],
"permissions": { "level": "everyone" },
"cooldown": { "globalCooldown": 5, "userCooldown": 15 },
"isPublic": true
}
]
}Field reference
| Field | Rules |
|---|---|
trigger | Required, max 50 chars. A leading ! is stripped, and matching is case-insensitive — send uptime or !uptime, both land the same. |
description | Optional, max 500 chars. Shown on the bot's public profile. |
aliases | Optional, up to 20 strings. |
permissions.level | One of everyone, followers, subscribers, tier1, tier2, tier3, vips, moderators, streamer_only. Optional followerAgeMinutes for follower-gated commands. |
cooldown | globalCooldown and userCooldown, seconds, 0–86400. |
isPublic | Defaults to true. Set false to keep a command working but hidden from the public list. |
Up to 500 commands per sync. Duplicate triggers inside a single payload are rejected with 400 — deduplicate before sending.
Creator-written descriptions are never overwritten. If a creator edits the description of one of your commands on their bot page, that edit survives every subsequent sync. Treat description as your default, not your property.
When your command collides with a built-in
If you sync a trigger that the bot already has as a Velora Bot Studio command, the built-in is deactivated and marked as overridden by your app— yours wins, and the creator's original definition is preserved rather than deleted.
When you later drop that trigger from your sync, or disconnect entirely, Velora restores the built-in automatically — but only if no other connected app is still syncing the same trigger. Nothing is silently lost, and no creator has to go re-enable anything by hand.
The response tells you exactly what happened:
{
"success": true,
"synced": { "total": 12 },
"overrides": {
"vbsCommandsDisabled": 1,
"vbsCommandsRestored": 0
}
}Surfacing vbsCommandsDisabled in your own UI is a kindness — it lets a creator see that your !uptime replaced theirs, instead of wondering why theirs stopped answering.
What a bot can actually do
The short version: read chat, talk in chat, moderate chat, and run commands gated by viewer role — plus react to everything happening on the channel through the Events API.
| You want to… | Use | Needs |
|---|---|---|
| Read every message as it arrives | Chat WebSocket | chat:read |
| Reply, announce, respond to a command | Chat WebSocket / send endpoint | chat:write + a connected bot |
| Delete a single message | moderate · action: "delete" + messageId | chat:moderate + mod role |
| Time a viewer out | moderate · action: "timeout" | chat:moderate + mod role |
| Ban / unban | moderate · action: "ban" / "unban" | chat:moderate + mod role |
| Mods-only or subs-only commands | permissions.level on the synced command | Nothing extra — Velora enforces it |
| React to follows, subs, raids, redemptions | Events API | Event scopes |
Two independent gates, and both matter. Moderation is gated on who your bot is in that channel (the moderator role). Commands are gated on who the viewer is(their role in chat). A mods-only command still runs under the creator's own rules even if your bot is not itself a moderator — the two systems do not depend on each other.
Moderating chat
Yes, a bot can moderate — and the way it gets that power is deliberately the same way a human does. There is no separate bot-permission system to learn: a bot is granted the moderator role in a channel, exactly like any other moderator, by the broadcaster.
Every moderation call is checked against that role at request time. The broadcaster always passes; anyone else must hold the moderator role in that specific channel. A bot with the role in one channel has no power in any other.
| Capability | Scope | What it needs |
|---|---|---|
| Read chat | chat:read | Nothing extra — see the Chat WebSocket docs. |
| Send messages | chat:write | A connected bot to send as. |
| Delete · timeout · ban · unban | chat:moderate | Moderator role in that channel, granted by the broadcaster. Otherwise 403. |
Both halves are required for moderation: your app needs the chat:moderate OAuth scope and the acting account needs the moderator role. A scope alone grants nothing — that is the point. A creator who authorizes your app has not thereby handed you their chat; they still have to make the bot a mod, and they can un-mod it the moment they want to, from the same place they manage every other moderator.
/api/integrations/oauth/chat/channels/:channelId/moderatedelete · timeout · ban · unban{
"action": "timeout", // delete | timeout | ban | unban
"targetUsername": "someviewer", // or targetUserId
"durationSeconds": 600, // timeout only; default 600
"reason": "spam", // optional
"messageId": "..." // delete only
}403 if the acting account is not a moderator or the broadcaster · 400 on an unknown action, a missing target, or delete without messageId.
Moderation actions are attributed to the bot, not to your app.A timeout issued through your integration shows the bot's name in chat, so viewers see a consistent identity and moderators can tell which tool acted. Name your bot accordingly.
If a moderation call returns 403, do not retry it.The account simply is not a moderator there. Surface it to the creator as “make the bot a moderator in your channel” — that is a thing they can fix in ten seconds, and a retry loop will never fix it for them.
Gating a command by viewer role
Separately from moderation, every command carries its own permission level, enforced by Velora before the command runs. Set it in the permissions field when you sync (see Command sync above):
| Level | Who can run it |
|---|---|
everyone | Anyone in chat. |
followers | Followers. Add followerAgeMinutes to require a minimum follow age — the standard defence against a fresh account spamming a command. |
subscribers | Any active subscriber. |
tier1 · tier2 · tier3 | Subscribers at that tier or above — tier2 also admits tier 3. |
vips | VIPs and moderators (mods are never locked out of a VIP command). |
moderators | Moderators and the broadcaster. |
streamer_only | The broadcaster alone. |
An unrecognised level is denied, not allowed — if you send a typo, the command stops working rather than opening up to everyone. Pair the level with cooldown for rate control; the two are independent.
Bot names
Bot names live in the same namespace as user accounts. A bot named CoolBot means no user can register CoolBot, and vice versa. This is deliberate: it guarantees that a name in chat always identifies exactly one entity.
| Rule | Detail |
|---|---|
| Length | 3–30 characters |
| Allowed characters | Letters, numbers and underscore only — [a-zA-Z0-9_]. No spaces, dots or dashes. |
| Uniqueness | Case-insensitive across bots and users. CoolBot and coolbot are the same name. |
| Display casing | Preserved as typed. The creator sees CoolBot even though the unique key is lowercase. |
/api/integrations/oauth/bot/check-name/:botNamecheck availability before creating{ "available": true, "botName": "coolbot" }Check as the creator types, not just on submit. Hitting 409 after they picked an avatar and a description is a bad first experience with your integration.
Renaming is limited to once every 90 days
A bot that people recognise in chat should not be able to change identity on a whim, so bot renames are on a 90-day cooldown. Attempting a rename inside that window fails with a clear error rather than silently succeeding.
/api/bots/:id/username-cooldowncheck whether a rename is currently allowed{
"canChange": false,
"lastChangeAt": "2026-01-01T10:30:00Z",
"nextChangeAt": "2026-04-01T10:30:00Z",
"daysRemaining": 45
}If your app offers renaming, call this before showing the field and surface daysRemainingwhen it is locked. A disabled input with “available in 45 days” is a far better experience than an error after they have typed a new name.
Bot avatars
/api/integrations/oauth/bot/:botId/avatarupload a custom avatarmultipart/form-data with a single file field. The caller must own the bot.
POST /api/integrations/oauth/bot/b3f1c2a4-.../avatar
Content-Type: multipart/form-data
file: <binary>
→ 200
{
"success": true,
"bot": {
"id": "b3f1c2a4-...",
"username": "coolbot",
"displayName": "CoolBot",
"avatarUrl": "https://assets.velora.tv/bot-avatars/b3f1c2a4-.../a1b2.png"
}
}| Constraint | Value |
|---|---|
| Formats | JPEG, PNG, GIF, WebP. Anything else returns 400. |
| Max size | 5 MB |
| Storage | Served from assets.velora.tv under a per-bot path with a random filename. |
| Errors | 400 bad type or size · 404 not your bot · 503 storage unavailable |
Each upload gets a fresh randomised filename, so the returned avatarUrl changes every time and is safe to cache indefinitely. Always store the URL you get back rather than constructing one.
You can also pass an avatarUrlwhen creating a bot if you already host the image. Uploading is preferred — it survives your CDN going away, and it keeps the bot looking right even if a creator later stops using your tool.
Where a bot appears to viewers
A bot is a visible identity on Velora, not an invisible API caller. Knowing where it shows up tells you what your synced metadata is actually for.
In chat
With its display name, avatar and a bot badge, so viewers can always tell a bot from a person.
On its profile page
Avatar, description, and its full command list, grouped by the app that published each one. This is where your description and permissions fields are read by humans.
On the viewer card
Clicking a bot in chat shows a bot-specific card: no follow button, no gift-sub button, no subscription actions. A bot is not a creator and Velora does not pretend otherwise.
Because commands are grouped by app on the profile page, a creator can see at a glance that !uptime came from your tool and !rafflecame from another. Write descriptions for that reader — a viewer deciding whether to type the command — not for your own internal docs.
What stays in the creator's hands
Creators manage their own bot from their Velora dashboard: profile and avatar, built-in commands, timed messages, variables, event triggers and shoutout overrides. Your app does not need to reimplement any of that, and generally shouldn't.
The dividing line is simple. You own your commands; the creator owns the bot. They can rename it, re-avatar it, revoke it from a channel, or disconnect your app entirely — without asking you, and without breaking the other tools connected to it.
Being a good citizen in a shared bot
Offer the existing-bot list before offering to create one.
The whole point is one bot per creator, not one per tool.
Sync the complete list every time.
Partial syncs delete the commands you left out.
Sync on change, not on a timer.
There is no reason to re-push an unchanged list every minute.
Do not claim generic triggers you do not implement.
Every trigger you sync is one another app cannot own, and may silently override a creator’s own command.
Disconnect cleanly.
Calling disconnect restores whatever you overrode. Leaving a stale connection behind holds triggers hostage.
Show the override count.
If your sync disabled a creator’s built-in command, tell them.