blocks, attachments, metadata, formatting flags, unfurl flags, and client message ids. Conversation writes update archive state, names, topics, purposes, membership, DMs, MPIMs, and read cursors. User writes update profile fields, status, custom fields, and deterministic active or away presence. File writes support the current external upload flow with local upload URLs, file share messages, reads, lists, downloads, and deletes. Pin and bookmark writes support channel message pins and link bookmarks. View writes support App Home publishing and modal stacks. Seeded OAuth apps and OAuth installs create bot users and installation records. OAuth exchanges and explicit token seeds create scoped token records. State changes dispatch event_callback payloads to configured webhook URLs.
Slack message text is limited to 40,000 Unicode characters across chat writes, incoming webhooks, and file upload initial comments. Longer text is truncated at a Unicode code point boundary before it is stored or dispatched. Successful Web API responses include warning: "message_truncated" and response_metadata with the matching warning and explanatory message. Rich fields such as blocks and attachments are preserved unchanged.
At a glance
Start
http://localhost:4003; alone, it starts on http://localhost:4000. Point @slack/web-api or Bolt at it with slackApiUrl: "http://localhost:4003/api/". For Socket Mode, start with npx -p @deploybaseline/cli -p @deploybaseline/ws baseline start --service slack (see Socket Mode).
Auth
POST /api/auth.test— Test authentication
Authorization: Bearer <token>. Seeded OAuth apps create local installation records, and the OAuth v2 flow with its user picker UI creates scoped bot tokens.
Web API arguments are read from the query string, a form body, or a JSON body, as Slack accepts them; when an argument is in both, the body wins.
Scope checks are relaxed by default for local development. Set slack.strict_scopes: true in seed config to make supported Web API methods return Slack-style missing_scope errors with needed and provided fields. Strict mode checks chat:write, channels:read, channels:history, channels:join, channels:manage, channels:write, groups:read, groups:history, groups:write, im:read, im:history, im:write, mpim:read, mpim:history, mpim:write, users:read, users:read.email, users.profile:read, users.profile:write, users:write, files:read, files:write, pins:read, pins:write, bookmarks:read, bookmarks:write, reactions:read, reactions:write, team:read, and admin.conversations:write. Slack lists no method-specific scopes for views.publish, views.open, views.update, or views.push, so the emulator requires auth but does not add strict-scope checks for those methods.
OAuth
GET /oauth/v2/authorize— Authorization endpoint (shows user picker)POST /oauth/v2/authorize/callback— User picker callback: creates the auth codePOST /api/oauth.v2.access— Token exchangePOST /api/apps.uninstall— Uninstall an app and revoke its workspace tokens
client_id. With apps configured, strict validation is enforced.
oauth.v2.access returns a bot token, bot_user_id, app_id, team details, and the authed_user object. When user_scope is requested, authed_user also includes a user token and granted user scopes.
POST /api/apps.uninstall accepts client_id, client_secret, and an installation’s user token in the Authorization header or POST token field. It removes that app’s installation in the token’s workspace and revokes all associated OAuth bot and user tokens, including earlier grants. Revoked credentials return token_revoked on Web API calls even with permissive authentication. Other apps and workspaces keep their credentials. Granular bot tokens cannot uninstall an app and return no_permission; configured development tokens without an app binding may uninstall a local installation. No method scope is required.
Uninstall dispatches app_uninstalled and tokens_revoked to matching event subscriptions with the removed app’s api_app_id. tokens_revoked.tokens.oauth and .bot contain deduplicated user IDs, never token strings. Credentials are invalid before callbacks are delivered. Handlers should accept either event order. The app definition remains available for a new OAuth installation; reinstalling issues fresh tokens without reactivating revoked ones.
Chat
POST /api/chat.postMessage— Post message with text or rich payload fields (supports threads viathread_tsand DM user IDs)POST /api/chat.postEphemeral— Post ephemeral message outside channel historyPOST /api/chat.update— Update message text and rich payload fieldsPOST /api/chat.delete— Delete messageGET /api/chat.getPermalink/POST /api/chat.getPermalink— Get message permalinkPOST /api/chat.scheduleMessage— Schedule pending messagePOST /api/chat.deleteScheduledMessage— Delete pending scheduled messagePOST /api/chat.scheduledMessages.list— List pending scheduled messagesPOST /api/chat.meMessage— Send a /me message
Conversations
POST /api/conversations.list— List conversations (cursor pagination,types,exclude_archived)POST /api/conversations.info— Get channel infoPOST /api/conversations.create— Create channelPOST /api/conversations.archive— Archive channelPOST /api/conversations.unarchive— Restore archived channelPOST /api/conversations.rename— Rename channelPOST /api/conversations.setTopic— Update channel topicPOST /api/conversations.setPurpose— Update channel purposePOST /api/conversations.history— Channel history with rich message fields (top-level messages)POST /api/conversations.replies— Thread replies with rich message fieldsPOST /api/conversations.join— Join channelPOST /api/conversations.leave— Leave channelPOST /api/conversations.invite— Invite user to a channelPOST /api/conversations.kick— Remove user from a channelPOST /api/conversations.open— Open or resume DM/MPIMPOST /api/conversations.close— Close DM/MPIMPOST /api/conversations.mark— Set read cursorPOST /api/conversations.members— List members
Channel privacy conversions
POST /api/admin.conversations.convertToPrivate and POST /api/admin.conversations.convertToPublic accept channel_id. Use a user token belonging to a seeded admin; bot and test tokens return not_allowed_token_type, and non-admin users return missing_scope. With slack.strict_scopes: true, the token also needs admin.conversations:write. For example:
message with subtype channel_convert_to_private or channel_convert_to_public. Event callbacks include the acting user and the new channel_type (group or channel). Admins need not belong to the channel. DMs, MPIMs, and repeated conversions are rejected; #general cannot become private.
These methods operate on the emulated workspace without Slack’s Enterprise plan or organization-wide installation requirements. Enterprise roles, channel-management policies, and MPIM conversion are not simulated.
Slack Connect fixtures
Seed a user with a differentteam_id to test messages from another workspace. users.info and users.list return that user’s workspace and is_stranger flag. Omitted user teams default to the emulated workspace. is_stranger defaults to false and can be seeded to model a user outside the app’s visible shared channels.
conversations.invite, then post a message mentioning the bot. The app-mention event includes the author’s user_team and source_team; the outer team_id identifies the receiving installation workspace selected by the token, which defaults to the emulated workspace. Ordinary message callbacks retain the author user ID, which can be resolved with users.info.
A channel with external members returns is_ext_shared: true and is_shared: true; its event callbacks include is_ext_shared_channel: true. Seed channels[].is_ext_shared: true to model an already shared channel. Sharing remains set after the last external member leaves or is removed, including across saved state. Slack Connect invitation and approval flows, workspace disconnection, and per-workspace channel settings are not simulated.
Users
POST /api/users.list— List users (cursor pagination)POST /api/users.info— Get user infoPOST /api/users.lookupByEmail— Look up by emailGET /api/users.profile.get/POST /api/users.profile.get— Get user profile fieldsPOST /api/users.profile.set— Update profile fields, status, and custom fieldsGET /api/users.getPresence/POST /api/users.getPresence— Get active or away presencePOST /api/users.setPresence— Set presence: the authed user to away or automatic
Reactions
POST /api/reactions.add— Add reactionPOST /api/reactions.remove— Remove reactionPOST /api/reactions.get— Get reactions for a message
Files
POST /api/files.getUploadURLExternal— Create a local external upload sessionPOST /upload/v1/:fileId— Receive raw uploaded file bytesPOST /api/files.completeUploadExternal— Complete uploads: optionally share file messagesGET /api/files.info/POST /api/files.info— Get file metadataGET /api/files.list/POST /api/files.list— List completed filesGET /files-pri/:fileId/:filename— Download file bytes: needs a bearer token that can access the filePOST /api/files.delete— Delete a completed file
Pins and bookmarks
POST /api/pins.add— Pin a message to a channelGET /api/pins.list/POST /api/pins.list— List pinned message items for a channelPOST /api/pins.remove— Remove a message pin from a channelPOST /api/bookmarks.add— Add a link bookmark to a channelPOST /api/bookmarks.edit— Update a link bookmarkPOST /api/bookmarks.list— List channel bookmarksPOST /api/bookmarks.remove— Remove a bookmark from a channel
Views
POST /api/views.publish— Publish or update an App Home view for a userPOST /api/views.open— Open a modal viewPOST /api/views.update— Update a view byview_idorexternal_idPOST /api/views.push— Push a modal view onto the current modal stackPOST /api/views.generateTriggerId— Generate trigger id: local helper for tests that need a modal trigger id
trigger_id or interactivity_pointer from an interaction payload or /api/views.generateTriggerId. Generate push values with an existing view_id. Triggers expire after 3 seconds by default; set slack.view_trigger_ttl_seconds to a positive number to allow more time in local tests.
Team
POST /api/team.info— Get workspace info
Bots
POST /api/bots.info— Get bot info
Incoming webhooks
POST /services/:teamId/:botId/:webhookId— Post via incoming webhook: text or rich payload fields (supportschanneloverride andthread_ts)
Interactivity
Buttons and modal submissions can invoke your app’s Interactivity Request URL. Configureinteractivity_url on a seeded slack.oauth_apps entry and set slack.signing_secret to the same secret used by your receiver:
POST /_slack/simulate/block-actions— Simulate a button click: acceptschannel,message_ts, and a button selector (action_id, exacttext, orurl), plus the actinguserID or name. It supports buttons in actions blocks and section accessories, including ephemeral messages visible to that user. The simulator requires an authenticated emulator token and the acting user must belong to the conversation.POST /_slack/simulate/view-submission— Simulate a modal submission: acceptsview_id, optionaluser, andvalueskeyed by input block ID. Text inputs take strings; static selects and radio buttons take an option value; multi-selects and checkboxes take arrays of option values. Omitted values retain previous input or initial values. Other input elements returnunsupported_element.
application/x-www-form-urlencoded with one payload JSON field, signed over the exact encoded request body. Button payloads include an app-bound, single-use trigger_id that can open a modal for the acting user. App ownership is preserved for messages posted with an app’s bot or user token. Simulators return the payload and delivery_id; attempts and delivery results are saved in slack.interaction_deliveries.
A button payload’s response_url accepts JSON without a bearer token, up to five times within 30 minutes. It posts an ephemeral reply by default, or a public reply with response_type: "in_channel". Use replace_original: true or delete_original: true to modify the source message. Replacement retains the source message’s visibility. Rich blocks and thread replies are supported.
A modal receiver must acknowledge within three seconds. An empty HTTP 200 closes the submitted view, while response_action: "clear" closes its stack. errors retains the view and returns block errors; update replaces its contents; push adds a new view subject to the three-view limit. Delivery failures and invalid acknowledgements leave the modal open. Shortcuts, non-button actions, view_closed, and block_suggestion are not simulated. Slash commands work for Socket Mode and HTTP apps; see Socket Mode.
Socket Mode
Setsocket_mode_enabled: true on an oauth_apps entry and that app receives its events, button clicks, modal submissions, and slash commands over a WebSocket instead of HTTP, as a Bolt app with socketMode: true expects. It works with a plain npx @deploybaseline/cli start; see WebSockets.
app_token: optional; defaultxapp-1-<app_id>-baseline
clientOptions.slackApiUrl to its Socket Mode client, so apps.connections.open and the socket both go to the emulator. Setting app_token alone also turns Socket Mode on.
POST /api/apps.connections.open— Open a Socket Mode connection: takes the app-level token from the Authorization header only, as Slack does. It returnsnot_authedwithout one,not_allowed_token_typefor a bot, user, or development token, andinvalid_authfor an unknown token. The URL isws://(orwss://when the base URL ishttps://), ends in/link/?ticket=...&app_id=..., and its ticket works once within 60 seconds.- On connect the emulator sends
hellowithnum_connections,debug_info, andconnection_info.app_id, and pings every 10 seconds. Adddebug_reconnects=trueto the URL to get arefresh_requesteddisconnect after 360 seconds. - Events arrive as
events_apienvelopes carrying the sameevent_callbackpayload andevent_idan HTTP subscriber gets, plusretry_attemptandretry_reason. The app must ack with{ "envelope_id": "..." }within 3 seconds. A missing ack, or no open connection, records a failed delivery in the inspector’s Events tab, andPOST /_slack/simulate/event-retryresends it withretry_attempt: 1and the givenretry_reason. With several connections, envelopes rotate between them. - A Socket Mode app’s events go only to its connections, never to HTTP event subscriptions. Apps without Socket Mode keep HTTP delivery.
- Button clicks (
POST /_slack/simulate/block-actions) and modal submissions (POST /_slack/simulate/view-submission) arrive asinteractiveenvelopes, so nointeractivity_urlis needed. Aview_submissionack’spayloadis the modal response, e.g.{ "response_action": "errors", "errors": { ... } }; Bolt’sack({...})sends it. POST /_slack/simulate/slash-command— Run a slash command: runs a declared command as a user would type it. It acceptscommand,text,channel(ID or name), and optionaluserandapp_id. Socket Mode apps get aslash_commandsenvelope; other apps needslash_commands[].urland get a signed form POST there. The payload carriestrigger_idandresponse_url. A reply in the ack (or HTTP response body) withtext,blocks, orattachmentsis posted ephemeral to the user, or in the channel withresponse_type: "in_channel". Errors:command_not_found,ambiguous_command,command_url_not_configured,app_not_installed, andcommand_delivery_failed.POST /_slack/simulate/socket-mode-disconnect— Simulate a disconnect: sendsdisconnectto open connections, for oneapp_idor all apps.reasonisrefresh_requested(default),warning, orlink_disabled. Slack clients reconnect when they get it; forrefresh_requestedandlink_disabledthe emulator also closes the connection after a second.GET /_slack/socket-mode/connectionslists open connections, and the inspector has a Socket Mode tab.- Socket Mode works in the CLI and in
createEmulator.
apps.connections.open rate limits.
Events API
For supported Slack writes that emit events, the emulator dispatchesevent_callback payloads to configured webhook URLs matching Slack’s Events API format.
Each callback includes outer token, api_app_id, authorizations, team_id, event_id, and integer Unix-seconds event_time alongside the inner event. For authenticated Web API writes, team_id comes from the presented Slack token’s installation. Development tokens without a stored Slack record fall back to the affected channel, user, or file’s team, then the seeded workspace team (or T000000001). Incoming webhook posts use their webhook record’s team, or the target channel’s team when no record matches. Each logical event gets a distinct event_id shared by deliveries to multiple subscribers.
Callbacks identify each workspace installation with its recipient api_app_id and bot authorization. With multiple installed apps, each receives a separate envelope; configured webhook subscriptions are shared and can route by api_app_id. Without an installation, callbacks use A000000001 and a local user authorization. The legacy token field defaults to baseline-verification-token; set slack.verification_token to match receivers that check it. Use slack.signing_secret for request signature verification.
Every inner event includes event_ts. Message events include channel_type (channel, group, im, or mpim). Posts made with an issued or seeded bot token, and incoming webhook and interaction replies, include the bot’s bot_id, app_id, and bot_profile when the corresponding bot record exists. These fields persist in history, replies, and updates so handlers can identify their own messages. Removing an installed bot with conversations.kick or conversations.leave also emits channel_left or group_left, with actor_id and event_ts, for the affected app.
To test bot handlers, invite an installed app’s bot_user_id with conversations.invite, subscribe to app_mention, then post text containing <@BOT_USER_ID> with chat.postMessage. Channel mentions produce one callback per mentioned app, with outer api_app_id, inner event_ts, and the original message and thread identity. DMs and MPIMs use message events.
messageevents onchat.postMessageapp_mentiononchat.postMessagewhen channel message text mentions an installed member botapp_uninstalled/tokens_revokedon successfulapps.uninstallmessagewithsubtype: message_changedonchat.updatemessagewithsubtype: message_deletedonchat.delete- rich message fields are included on posted
messageevents when present reaction_added/reaction_removedevents onreactions.add/reactions.removepin_added/pin_removedevents onpins.add/pins.removemessagewithsubtype: bot_messageon incoming webhook postschannel_archive/channel_unarchivefor public lifecycle archive writesgroup_archive/group_unarchivefor private lifecycle archive writeschannel_rename/group_renameand matching name message subtypes onconversations.renamemessagewith publicchannel_topic/channel_purposeor privategroup_topic/group_purposesubtypes on topic and purpose writesmember_joined_channel/member_left_channelon invite, join, leave, and kick writeschannel_left/group_leftwhen an installed bot leaves or is removedim_created,im_open,im_close,im_marked, and group open/close/marked events for DM and MPIM writesuser_changeon profile writespresence_changeon presence writesfile_created,file_shared, andfile_deletedon file writesmessagewithsubtype: file_shareon shared file uploads
slack.signing_secret in seed config to sign every outbound event subscription callback, including subscriptions that already exist. Signed callbacks include X-Slack-Request-Timestamp and X-Slack-Signature. The signature is v0=<HMAC-SHA256(secret, "v0:<timestamp>:<raw-body>")>, calculated from the exact serialized request body. Configure the receiver with the same secret and verify the unparsed body. When the secret is absent or empty, callbacks are unsigned.
Event retries
UsePOST /_slack/simulate/event-retry with an authenticated emulator token and JSON { "event_id": "Ev...", "reason": "http_timeout" } to test event deduplication. Get the event ID from a received callback or the inspector’s Events tab. The simulator resends the latest recorded delivery for that event to its original subscription, preserving the entire payload and event ID. If several subscribers received it, only the latest recipient is retried.
Each attempt gets an increasing X-Slack-Retry-Num, starting at 1, and X-Slack-Retry-Reason (default http_timeout). Reasons also accept http_error, connection_failed, ssl_error, too_many_redirects, and unknown_error. When slack.signing_secret is configured, the retry gets a fresh request timestamp and signature. The response includes delivery_id, retry_num, success, and status_code; a failed callback is recorded and still consumes a retry number.
Retries are explicit, with no automatic scheduling or three-attempt limit. Only the latest 1,000 event deliveries are retained in memory, so retries are unavailable after their history is evicted or the emulator restarts. Unknown events return event_not_found, and an inactive subscription returns subscription_inactive.
Seed config
signing_secret: optional; signs event callbackssocket_mode_enabled(not set above, for exampletrue): deliver over Socket Modeslash_commands(not set above, for example[{ "command": "/deploy", "url": "http://localhost:3000/slack/commands" }]).
Inspector
GET /— Tabbed local inspector for conversations, messages, files, views, auth records, incoming webhooks, event subscriptions, event deliveries, and Socket Mode connections
Limits
Slack Connect invitation and approval flows, per-workspace channel settings, other Enterprise Grid admin APIs, Audit Logs API, SCIM, Legal Holds, shortcuts, interaction types other than button actions and modal submissions, user groups, reminders, stars, calls, canvases, lists, functions, workflows, chat streaming, legacyfiles.upload, exact rate limiting, and paid-plan behavior are not implemented.
Conformance
See CONFORMANCE.md for every provider.