Cards
A card sits on a wisp’s line in a channel, or on an aside. It is built from a small set of blocks that Woosh draws natively. Nothing in a card is CSS or HTML: you choose a tone or a size, and Woosh makes it look right in every theme, with contrast checked once, by us. What you get in exchange for that is room to make really good things: boards, reels, keypads, shops, leaderboards, rank cards.
The desktop app and the web draw cards. The phone app does not yet: it shows the line’s text.
The shape
Section titled “The shape”{ "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" }] }, { "type": "actions", "actions": [{ "action": "spin", "label": "Spin again", "kind": "press", "tone": "accent" }] } ]}| Key | |
|---|---|
color |
the card’s edge, written #rrggbb, such as #f97316. Optional: left out, the edge is Woosh’s accent |
blocks |
one or more blocks, top to bottom |
A card holds at most:
- 40 parts: every block (those on pages too), and every cell, field, picture and action in them, counted together. A menu’s options are not parts.
- 4,000 characters of words, all of them together: texts, names and values, labels, cells, alt words.
- 8 KB of JSON.
A card past a limit, or with a part out of place, is refused, and w says what is wrong:
{"r":12,"e":"refused","w":"A grid has 1 to 5 rows of 1 to 5 cells."}A card goes on a channel’s line, not in a thread. A line with a card always carries text as
well: a notification shows it, and so does any app that does not draw cards. Say the same thing
in words.
Style is chosen, never written
Section titled “Style is chosen, never written”| Steps | Where | |
|---|---|---|
tone |
neutral, accent, good, warn, bad |
a text, a section, a progress bar, a grid cell, an action |
size |
small, normal, large, huge: steps of Woosh’s own type scale |
a text |
size |
small, normal, large |
a media picture |
Left out, a part is neutral and normal, and a progress bar is accent. On an action, accent
marks the main one and bad a risky one; good and warn draw it plain.
Each block holds only what its type does. Anything else is refused: a fields block with a
tone answers “A fields block holds no tone.”
The blocks
Section titled “The blocks”1 to 4,000 characters of words, with a few marks, and an optional size and tone.
{ "type": "text", "text": "**Win a monitor**", "size": "large" }{ "type": "text", "text": "Entries close {in:1792353600000000}. Good luck, {user:3f1c9a52-6d1e-4c8b-9a51-2b7e0c4d8f11}!", "tone": "accent" }| Mark | Written | Drawn |
|---|---|---|
| bold | **words** |
words |
| italic | *words* |
words |
| code | `words` |
words |
| a person | {user:<id>} |
@their name |
| a channel | {channel:<id>} |
#its-name |
| an emoji | :fire: (a standard emoji’s name), or the emoji itself |
the emoji |
| a time, counting | {in:<time>} |
“in 45s”, “in 5m”, “in 3h”, “in 2d”, counting down; “now” once it passes |
| a time, on the clock | {at:<time>} |
“20:00” today, “Mar 5 20:00” on another day, in the reader’s own time |
A time is in microseconds since 1970, as every time on the line. Ids are lowercase, as on the line. Marks do not nest, and an emoji name Woosh does not know stays as written. Times are drawn in the reader’s own language.
Marks are drawn in a text block and in a section’s text. Every other word on a card (a
field, a cell, a label) is drawn as written.
A title is a large text in bold; a footnote is a small one.
fields
Section titled “fields”1 to 10 name and value pairs, in columns: stats, bets, balances. A name is 1 to 256 characters,
a value 1 to 1,024.
{ "type": "fields", "fields": [{ "name": "Bet", "value": "50" }, { "name": "Coins", "value": "1,150" }] }1 to 5 rows of 1 to 5 cells. A cell shows one thing: words (text, 1 to 80 characters), an
emoji (1 to 64 characters), or a picture (url and alt). It may carry a tone and a
key. With an action it can be pressed, and only then may it carry a
value and disabled. Boards, reels, keypads, calendars, bingo.
{ "type": "grid", "rows": [ [{ "text": "X", "tone": "accent" }, { "text": "O" }, { "text": "·", "action": "move", "value": "3|XO..X...O" }], [{ "text": "·", "action": "move", "value": "4|XO..X...O" }, { "text": "X", "tone": "accent" }, { "text": "·", "action": "move", "value": "6|XO..X...O" }], [{ "text": "·", "action": "move", "value": "7|XO..X...O" }, { "text": "·", "action": "move", "value": "8|XO..X...O" }, { "text": "O" }] ]}A picture, still or animated (PNG, APNG, JPEG, WebP or GIF), that your wisp
uploaded, with alt words (1 to 256 characters) for people who cannot see it and an
optional size (small, normal, large). Art, prizes, rank cards your wisp renders.
{ "type": "media", "url": "<media>/wisps/d2b6f4a8-3e1c-4b7d-9a05-6c8e0f2a4b19/7c8d9eaf-b0c1-4d2e-8f30-4a5b6c7d8e93", "alt": "A 27 inch monitor, the prize", "size": "large"}gallery
Section titled “gallery”2 to 10 pictures together as items, each a url and its alt: screenshots, prizes.
{ "type": "gallery", "items": [ { "url": "<media>/wisps/d2b6f4a8-3e1c-4b7d-9a05-6c8e0f2a4b19/1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "alt": "First prize" }, { "url": "<media>/wisps/d2b6f4a8-3e1c-4b7d-9a05-6c8e0f2a4b19/2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e", "alt": "Second prize" } ]}section
Section titled “section”A text (1 to 4,000 characters, with marks) and, beside it, one picture (media: url and alt)
or one action, never both. An optional tone colours the text. List rows, a shop’s items.
{ "type": "section", "text": "**Golden reel**\nPays double for a day. 500 coins.", "action": { "action": "buy", "label": "Buy", "kind": "press", "tone": "accent", "value": "golden-reel" }}progress
Section titled “progress”A bar: value of max, where max is more than 0 and value is 0 to max. An optional label
(1 to 256 characters) sits above it, and an optional tone colours it. Quests, levels, a giveaway
filling.
{ "type": "progress", "value": 340, "max": 500, "label": "Level 7: 340 of 500", "tone": "good" }actions
Section titled “actions”A row of 1 to 5 actions.
{ "type": "actions", "actions": [ { "action": "spin", "label": "Spin", "kind": "press", "tone": "accent" }, { "action": "bet", "label": "Bet", "kind": "menu", "options": [{ "value": "10", "label": "10 coins" }, { "value": "50", "label": "50 coins" }] }, { "action": "enter", "label": "Enter", "kind": "tally" }, { "action": "rules", "label": "Rules", "kind": "link", "url": "https://lucky.example.com/rules" } ]}| Key | |
|---|---|
action |
your own name for it, which a press brings back: 1 to 32 of a to z, 0 to 9, _ and -, starting with a letter or a digit |
label |
the words on it, 1 to 80 characters |
kind |
press, menu, tally or link (below) |
tone |
optional |
value |
a press only, optional: 1 to 100 bytes of anything, brought back with the press |
disabled |
true: drawn switched off, and a press on it refused |
kind |
With | Pressing it |
|---|---|---|
press |
value? |
your wisp hears act, with the value |
menu |
options: 1 to 25, each a value (written as an action’s name: blackjack, bet-50) and a label (1 to 80 characters) |
your wisp hears act with the value chosen |
tally |
count? (Woosh’s own), tell? (true) |
Woosh counts one press a person, a second press takes it back, and draws the count within a second. Your wisp hears act only with tell. A card’s tallies each have a name of their own (Tallies) |
link |
url, an https address |
opens it. Nobody hears it |
2 to 10 pages, each a label (1 to 80 characters) and one or more blocks of its own, turned in
the app without asking your wisp. A page holds no pages. Leaderboards, help, shops.
{ "type": "pages", "pages": [ { "label": "1 to 3", "blocks": [{ "type": "fields", "fields": [{ "name": "1. ana", "value": "4,210 coins" }, { "name": "2. dani", "value": "3,980 coins" }, { "name": "3. bo", "value": "1,150 coins" }] }] }, { "label": "4 to 6", "blocks": [{ "type": "fields", "fields": [{ "name": "4. caio", "value": "990 coins" }] }] } ]}divider
Section titled “divider”Space between parts. It holds nothing.
{ "type": "divider" }Pressing
Section titled “Pressing”A pressable grid cell, a section’s action and an action in a row all press the same way: your
wisp hears act with the action and the value you gave it. A value is 1 to 100 bytes, and
yours to choose, so your wisp can keep a game’s state in its own cards and stay stateless: the
board above carries the square and the whole board in each cell’s value. Several parts may share
a name, as the board’s cells share move: a press is matched by its name and its value.
Presses and private cards covers the rest.
Pictures
Section titled “Pictures”Woosh fetches nothing from outside addresses: an outside picture would tell its host who looked. A card’s picture is one your wisp uploaded; an outside address is refused (“A card’s picture is a file the wisp uploaded.”). Upload it first with your wisp’s own token, the one thing besides buying that token your wisp does over HTTP:
POST https://api.woosh.no/api/wisps/filesAuthorization: Bearer <the wisp's token>Content-Type: multipart/form-data; boundary=…
(one part named "file", its own Content-Type image/png, image/jpeg, image/webp or image/gif){ "url": "<media>/wisps/d2b6f4a8-3e1c-4b7d-9a05-6c8e0f2a4b19/7c8d9eaf-b0c1-4d2e-8f30-4a5b6c7d8e93" }<media> stands for the media address your line’s ready names. Use the url
exactly as it comes back, as many times as you like: a media block, a gallery’s items, a
section’s media, or a grid cell’s url.
| Formats | PNG (APNG too, as image/png), JPEG, WebP, GIF |
| Size | up to 2 MB |
| Rate | two a second for each wisp |
| Answer | Why |
|---|---|
409 {"error":"A card's picture is a PNG (APNG too), JPEG, WebP or GIF.", …} |
the part says another type |
409 {"error":"A card's picture is up to 2 MB.", …} |
empty, or past 2 MB |
429, with Retry-After in seconds |
past two a second: upload again after it |
Keep pictures small: everyone looking loads each one once.
Motion is media the app plays
Section titled “Motion is media the app plays”An animated picture (an APNG, an animated WebP or a GIF) loops in the app at no cost on the network after its first load, and is cached for every card that uses it. A slot’s spinning reel is your own animated picture in a grid cell; the result is one edit. Never send frames as fast as you can: there is no need, and the edits budget would drop them.
Time is drawn by the app
Section titled “Time is drawn by the app”A time mark in text ({in:…}, {at:…}) is drawn by the app on its own clock, and a countdown
moves on by itself. A giveaway’s countdown costs nothing after the card is sent: no edits every
second.
Updates are patches
Section titled “Updates are patches”Any block or cell may carry a key of your choosing, written as an action’s name (reel-1,
purse), each once in a card. Wherever a line’s card can be sent again (an edit, a card.live
frame, a respond with edit), you may send a patch instead of the whole card: keys, and
what replaces each, a block for a block’s key and a cell for a cell’s. The new part takes the
key’s place and keeps the key; null takes it out. A key the card does not hold is refused (“The
card holds no part keyed ‘reel-4’.”), and the card the patch makes is held to every rule above.
The slots machine, with its parts keyed:
{ "color": "#f97316", "blocks": [ { "type": "text", "text": "**bo's slots**", "size": "large" }, { "type": "grid", "rows": [[{ "emoji": "❔", "key": "reel-1" }, { "emoji": "❔", "key": "reel-2" }, { "emoji": "❔", "key": "reel-3" }]] }, { "type": "fields", "key": "purse", "fields": [{ "name": "Bet", "value": "10" }, { "name": "Coins", "value": "1,000" }] }, { "type": "actions", "key": "buttons", "actions": [ { "action": "spin", "label": "Spin", "kind": "press", "tone": "accent" }, { "action": "cashout", "label": "Cash out", "kind": "press", "tone": "bad" } ] } ]}bo presses Spin. One frame starts the reels turning and switches the buttons off:
{ "i": 68, "t": "card.live", "d": { "message": "0199dea4-1d31-7b4c-9d5e-6f7a8b9c0d21", "patch": { "reel-1": { "url": "<media>/wisps/d2b6f4a8-3e1c-4b7d-9a05-6c8e0f2a4b19/3c4d5e6f-7a8b-4c9d-8e0f-2a3b4c5d6e7f", "alt": "Spinning" }, "reel-2": { "url": "<media>/wisps/d2b6f4a8-3e1c-4b7d-9a05-6c8e0f2a4b19/3c4d5e6f-7a8b-4c9d-8e0f-2a3b4c5d6e7f", "alt": "Spinning" }, "reel-3": { "url": "<media>/wisps/d2b6f4a8-3e1c-4b7d-9a05-6c8e0f2a4b19/3c4d5e6f-7a8b-4c9d-8e0f-2a3b4c5d6e7f", "alt": "Spinning" }, "buttons": { "type": "actions", "actions": [ { "action": "spin", "label": "Spin", "kind": "press", "tone": "accent", "disabled": true }, { "action": "cashout", "label": "Cash out", "kind": "press", "tone": "bad", "disabled": true } ] } } }}Then one answer lands the result and switches them on again:
{ "i": 69, "t": "respond", "d": { "to": "0199dea4-2e42-7c5d-ae6f-7a8b9c0d1e32", "text": "cherry, cherry, lemon: bo wins 20, 1,010 coins.", "patch": { "reel-1": { "emoji": "🍒" }, "reel-2": { "emoji": "🍒" }, "reel-3": { "emoji": "🍋" }, "purse": { "type": "fields", "fields": [{ "name": "Bet", "value": "10" }, { "name": "Coins", "value": "1,010" }] }, "buttons": { "type": "actions", "actions": [ { "action": "spin", "label": "Spin", "kind": "press", "tone": "accent" }, { "action": "cashout", "label": "Cash out", "kind": "press", "tone": "bad" } ] } }, "edit": true }}A frame that changes three cells is three cells long. For an edit, Woosh merges the patch into
the card as stored, and the people looking get the whole card. For a frame, Woosh merges it onto
the card as it shows now, to check presses against, and the people looking get the patch and
merge it into the card on screen (an app that does not hold a key passes it over).
An edit’s patch starts from the stored card, never from a frame: patch again every part your
frames changed, as the answer above does with the reels and the buttons. To change the edge’s
color, send the whole card.
A private card is always sent whole, never patched: one person looks, and Woosh keeps no card of theirs to patch, only what it offers to press.
What it costs
Section titled “What it costs”A card is stored once with its line (at most 8 KB) and sent once to each person looking. Pictures are fetched once and cached. A patch frame is tens of bytes. Countdowns, pages and animation cost the network nothing. That is why the limits are where they are, and why a busy channel full of cards stays fast.
© 2026 Woosh AS. All rights reserved.