Modals
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.
Open a modal
Section titled “Open a modal”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.openAuthorization: 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.
View fields
Section titled “View fields”| 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. |
Inputs
Section titled “Inputs”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).
Handle the submission
Section titled “Handle the submission”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.
Update and push from your app
Section titled “Update and push from your app”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.
When the person closes it
Section titled “When the person closes it”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.
Errors
Section titled “Errors”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 |