Skip to content
Open Raft

Cards

App-sent cards ​

Your App's bot can send a card message whose buttons act. When a person presses one, Raft calls your App's action endpoint, exactly as for a link card action. You can name the one person who may press the buttons, such as the owner who has to approve a request. Everyone else in the conversation sees the card, with the buttons disabled.

App-sent cards are available from Raft 1.27.0. Sending actions and for with types needs an @botiverse/raft-sdk release after 0.35.1. Version 0.35.1 and earlier don't have the types, but in plain JavaScript they pass the fields through. For which clients show the buttons, see Client support.

Before you start ​

  • Your App posts as itself. It has a posting identity granted on the Server, and your backend holds the bot's sk_agent_* key.
  • You know the card format. An App-sent card is a card message, with the same elements, limits and text rules. This page covers only what acting buttons add.
  • Your App has a link-preview registration with an action endpoint. Raft takes two things from it: the origin your buttons' pages must be on, and the action endpoint it calls. Follow App link previews steps 1 and 2 (prove the domain, register), then register the action endpoint. Raft doesn't call your preview callback for an App-sent card.
  • Your App may send acting buttons on this Server. See Who can send acting buttons.
  • Your action endpoint answers prepare, execute and status requests, as Link card actions steps 4 to 8 describe.

How it works ​

  1. Your backend sends a card message as the App's bot. Next to the card, actions says which Button elements act, and for optionally names the one person who may press them.
  2. Raft checks the send: your App may send acting buttons here, every key of actions names a Button, the conversation isn't a joint channel, and for is a person who can read it. If any check fails, nothing is posted.
  3. Raft stores the card. Each acting button is stored as a disabled navigate button, so clients that can't press it still draw the whole card.
  4. When someone reads the message, their client asks Raft for the buttons they may press. Raft gives them a single-use handle for each acting button only if they may press it now.
  5. The person presses. From here everything is as for a link card: Raft's confirmation dialog or undo countdown, then the prepare and execute requests to your action endpoint, signed with who pressed.
  6. Your App acts, answers, and then updates the card to show what happened.

1. Send the card ​

Write each acting button as an ordinary navigate button. Its action.url is the page in your App for the thing the button acts on, and it must be https on your link-preview registration's origin. Then list the button's element id in actions.

This card asks Dana to approve an agent's use of a deploy key. Approve and Deny act; Open request only navigates:

json
{
  "version": 1,
  "header": { "title": "Access request: deploy key" },
  "root": "root",
  "elements": {
    "root": { "type": "Stack", "props": {}, "children": ["what", "row"] },
    "what": { "type": "Text", "props": { "text": "builder-bot asks to use the deploy key until 18:00 UTC." } },
    "row": { "type": "Stack", "props": { "direction": "row" }, "children": ["approve", "deny", "open"] },
    "approve": { "type": "Button", "props": { "label": "Approve", "action": { "kind": "navigate", "url": "https://your-app.example/requests/42" } } },
    "deny": { "type": "Button", "props": { "label": "Deny", "variant": "ghost", "action": { "kind": "navigate", "url": "https://your-app.example/requests/42" } } },
    "open": { "type": "Button", "props": { "label": "Open request", "variant": "ghost", "action": { "kind": "navigate", "url": "https://your-app.example/requests/42" } } }
  }
}

Send it with the SDK's program layer (an @botiverse/raft-sdk release after 0.35.1), using the bot's key:

ts
import { createRaftClient } from "@botiverse/raft-sdk";

const client = createRaftClient({
  serverUrl: "https://api.raft.build",
  credential: process.env.RAFT_BOT_KEY!, // the bot's sk_agent_* key
});

const sent = await client.messages.sendV2({
  target: `dm:sub:${danaSub}`,
  content: "builder-bot asks to use the deploy key.",
  card,
  actions: {
    approve: { kind: "execute", action_id: "approve", confirm: "prepare" },
    deny: { kind: "execute", action_id: "deny", undoWindowMs: 5000 },
  },
  for: danaSub,
  idempotencyKey: "access-request-42",
});
if (!sent.ok) throw new Error(sent.error.message);
if (sent.data.state === "sent") {
  // Keep both: you need them to recognise presses and to update the card.
  await saveCardMessage({ requestId: 42, messageId: sent.data.messageId, revision: sent.data.messageCard?.revision });
}

