Skip to content

A modal is a dialog your app opens for one person — usually a form. It is a view: a title, an optional submit and close button, and up to 100 blocks, including input blocks.

Modals open in answer to something the person did, so you need the trigger_id from an interaction payload. Use it within 60 seconds, once.

POST /api/bot/views.open
Authorization: Bearer foxa-…
{
"trigger_id": "c0b6f3d1-…",
"view": {
"type": "modal",
"callback_id": "refund_form",
"title": { "type": "plain_text", "text": "Refund request" },
"submit": { "type": "plain_text", "text": "Submit" },
"close": { "type": "plain_text", "text": "Cancel" },
"private_metadata": "order-4821",
"blocks": [
{
"type": "input",
"block_id": "reason",
"label": { "type": "plain_text", "text": "Reason" },
"element": { "type": "plain_text_input", "action_id": "value", "multiline": true, "min_length": 3 }
},
{
"type": "input",
"block_id": "amount",
"label": { "type": "plain_text", "text": "Amount (JOD)" },
"element": { "type": "number_input", "action_id": "value", "is_decimal_allowed": true, "min_value": "1" }
}
]
}
}

The answer is { "ok": true, "view": { "id": "…", "hash": "…", … } }. Keep view.id (or set your own external_id) to update it later.

Field Description
type modal
title plain_text, up to 24 characters. Required.
submit Submit button label (plain_text, 24). Required when the view has input blocks.
close Close button label (plain_text, 24). Defaults to “Cancel”.
blocks Up to 100 blocks.
private_metadata Up to 3000 characters, returned to you in every payload from this view.
callback_id Your name for the view, returned in payloads.
external_id Your own unique id for the view, usable with views.update.
clear_on_close Closing this view closes the whole stack.
notify_on_close Send view_closed when the person closes it.
submit_disabled Show the submit button disabled.

Input blocks hold one element each: plain text, email, URL, number, rich text, file, or any menu, picker, checkbox group or radio group. Mark a block optional: true to allow it empty.

Inputs don’t send anything while the person types. Set dispatch_action: true on an input block to also get a block_actions payload when that element changes (text inputs dispatch on Enter, or as they type with a dispatch action configuration).

When the person presses submit, Workstation first checks the form itself — required fields, lengths, number ranges, email and URL formats — and shows errors under the fields without bothering your app. Then it sends view_submission:

{
"type": "view_submission",
"user": { "id": "42bc76d7-…", "name": "Dana Reader" },
"api_app_id": "75385d0c-…",
"trigger_id": "…",
"view": {
"id": "8d3bc164-…",
"callback_id": "refund_form",
"private_metadata": "order-4821",
"state": {
"values": {
"reason": { "value": { "type": "plain_text_input", "value": "Box arrived damaged" } },
"amount": { "value": { "type": "number_input", "value": "129.5" } }
}
}
}
}

This one is synchronous: answer within 3 seconds. Your answer decides what happens next.

Answer Effect
Empty 200 Close this view (the one below it, if any, shows again)
{ "response_action": "errors", "errors": { "reason": "Too short" } } Keep the view open and show each message under its input block
{ "response_action": "update", "view": { … } } Replace this view
{ "response_action": "push", "view": { … } } Stack a new view on top (up to 3)
{ "response_action": "clear" } Close every view in the stack

If your app doesn’t answer in time, the person sees an error and the view stays open.

Files from a file input arrive as { "id", "name", "mimetype", "size", "url_private" }. Download url_private with your bot token within 7 days.

POST /api/bot/views.update
{ "view_id": "8d3bc164-…", "hash": "…", "view": { "type": "modal", … } }

Pass external_id instead of view_id if you set one. With hash, a view the person changed since you read it fails with hash_conflict instead of being overwritten. views.push stacks a new view and needs a trigger_id from inside the modal.

The person’s typed values are kept for inputs whose block_id and action_id are still there after an update.

Cancel or the back arrow closes the top view; × closes the whole modal. With notify_on_close, you get:

{ "type": "view_closed", "view": { … }, "is_cleared": true }

is_cleared is true when the whole stack closed. It is delivered from the queue and needs no answer.

error Meaning
invalid_trigger_id, expired_trigger_id, exchanged_trigger_id Unknown, older than 60 seconds, or already used
push_limit_reached Three views are already stacked
not_found No such open view for this app
hash_conflict The view changed since your hash
duplicate_external_id Another open view of this app already uses that external_id
invalid_arguments Missing view, wrong view.type, or neither view_id nor external_id