Skip to content

Commands

A command is something a person types after a /: /spin 50, /balance, /giveaway. Once an admin grants your wisp commands, the apps offer its commands, with their options, in every channel its grant names.

PUT /api/wisps/{id}/commands, with your own token as the wisp’s owner, sets the whole list each time. Your wisp’s own token cannot set it: you send it as yourself, by hand or from your deploy script.

[
{
"name": "spin",
"description": "Spin the reels",
"options": [
{ "name": "bet", "kind": "number", "description": "Coins to bet" },
{
"name": "machine",
"kind": "choice",
"description": "Which machine",
"choices": [{ "value": "classic", "label": "Classic" }, { "value": "gems", "label": "Gems" }]
}
]
},
{ "name": "balance", "description": "How many coins you have" },
{
"name": "gift",
"description": "Give someone coins",
"options": [
{ "name": "to", "kind": "member", "required": true },
{ "name": "amount", "kind": "number", "required": true }
]
}
]
Key
name 1 to 32 lowercase letters, digits and -, starting with a letter or a digit; one of its own
description 1 to 100 characters, shown in the picker
options up to 10, in the order the picker asks for them

An option:

Key
name as a command’s name; one of its own within the command
kind text, number, member, channel, role or choice
description 1 to 100 characters, optional
required true when the command cannot run without it
choices for a choice only, and required there: 1 to 25, each a value and a label of 1 to 100 characters

A wisp has up to 50 commands, and one list serves every server it is in. An empty list takes them all away. The answer is the list Woosh now holds. Sending the same list again changes nothing and tells nobody, so setting it on every deploy is fine.

A list that breaks a rule is not set: it answers 409 with the rule in error, for example {"error":"A command's name is up to 32 lowercase letters, digits and -.","traceId":"…"}, or 400 for a body Woosh cannot read. A wisp that is not yours answers 404, “No such wisp”.

The apps read a server’s commands from GET /api/servers/{id}/commands, which any member of the server may read. It lists each wisp there with commands granted: its wisp id, name, avatarUrl, channels (where it may be asked; null for every channel) and commands.

When that list changes, the server hears commands, and the apps read it again the next time someone types / there. That happens when you set a new list, when your wisp is added to the server or taken out, and when its grant there changes. A command you add can be run at once. Your wisp does not hear commands: it knows its own list.

When a person runs a command, Woosh checks it before your wisp hears a thing: that they are in the server, that your wisp is there with commands granted in that channel, that the command exists, and that the options fit (every required one there, none it does not have, each of its kind). A person may run commands and press wisps’ cards at most 3 times a second in all; past that, they are asked to wait and your wisp hears nothing.

Then your line hears command:

{
"c": 61,
"n": 109,
"t": "command",
"d": {
"server": "5c3b1a2d-8e4f-4a6b-9c7d-0e1f2a3b4c55",
"channel": "6d4c2b3e-9f5a-4b7c-8d6e-1f2a3b4c5d66",
"id": "0199dea4-1c20-7a3b-8c4d-5e6f7a8b9c10",
"user": "3f1c9a52-6d1e-4c8b-9a51-2b7e0c4d8f11",
"name": "ana",
"avatar": "639201477315520417",
"command": "spin",
"options": {"bet":50},
"expires": 1791916215000000
}
}
Key
server, channel where it was run
id the run: what you answer
user, name?, avatar? who ran it (Avatars)
command its name
options? by name, as the person gave them: text as text (1 to 1,000 characters), a number as a number, a member, channel or role as its id, a choice as its value. Left out when none were given
expires when your chance to answer ends: 15 seconds after the person ran it, by Woosh’s clock

bo asks for his balance, a command with no options:

{"c":61,"n":110,"t":"command","d":{"server":"5c3b1a2d-8e4f-4a6b-9c7d-0e1f2a3b4c55","channel":"6d4c2b3e-9f5a-4b7c-8d6e-1f2a3b4c5d66","id":"0199dea4-1e41-7b4c-9d5e-6f7a8b9c0d22","user":"7a2e4c19-0b3d-4f6a-8e25-91c3d5f0a622","name":"bo","avatar":"fox","command":"balance","expires":1791916230000000}}

Options are a person’s input: check that a number is in range, and look an id up before you act on it.

The person’s app shows your wisp working on it at once (“lucky is working on /spin”), so there is no dead time to fill. If no answer comes within the 15 seconds, it says “lucky didn’t answer”.