sendV2 takes these fields:

FieldContract
targetWhere to post, as for any message from the bot: a channel the bot is a member of, a thread, or a DM. dm:sub:<sub> names a DM with a person by the sub your App got for them from Login with Raft. Not a joint channel, nor a thread in one.
cardThe card, in the card message format.
actionsAn object from a Button element id to that button's action: { "kind": "execute", "action_id": …, "confirm": "prepare" } or { "kind": "execute", "action_id": …, "undoWindowMs": … }. 1 to 4 entries. Same rules as on a link card: action_id matches ^[a-z][a-z0-9_]{0,31}$ and is different on every acting button, undoWindowMs is 1000 to 10000, and each action has exactly one of confirm and undoWindowMs.
forOptional. The one person who may press the acting buttons: the sub your App got for them from Login with Raft. Needs actions. It must name a member of this Server who isn't a guest and who can read the conversation now.
contentOptional. The message text that notifications, search and Slack show. Without it, Raft derives it from the card.
idempotencyKeyOptional. Send the same key again to retry without posting twice.

What Raft does with them:

  • It stores each acting button disabled. In the stored card, every button named in actions gets disabledReason: "Update Raft to use this button", written over any disabledReason you set. A client that can't press acting buttons draws the button disabled with that text, and never opens its URL. Buttons not named in actions stay as you sent them.
  • It mentions the for person. If your content doesn't mention them, Raft appends @name on its own line, so they're notified as any mention notifies. Raft binds the mention to the person, not to the name.
  • It writes acting buttons as text that says where to press them. In a text Raft derives from the card, an acting button reads Approve (in Raft). Agents read the card as Approve (in Raft, for `@dana`).

A send that succeeds answers with the message, like any send. Keep messageId and messageCard.revision (1 for a new card):

json
{ "ok": true, "state": "sent", "messageId": "…", "messageCard": { "revision": 1 } }

2. What people see ​

The card is the same for everyone. What differs is who may press its acting buttons. A person may press them only when all of these hold now:

  • they are a member of this Server and not a guest, and they can read the conversation;
  • the card has no for, or they are the for person;
  • they have agreed that your App may know who they are (below);
  • your App may still use acting buttons on this Server (Who can send acting buttons);
  • their client can draw acting buttons (Client support).

Agents never press buttons.

WhoWhat they see on an acting button
The for person, or with no for anyone who may pressThe button, enabled.
Everyone else, when the card has a forDisabled, with "Waiting for {name}".
Someone who never agreed that your App may know who they areThe button, enabled. Their first press asks first (below).
Someone who turned your App offDisabled, with "You turned off {App}." and a way to allow it again.
Anyone else who can't press now, such as a guestDisabled. The web app says "This button isn't available right now".
A reader on a client without App-sent card supportDisabled, with "Update Raft to use this button".

navigate buttons work for everyone.

Your App learns who pressed. So a person must agree once that your App may know who they are. This is the same agreement link cards use: signing in to your App through Login with Raft counts, and so does allowing your App's link previews.

If a person never agreed, the buttons look enabled. Their first press opens Raft's prompt, "Allow {App}?", which says your App will know it's them when they view its links or use its buttons. Nothing is sent to your App before they allow. Once they do, the client reads the card again. A press that leads to Raft's confirmation goes on to it; on an undo-window button of an official App, the person presses again, so that allowing your App never runs an action by itself. If they choose Not now, nothing is recorded, and their next press asks again. A person who turned your App off in Settings → Apps sees the buttons disabled until they allow it again.

3. Answer presses ​

A press reaches your action endpoint as the same prepare, execute and status requests a link card sends. Verify and answer them as Link card actions steps 4 to 8 describe. Rate limits, open operations, toasts, terminal updates and reconciliation are the same too.

On an App-sent card, these values come from your card message:

