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.
Registering
Section titled “Registering”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”.
In people’s apps
Section titled “In people’s apps”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”.
Answering
Section titled “Answering”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.
A line in the channel
Section titled “A line in the channel”{ "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.
Privately: an aside
Section titled “Privately: an aside”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.
What it spends
Section titled “What it spends”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).
What can go wrong
Section titled “What can go wrong”| 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.
Work that takes longer
Section titled “Work that takes longer”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 takesawait 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.