Interactive Controls
Add buttons and other interactive components to messages.
When a user clicks a control, your bot receives a ControlInteraction event.
How Controls Work #
Controls are interactive UI elements attached to messages. They're sent as an optional controls
array alongside the message text and entities. Controls are organized into rows — up to 5 rows with up to 5 controls each (25 total).
Two button variants: Callback buttons fire a ControlInteraction event with the button's id.
Link buttons open a URL in the browser — no event is fired.
Control Schema #
button. "ru", "en-US"). OKLCH Colours #
Buttons use OKLCH colours for theme-adaptive rendering. The client adjusts lightness to match the active theme while preserving hue and chroma.
// Common colours { "l": 0.72, "c": 0.19, "h": 142 } // green { "l": 0.55, "c": 0.25, "h": 29 } // red { "l": 0.65, "c": 0.20, "h": 260 } // blue { "l": 0.75, "c": 0.18, "h": 85 } // yellow
Colour Visualizer
Sending Controls #
Pass controls in your
IMessages/Send or
IInteractions/Reply request.
curl — send a message with buttons
curl -X POST /IMessages/v1/Send \
-H "Authorization: Bot YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"channelId": "...",
"text": "Deploy to production?",
"randomId": 42,
"controls": [
{
"controls": [
{
"type": 0,
"variant": 0,
"label": "✅ Deploy",
"id": "deploy_confirm",
"colour": { "l": 0.72, "c": 0.19, "h": 142 }
},
{
"type": 0,
"variant": 0,
"label": "❌ Cancel",
"id": "deploy_cancel",
"colour": { "l": 0.55, "c": 0.25, "h": 29 }
},
{
"type": 0,
"variant": 1,
"label": "📚 Runbook",
"url": "https://wiki.example.com/deploy"
}
]
}
]
}' C# — MessageBuilder
// Fluent builder for messages with entities + controls var (text, entities, controls) = new MessageBuilder() .Text("Deploy to production?") .Row(row => row .CallbackButton("deploy_confirm", "✅ Deploy", colour: new OklchColor(0.72f, 0.19f, 142)) .CallbackButton("deploy_cancel", "❌ Cancel", colour: new OklchColor(0.55f, 0.25f, 29)) .LinkButton("📚 Runbook", "https://wiki.example.com/deploy")) .Build(); await channel.SendMessage(text, entities, randomId, replyTo: null, controls);
C# — Rich text with entities + controls
var (text, entities, controls) = new MessageBuilder()
.Bold("Build #847")
.Text(" passed on ")
.Italic("main")
.Text(" — ")
.Url("ci.example.com", "/builds/847")
.Row(row => row
.CallbackButton("deploy", "Deploy",
colour: new OklchColor(0.72f, 0.19f, 142))
.CallbackButton("logs", "View Logs"))
.Build(); Handling Interactions #
When a user clicks a Callback button, your SSE stream receives a ControlInteraction event.
Subscribe to the ControlInteractions intent (bit 13, value 8192).
Event payload
event: ControlInteraction
data: {
"interactionId": "a1b2c3d4-...",
"controlType": 0,
"messageId": 123456789,
"channelId": "...",
"spaceId": "...",
"user": {
"userId": "...",
"username": "alice",
"displayName": "Alice",
"avatarUrl": null
},
"controlId": "deploy_confirm"
} Responding — reply with updated controls
// Handle the interaction in your SSE event loop if (eventType == "ControlInteraction") { var data = JsonSerializer.Deserialize<ControlInteractionEvent>(payload); if (data.ControlId == "deploy_confirm") { // Reply to the channel await httpClient.PostAsJsonAsync("/IInteractions/v1/Reply", new { channelId = data.ChannelId, text = "🚀 Deploying...", randomId = Random.Shared.NextInt64() }); // Disable the buttons on the original message await httpClient.PatchAsJsonAsync("/IInteractions/v1/EditMessage", new { channelId = data.ChannelId, messageId = data.MessageId, controls = new[] { new { controls = new[] { new { type = 0, variant = 0, label = "✅ Deployed", id = "deploy_confirm", disabled = true }, new { type = 0, variant = 0, label = "❌ Cancel", id = "deploy_cancel", disabled = true } }} } }); } }
TypeScript — full interaction handler
const sse = new EventSource(
`/IEvents/v1/Stream?intents=${8192}`,
{ headers: { Authorization: `Bot ${TOKEN}` } }
);
sse.addEventListener("ControlInteraction", async (e) => {
const { controlId, channelId, messageId, user } = JSON.parse(e.data);
switch (controlId) {
case "approve":
await fetch("/IInteractions/v1/Reply", {
method: "POST",
headers: { Authorization: `Bot ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
channelId,
text: `Approved by ${user.displayName}`,
randomId: Date.now(),
}),
});
break;
}
}); Editing Controls #
Use PATCH /IInteractions/v1/EditMessage to update the text or controls on a message your bot sent.
Pass an empty controls: [] array to remove all controls.
Limits
- Max 5 rows per message, 5 controls per row (25 total)
- Control
idvalues must be unique across all rows - Link button URLs must be
https://orhttp:// - Bots can only edit their own messages
- Rate limit: 15 requests/min for IInteractions