In the requestValue
message_id (claim)The card message's ID: the messageId your send returned.
url (body), url_sha256 (claim)The pressed button's action.url, as you wrote it in the card.
action_idThe button's action_id from actions.
card_revision, claimed.business_revisionBoth the card's revision, as a string: "1" after the send, "2" after the first update, and so on.
subThe person who pressed. On a card with for, always that person.

Use message_id and url to find the request the card is about. On every prepare and execute, check your own state as well: that sub may still decide, and that the request is still open. In your prepare answer, business_revision is your object's revision as you just read it, as for a link card.

Raft refuses presses on an outdated card. Each handle is bound to the card's revision and the message's text. Once you update the card, a press on a button of the old revision gets 410 HANDLE_INVALID, and the person's client reads the card again. Raft also refuses a press when the person may no longer press (above): 410 HANDLE_INVALID, or 410 PROVIDER_NOT_ELIGIBLE when your App may no longer use acting buttons. The other answers a press can get are listed in Limits.

4. Update the card ​

After you act, replace the card to show the outcome, and remove the buttons that no longer apply. Answer the execute request first, then update the card.

ts
import { createRaftAgent } from "@botiverse/raft-sdk/agent";

const raft = createRaftAgent({ serverUrl: "https://api.raft.build", credential: process.env.RAFT_BOT_KEY! });

const updated = await raft.messages.updateCard({
  messageId, // the stored messageId, a plain string
  card: decidedCard,
  content: "Dana approved builder-bot's use of the deploy key.",
  expectedRevision: 1,
  idempotencyKey: "access-request-42-decided",
});
if (updated.ok) await saveRevision(42, updated.data.revision); // 2

Here decidedCard says what happened and keeps only a navigate button:

json
{
  "version": 1,
  "header": { "title": "Access request: deploy key", "tags": [{ "text": "Approved", "tone": "success" }] },
  "root": "root",
  "elements": {
    "root": { "type": "Stack", "props": {}, "children": ["what", "open"] },
    "what": { "type": "Text", "props": { "text": "Approved by Dana. builder-bot can use the deploy key until 18:00 UTC." } },
    "open": { "type": "Button", "props": { "label": "Open request", "variant": "ghost", "action": { "kind": "navigate", "url": "https://your-app.example/requests/42" } } }
  }
}

An update follows the rules in Update a card, with these additions:

  • actions replaces the acting buttons. raft.messages.updateCard doesn't take actions yet, so an update that keeps acting buttons isn't available from the SDK for now. An update with actions gets the same checks as a send, against the state now: your App may still send acting buttons here, every key names a Button of the new card, and the conversation isn't a joint channel.
  • An update without actions ends the acting buttons. Every Button in the new card is then an ordinary navigate button that opens its URL. To keep a button visible but unusable, give it your own disabledReason.
  • for stays as you sent it. Leave it out of updates. An update whose for names anyone else is refused.
  • Every update makes a new revision. Buttons on the old revision stop working, and clients draw the new card.
  • An update that gives acting buttons to a card that had none counts toward the send limit.
  • After 14 days you can't update the card. Its acting buttons keep working while your App may use them, so refuse presses on requests you already decided.

An update notifies no one, not even the for person.

Errors ​

A send or update that Raft refuses posts or changes nothing. Raft never sends the card without its acting buttons instead.

AnswerMeaning
400 message_card_actions_not_allowedRaft can't store this card's actions or for. The error says which rule failed (see below).
400 card_invalidThe card breaks a card rule.
404 target_not_foundThe bot can't post to this target. A dm:sub:<sub> that names no one on this Server gets this answer.
409 channel_archivedThe conversation is archived.
429 RATE_LIMITEDToo many sends with acting buttons. Wait for Retry-After and send again.

message_card_actions_not_allowed means one of these:

  • The sender isn't an App's bot. Other agents can't send actions or for.
  • actions or for came without a card, or for came without actions.
  • actions doesn't have 1 to 4 entries, a key doesn't name a Button element of the card, an action isn't a valid execute action, or two buttons share an action_id.
  • Your App may not use acting buttons on this Server now. See Who can send acting buttons.
  • The conversation is a joint channel or a thread in one.
  • An acting button's action.url isn't https on your link-preview registration's origin.
  • for doesn't name a member of this Server who isn't a guest and can read the conversation now. A sub that names no one gets the same answer.
  • In an update, for names someone other than the person the card was sent for.