Answer every run once, before expires, with respond:

d
to the run’s id
text up to 4,000 characters. A line always has it, even with a card
card a card
private true: an aside to the person alone, instead of a line in the channel

A press is answered with respond too, where edit and patch can move its card on in place: Answering a press.

{
"i": 64,
"t": "respond",
"d": {
"to": "0199dea4-1c20-7a3b-8c4d-5e6f7a8b9c10",
"text": "ana bet 50: cherry, cherry, lemon. No win this time.",
"card": {
"color": "#f97316",
"blocks": [
{ "type": "text", "text": "**Spin**", "size": "large" },
{ "type": "grid", "rows": [[{ "emoji": "🍒" }, { "emoji": "🍒" }, { "emoji": "🍋" }]] },
{ "type": "fields", "fields": [{ "name": "Bet", "value": "50" }, { "name": "Balance", "value": "1,150" }] }
]
}
}
}
{"r":64,"v":"0199dea4-1d31-7b4c-9d5e-6f7a8b9c0d21"}

v is the line’s id. Everyone looking at the channel hears it as a message, with the run on it, which the apps draw above your answer as “ana used /spin”:

"run": {"id":"0199dea4-1c20-7a3b-8c4d-5e6f7a8b9c10","user":"3f1c9a52-6d1e-4c8b-9a51-2b7e0c4d8f11","name":"ana","command":"spin"}

The line needs post granted in the channel. It always carries text, even with a card: it is what a build that does not draw cards shows, and what a notification says.

With private, the answer goes to the person alone:

{"i":65,"t":"respond","d":{"to":"0199dea4-1e41-7b4c-9d5e-6f7a8b9c0d22","text":"You have 1,150 coins.","private":true}}
{"r":65,"v":"0199dea4-1e41-7b4c-9d5e-6f7a8b9c0d22"}

v is the aside’s id, which is the run’s own: an aside goes under the run it answers. bo’s apps hear it as an aside:

{"t":"aside","d":{"id":"0199dea4-1e41-7b4c-9d5e-6f7a8b9c0d22","channel":"6d4c2b3e-9f5a-4b7c-8d6e-1f2a3b4c5d66","author":"d2b6f4a8-3e1c-4b7d-9a05-6c8e0f2a4b19","name":"lucky","avatar":"639201499012345678","to":"0199dea4-1e41-7b4c-9d5e-6f7a8b9c0d22","text":"You have 1,150 coins.","at":1791916216405000}}

Each of his apps that is open shows it in the channel, marked “Only you can see this”, and he can dismiss it. An aside is kept nowhere, not even for a reconnect: an app that was closed never sees it, and it is gone at his next load. Your wisp does not hear it either. Use it for what is nobody else’s business (a balance, a private game, a “you cannot do that”). It may carry text, a card, or both; a private card has no tally. Presses and private cards shows how a private card moves on.

An answer spends none of the server’s calls and none of the channel’s lines: the person paid for the run with their own budget, and a run has one answer. It still counts toward your line’s own calls a second (Budgets and limits).

Answer Why
{"r":69,"e":"refused","w":"Already answered"} the run was answered already, another way
{"r":67,"e":"refused","w":"Too late"} past expires: the person’s app already says your wisp didn’t answer
{"r":…,"e":"refused","w":"No such run or press"} not a run of your wisp’s, or more than a minute old
{"r":…,"e":"refused","w":"Not granted"} a line where your wisp may not post, or a server it is no longer in
{"r":…,"e":"refused","w":"An answer holds words or a card."} neither text nor card
{"r":…,"e":"limited","w":"Past the line's calls a second","wait":200} past your line’s calls a second: send the same answer again after wait ms

Sending the same answer again is always safe, so answer a failed again the same way. A run never takes two answers: until expires, a retry of one that went through is told the same v, and nothing is posted twice. An answer that came back limited, or refused for any reason but the first three above, did nothing: the run is still yours to answer until expires.

Fifteen seconds is for the first answer, not the whole job. Answer at once with a line that says what is happening, then edit that line (its id is the answer’s v) when the work is done, or send more lines after it:

const message = await call("respond", { to: run.id, text: "Drawing a winner..." });
const winner = await drawWinner(); // as long as it takes
await call("edit", { message, text: `The winner is ${winner.name}!` });

An aside cannot be edited later: only a press on its card moves it on. Acting in a server covers send and edit.

© 2026 Woosh AS. All rights reserved.