Cards
A card is a small, native piece of content: a title, text, facts, images and buttons. Cards are how your app answers. There is no HTML, script or custom layout, so a card looks right in every room and in notifications.
Cards come from three places: a webhook's reply, a room bot's post, and a web app's postCard. The rules
are the same for all three.
{
"type": "card",
"visibility": "room",
"card": {
"blocks": [
{ "type": "title", "text": "Build #1432 passed" },
{ "type": "status", "text": "Deployed", "tone": "ok" },
{ "type": "text", "text": "**main** is live on production. *No action needed.*" },
{ "type": "facts", "items": [ { "k": "Commit", "v": "a41d07e" }, { "k": "Duration", "v": "4 min 12 s" } ] },
{ "type": "divider" },
{ "type": "image", "url": "https://cdn.ci.example.com/graphs/1432.png", "alt": "Build time over the last week" }
],
"actions": [
{ "type": "open_url", "label": "Open build", "url": "https://ci.example.com/builds/1432" },
{ "type": "submit", "label": "Roll back", "style": "danger", "actionId": "rollback", "payload": { "build": 1432 } }
]
}
}
Blocks
A card has 1 to 12 blocks, drawn in order.
| Block | Fields | Rules |
|---|---|---|
title | text | Up to 80 characters. At most one, and it must be the first block. |
text | text | Up to 1000 characters. **bold** and
*italic* only. Links go in open_url actions. |
facts | items: [{"k", "v"}] | 1 to 10 items. Keys up to 20 characters, values up to 120. |
image | url, alt | Up to 10 per card. https on a
host in allowedImageHosts. alt is required, up to 96 characters. Two or more images in a
row are drawn as a row of thumbnails; a single image is drawn full width. Phones load images directly, the
platform never fetches them. |
status | text, tone | Up to 30 characters, at most one.
tone is neutral, ok, warn or error. |
divider | A separator line. | |
qr | value, caption? | A QR code the phone draws from
value (up to 512 characters), with an optional caption under it (up to 80). At most one
per card. See QR codes. |
room | roomId, name | A public room, drawn with its picture, its name and a Join or Open button. At most one per card. See Rooms. |
QR codes
A qr block is a code for someone to scan: a ticket at the door, a booking at a desk, a voucher at a till.
You send only the text to encode; the phone draws the code itself, black on white, large enough to scan from its
screen. Nothing is downloaded, so it works offline, in end-to-end encrypted rooms, and without an
allowedImageHosts entry, and your server does not learn when the user opens it.
{
"blocks": [
{ "type": "title", "text": "Coffee voucher" },
{ "type": "facts", "items": [ { "k": "Valid until", "v": "31 Oct" }, { "k": "Worth", "v": "1 flat white" } ] },
{ "type": "qr", "value": "VOUCHER:7Q4K-2XPA-91LM", "caption": "Show this at the counter" }
]
}
- What to encode. Anything your own scanner reads back: a code, a signed token, an
httpslink to your check-in page. The platform does not read, check or expire it; checking it at the door, and refusing it the second time, is your backend's job. Make it impossible to guess: include a random part, or sign it, rather than a seat number alone. - Who sees it. A code in a
privatecard (a receipt, say) is seen only by the user it was sent to. A code in a card posted into a room is seen by every member, so do not put a personal ticket in aroomcard. - Where the text version goes. Push previews and the public web portal show a card as text; for a
qrblock that is itscaptiononly, never the value. - For a whole worked example, from selling a ticket to scanning it, see Payments.
Rooms
A room block points people at a public room: one your app created, or one it follows. The phone shows
the room's picture and name, and a Join button for people who are not in it yet, or
Open for those who are.
{
"blocks": [
{ "type": "title", "text": "Following Hacker News" },
{ "type": "text", "text": "A public room now posts every new item of this feed." },
{ "type": "room", "roomId": "2bTBIYWDF7nl1mcJ2KRbOSIuM16", "name": "Hacker News" }
]
}
- Public rooms only. The room must be a public group. A card that points at a private room, a direct chat or a room that does not exist is refused like any other invalid card.
- The name is the room's own. Before the card is signed, the platform replaces
namewith the room's real name (up to 120 characters), so a card cannot show a room under another name. - Where the text version goes. Push previews and the public web portal show the room's name.
Actions
Up to 6 buttons. Each has a label of up to 24 characters and an optional style:
default, primary or danger.
type | Fields | When tapped |
|---|---|---|
open_flow | flowId, payload? | Starts that flow of your
manifest on the tapper's phone. Payload keys that are input ids of the flow pre-fill those inputs. The whole
payload reaches your webhook as payload, next to the answers, so it can carry things the user is
not asked, like an order number. See Flow payload. |
submit | actionId (up to 64 characters), payload? | Calls your
webhook once, as the tapper, with action: {"id", "payload"}. See
Card actions. |
open_url | url | Shows the address and, once the user confirms, opens it
in the browser. https on a host in allowedLinkHosts. |
open_app | appId?, path?, payload? | Opens a web
app, this one unless appId names another. path starts with / and is
added to the app's URL. The payload reaches the page through
getContext(). |
pay | amount, currency, reference (up to 120),
description (up to 200) | Asks the tapper to pay your app that amount: a native confirmation
sheet, then a signed payment.completed event to your webhook. Needs the payments scope
in the manifest. The amount is a decimal string such as "5.00"; the reference must be unique per
user, so the same card can be tapped by everyone in the room. See
Payments. |
A payload is a JSON object of up to 2048 characters. Actions always run as the person who taps them, not as the person who posted the card. Someone who taps a button without the app installed is offered to install it first.
Who sees a card
"visibility": "private"(the default): only the person who ran the command sees it, and it is not stored in the room."visibility": "room": the card is offered for the room the command ran in. The user sees a preview and confirms before it is posted as their message. This needs thepost.cardsscope; without it the card is shown privately.
Card templates
Instead of building the card in every reply, declare it once in the manifest's cards and fill it in.
Template ids use lower-case letters, digits, dashes and underscores, up to 32 characters.
"cards": {
"order_open": {
"blocks": [ { "type": "title", "text": "Lunch from {{restaurant}}" } ],
"actions": [ { "type": "submit", "label": "Close order", "actionId": "close", "payload": { "order": "{{order}}" } } ]
}
}
A reply then names the template and gives the values:
{ "type": "card", "visibility": "room", "card": { "template": "order_open", "values": { "restaurant": "Bangkok Kitchen", "order": "4821" } } }
Every {{name}} in any text of the template is replaced with that value as plain text. Names use
letters, digits, dots and underscores. A name with no value becomes empty. The filled-in card is checked like any
other, including the hosts of image and link URLs that contain placeholders.
What the platform adds
- A header with your app's icon and name, and who posted the card. You cannot change it.
- A signature. The platform signs every card, and phones check it, so a card cannot be forged or altered after your app made it. A card that fails the check is shown without its buttons.
- A plain-text version, for notifications and for public room pages on the web: the title, then each fact as
key: value, then the text and status. Buttons never appear there, so make sure the text makes sense on its own.