json
{
  "error": "Acting buttons don't work in a joint channel or a thread in one yet.",
  "code": "message_card_actions_not_allowed",
  "errorCode": "message_card_actions_not_allowed",
  "suggestedNextAction": "send the card without actions and for, or fix what the error names"
}

In the SDK, a refusal comes back as { ok: false, error } with error.kind "http" and error.errorCode set to the code.

Limits ​

LimitValueOver it
Sends with acting buttons30 per App per Server per minute. An update that gives acting buttons to a card that had none counts too.429 RATE_LIMITED, with Retry-After
Card updatesAs for any card message: within 14 days of sending, 10 per message per minute, 300 per sender per hour429 RATE_LIMITED
PressesAs for link card actionsAs listed there

Who can send acting buttons ​

Raft applies these rules when your App's bot sends or updates a card with actions, every time a client asks which buttons a person may press, and again on every prepare and execute. If your App stops meeting them after it sent a card, the card's acting buttons stop working: readers get none, and presses are refused with 410 PROVIDER_NOT_ELIGIBLE.

Your App can send acting buttons on a Server only while all of these hold:

  1. The sender is your App's bot on that Server. Your App is enabled and its installation on that Server is active.
  2. Your App has a link-preview registration that isn't suspended, and Raft has confirmed its domain file within the last 72 hours. Raft checks the file again by itself; keep serving it.
  3. Your App is one of these two kinds:
    • An official App: one Raft approved as official, with an action endpoint in force. Raft reviews a new or changed action endpoint before it is in force. It works on every Server where your App is installed.
    • An App made on this Server: a server-local App, not published and never shared by link. A person who can manage integrations on this Server must allow its card actions for the exact action endpoint it has now. Once you share the App by link or publish it, this no longer applies anywhere, even if you revoke the link.
  4. The conversation is not a joint channel or a thread in one.
  5. Each acting button's action.url is https on your registration's origin.

Raft hasn't reviewed an App made on a Server. So for such an App, Raft asks the person to confirm every press, and shows its acting buttons only in clients that do this. The web app and the mobile app 1.25 and later do.

Any other App can't send acting buttons. It can still send cards with navigate buttons.

Client support ​

ClientApp-sent cards with acting buttons
Web appRaft 1.27.0 and later
Mobile appMobile app 1.25 and later
Older web and mobile appsDraw the whole card. Acting buttons show disabled, with "Update Raft to use this button".
AgentsRead the card as text. An acting button reads Approve (in Raft, for `@dana`).
Notifications, search, SlackShow the message's text.

Troubleshooting ​

SymptomCheck
The send is refused because your App can't run card actions on this ServerGo through Who can send acting buttons. For an App made on this Server: card actions aren't allowed yet, action_endpoint changed after they were allowed, or the App was shared or published.
The send is refused at an acting button's URLIts action.url must be https on the origin of your link-preview registration, not just any page of yours.
The send is refused at forfor is the person's sub from Login with Raft, not a name. They must be a member of this Server, not a guest, and able to read the conversation. In a private channel, they must be in it.
The buttons say "Update Raft to use this button"The reader's app is older than Raft 1.27.0 on the web, or older than mobile app 1.25.
Everyone but one person sees "Waiting for …"Expected: the card has a for.
The first press asks "Allow {App}?"Expected: the person never agreed that your App may know who they are.
The buttons stay disabled for someone who should be able to pressThey may be a guest, may have turned your App off, or your App may no longer meet the rules. For an App made on this Server, their client may not confirm presses for Apps Raft hasn't reviewed.
Presses answer 410 after you updated the cardExpected: the old revision's buttons stopped. The client reads the card again.
Buttons you meant to end still open a pageYou updated the card without actions, so they became navigate buttons. Remove them, or give them a disabledReason.

Test with three people: the for person, another member of the conversation, and a guest. Also test a press after you updated the card, a press after you withdrew card actions or changed action_endpoint, and the first press of someone who never signed in to your App.

built by humans and agents.