action_config,
and the API checks it before anything is stored.
What a button can carry
Buttons can sit in an
actions block, as a section accessory, in context_actions, on a card,
or on a carousel card. Where a button sits decides which update strategies make sense:
Payload variables
GET /v1/templates/block-schema returns this whole contract as data under interactivity, so an
assistant can read it rather than remember it.
What the API checks
POST /v1/templates/validate, POST /v1/templates and POST /v1/templates/{id}/versions all
check every configured button and refuse, or report, when:
- the
action_confighas the wrong shape (an unknown strategy,replace_blockwithouttarget_block_id, aroutewithout a destination, a message update without idempotency); - a
destination_idis not an active destination in your organization, or amodal_definition_idis not one of your active modals; - a strategy is not available for the button’s position;
- the message metadata a send would embed exceeds 16,000 bytes. Action configs travel inside
each sent message’s metadata together with the send’s data, so a posted message keeps doing what
it did when sent.
validatereports the measured size asinteractivity.metadata_bytes.
valid: false and one error per problem naming the action_id; create and
version answer 400 with code: "invalid_data" and the same list under issues. Nothing is stored
until every check passes. Before this contract, a mistake here was accepted and failed at click
time, in a Slack message only the clicker saw.
Manage destinations
The/v1/destinations API supports list, get, create, patch and delete. Reads require
destinations:read; writes require destinations:write (admin or owner). Credentials go in
headers; auth_type describes the authentication style and does not synthesize an Authorization
header. URLs are readable by organization members: keep secrets in headers, not URL query strings.
/v1/destinations with a unique Idempotency-Key. All responses replace header
values with •••. A PATCH omitting headers preserves credentials; a supplied object replaces the
entire header set, and {} clears it. Sending masked values back is rejected. Unknown fields,
invalid enums and numeric limits produce 422 validation_failed, before database writes. Invalid
compiled modal content returns 400. Bodies over 256 KiB are rejected with 413.
DELETE returns 409 while a current template version references the destination, including a
modal submission webhook configured on a button. Old sent messages and draft/historical versions
are not protected by that check: deleting their destination makes subsequent clicks fail. Historical
interaction logs remain, with the deleted destination ID set to null. To stop dispatch deliberately,
PATCH is_active: false. Existing runtime dispatch already checks active status.
Signed destination tests
POST /v1/destinations/{id}/test accepts { "signing_secret": "YOUR_SHARED_TEST_SECRET" } and an
optional Idempotency-Key. This dispatches one real request using the destination’s method and
stored headers, with a generated lithoblocks.destination_test payload. It does not spend a message
credit or retry automatically. The receiver’s HTTP status and elapsed milliseconds are returned;
its response body is discarded. A completed non-2xx response returns delivered: false with that
status. A transport failure, timeout or blocked redirect returns HTTP 502: delivery is uncertain,
and an idempotency key retains that outcome for reconciliation.
Tests are disabled unless the API operator configures INTERACTIVITY_TEST_ALLOWED_HOSTS with exact,
comma-separated HTTPS hostnames they trust and control. No wildcard hosts, IP literals, alternate
ports, URL credentials or redirects are allowed. The operator must ensure approved hosts resolve
to public destinations and cannot be rebound to internal services. This is an operator allowlist,
not arbitrary customer-host DNS validation. No hosts are enabled by this code change.
The receiver verifies X-Lithoblocks-Signature: v1=<hex> as HMAC-SHA256 over
v1:<X-Lithoblocks-Timestamp>:<raw request body>, using the supplied test secret. Compare signatures
in constant time and reject stale timestamps/reused test IDs. The test secret is never stored or
returned. This test signature does not change the existing authentication protocol for ordinary
button or submission delivery; those continue to use configured headers.
Create and version modals
modals:read and modals:write are available to members, admins and owners. Validate before saving:
blocks, the builder-compatible placeholder_mapping, and a
compiled_view. It uses the same compiler as slack-modal-open, calls no external service and
spends no credits. For creation, add name and POST the same content to /v1/modals, optionally
with activate: true. Definition and first version commit together or neither exists. The default
is a current draft version, so the web editor can load it. Current does not mean active: activate it before referencing it from an interactive
button; a foreign, draft, inactive or missing modal is rejected during template authoring.
GET /v1/modalslists definitions;GET /v1/modals/{id}includes the current version.PATCH /v1/modals/{id}edits name, description, title and labels; omitted fields stay unchanged.GET /v1/modals/{id}/versionslists newest first.POST /v1/modals/{id}/versionsappendsblocks,sample_data, and optionalactivate.POST /v1/modals/{id}/versions/{version_id}/activateatomically selects an existing version.
save_modal_version RPC share a parent-row lock to serialize version numbering and
activation. Creating another draft through the API leaves the existing active version alone. The
browser RPC retains its historical behavior: every save becomes current with its supplied status.
Saving a draft in the browser therefore makes that modal unavailable to runtime opening until active.
Supported authoring blocks are input, section, context and divider, with existing modal directive
containers. Supported inputs are plain-text, static select, checkboxes, radio buttons, date and time
pickers. Title/labels allow 24 characters; compiled views allow at most 100 blocks, following
Slack’s view contract. The modal compiler’s
if directive tests a truthy dot path; it does not support the message compiler’s operator conditions
or elseChildren. Validate representative sample data for every branch. Dynamic data can still
produce an invalid Slack view later; local validation does not call Slack. Multi-step authoring and
an API modal-open/test endpoint remain outside this phase.
Observe the flow
GET /v1/interactions accepts template_id, action_id, since, limit and offset.
GET /v1/interactions/{id} returns the selected outcome. Results expose safe component type/status
and timestamps, delivery status, timing and retry metadata. They omit arbitrary component data,
webhook request/response bodies, URLs, error text and raw idempotency keys.
GET /v1/modal-submissions accepts modal_definition_id, since, limit and offset. Organization
access is checked through both the owning modal and interaction log. It returns submitted values,
which can themselves contain sensitive customer input, but excludes private metadata. List responses
use data and pagination: { total, limit, offset }; since is inclusive ISO 8601 with a timezone.
All new routes are guarded by scopes; write/validate routes share the organization authoring rate
limit. Existing keys receive only the new read scopes permitted by their owner’s role after the
migration. New write scopes require an explicit grant. Cached authentication may take up to 15
minutes to reflect a changed key. The browser API-key catalog, OpenAPI and Make scope list include
all five scopes. Make uses its existing Make an API Call module for these endpoints.
Use MCP
The MCP server exposes all 17 new operations, includinglist_modal_versions and update_modal.
Read/validate tools are read-only. Destination tests are marked destructive and open-world;
deletions, activation and edits that affect existing clicks are destructive. Create-modal and
create-version tools are conservatively destructive because activate: true is an optional input.
The interactive-flows plugin skill walks through destination → modal → template → send → history.
compile_template includes configured action IDs and their effects without exposing payloads.
Destination/modal/version creation, modal activation and destination tests honor Idempotency-Key
(idempotency_key in MCP). Keys are scoped to the organization and operation. The same key and
identical body replay the recorded response; a different body or in-progress owner returns 409.
Completed responses below 500 expire after 24 hours; incomplete/5xx outcomes remain for operator
reconciliation. A separately intended operation gets a new key. Never automatically rotate keys to
bypass an uncertain outcome. PATCH operations do not use this replay store.
Modal submission retries and delivery status
Submitting an authored modal saves one receipt per Slack workspace and modal view, then closes the modal. Webhook and message update results arrive asynchronously in interaction history. A closed modal confirms that the input was saved; it does not confirm that the receiver processed it. Retries of the same view reuse the original receipt and first accepted values. Opening a new modal permits another intentional submission, even with identical values. Webhook payloads can reference{{submission.id}}, and each request includes X-LithoBlocks-Submission-Id for receiver correlation.
A failed message update does not cause the webhook to be sent again.
Component status pending means accepted work is waiting for delivery. uncertain means the
outcome needs review: inspect the receiver before submitting again. Failed and uncertain attempts
are not automatically retried, including when a destination has retry settings. Submission ownership
is retained with its receipt; deleting receipt history removes that protection.
The legacy close_on_submission field remains accepted, but saved submissions always close.
close_behavior controls when the message update runs after a confirmed webhook result:
on_success (also when no webhook is configured), on_error, or always. Unknown webhook outcomes
skip the message update. These rules apply to authored interactive modals; slash-command send forms
use a separate handler.
Track link-button clicks
Enable Track clicks only beside the URL in the button editor, or setaction_config.tracking_only: true. The URL still opens in the user’s browser. Slack sends an
interaction for URL buttons too; LithoBlocks acknowledges it and records the click without calling
another destination, opening a modal or updating the message. See the
Slack button contract.
actions block or section accessory. It also works without a URL as a
simple acknowledgement. Tracking only cannot be combined with a webhook destination/route,
modal, or enabled message update. The editor requires those effects to be removed first.
Use a distinct action ID for each button you want to distinguish; the editor supplies one if missing.
Save the channel and message_ts returned by send, then query:
list_interactions and the TypeScript SDK’s existing interactions
list operation. Add slack_team_id to disambiguate Slack workspaces. The result contains who clicked,
when, the exact action ID and message coordinates, and a click_tracking component with success.
It uses existing interactions:read permissions and organization isolation. pagination.total
counts records matching the filters; paginate to collect records and deduplicate slack_user_id
locally if you want unique people. New clicks by the same person count again. since is inclusive;
when polling, overlap the time window and deduplicate by interaction id.
Slack retries with the same team, channel, message, user, action ID and exact action timestamp share
one stored record. Distinct clicks count separately, even in the same millisecond. No webhook
destination, webhook idempotency key, extra credit or background delivery worker is needed.
This records receipt of a click, not a page view, successful page load, read receipt, or conversion.
URL navigation can succeed even if tracking is unavailable; failed persistence is not acknowledged
as successful, and collection is not guaranteed during outages. There is no automatic alert subscription
or aggregate analytics endpoint in this addition. Receipt protection lasts while interaction history
is retained; deletion removes that protection. Buttons in messages sent before tracking was enabled
keep their original configuration—publish a version and send a new message to enable it.
