Skip to main content
Stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, modern file uploads, pins, bookmarks, views, OAuth v2, incoming webhooks, interactivity, slash commands, and Socket Mode. Chat writes preserve common rich message fields such as 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

When all services are started, Slack listens on 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
All Web API endpoints require 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 code
  • POST /api/oauth.v2.access — Token exchange
  • POST /api/apps.uninstall — Uninstall an app and revoke its workspace tokens
When no OAuth apps are configured, the emulator accepts any 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 via thread_ts and DM user IDs)
  • POST /api/chat.postEphemeral — Post ephemeral message outside channel history
  • POST /api/chat.update — Update message text and rich payload fields
  • POST /api/chat.delete — Delete message
  • GET /api/chat.getPermalink / POST /api/chat.getPermalink — Get message permalink
  • POST /api/chat.scheduleMessage — Schedule pending message
  • POST /api/chat.deleteScheduledMessage — Delete pending scheduled message
  • POST /api/chat.scheduledMessages.list — List pending scheduled messages
  • POST /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 info
  • POST /api/conversations.create — Create channel
  • POST /api/conversations.archive — Archive channel
  • POST /api/conversations.unarchive — Restore archived channel
  • POST /api/conversations.rename — Rename channel
  • POST /api/conversations.setTopic — Update channel topic
  • POST /api/conversations.setPurpose — Update channel purpose
  • POST /api/conversations.history — Channel history with rich message fields (top-level messages)
  • POST /api/conversations.replies — Thread replies with rich message fields
  • POST /api/conversations.join — Join channel
  • POST /api/conversations.leave — Leave channel
  • POST /api/conversations.invite — Invite user to a channel
  • POST /api/conversations.kick — Remove user from a channel
  • POST /api/conversations.open — Open or resume DM/MPIM
  • POST /api/conversations.close — Close DM/MPIM
  • POST /api/conversations.mark — Set read cursor
  • POST /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:
Conversions preserve the channel ID, members, and history, update visibility in conversation and file reads, and append a 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 different team_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.
Use the seeded user token to act as the external user. Invite the user and your app’s bot to a channel with 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 info
  • POST /api/users.lookupByEmail — Look up by email
  • GET /api/users.profile.get / POST /api/users.profile.get — Get user profile fields
  • POST /api/users.profile.set — Update profile fields, status, and custom fields
  • GET /api/users.getPresence / POST /api/users.getPresence — Get active or away presence
  • POST /api/users.setPresence — Set presence: the authed user to away or automatic

Reactions

  • POST /api/reactions.add — Add reaction
  • POST /api/reactions.remove — Remove reaction
  • POST /api/reactions.get — Get reactions for a message

Files

  • POST /api/files.getUploadURLExternal — Create a local external upload session
  • POST /upload/v1/:fileId — Receive raw uploaded file bytes
  • POST /api/files.completeUploadExternal — Complete uploads: optionally share file messages
  • GET /api/files.info / POST /api/files.info — Get file metadata
  • GET /api/files.list / POST /api/files.list — List completed files
  • GET /files-pri/:fileId/:filename — Download file bytes: needs a bearer token that can access the file
  • POST /api/files.delete — Delete a completed file

Pins and bookmarks

  • POST /api/pins.add — Pin a message to a channel
  • GET /api/pins.list / POST /api/pins.list — List pinned message items for a channel
  • POST /api/pins.remove — Remove a message pin from a channel
  • POST /api/bookmarks.add — Add a link bookmark to a channel
  • POST /api/bookmarks.edit — Update a link bookmark
  • POST /api/bookmarks.list — List channel bookmarks
  • POST /api/bookmarks.remove — Remove a bookmark from a channel

Views

  • POST /api/views.publish — Publish or update an App Home view for a user
  • POST /api/views.open — Open a modal view
  • POST /api/views.update — Update a view by view_id or external_id
  • POST /api/views.push — Push a modal view onto the current modal stack
  • POST /api/views.generateTriggerId — Generate trigger id: local helper for tests that need a modal trigger id
Modal opens and pushes accept 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 (supports channel override and thread_ts)

Interactivity

Buttons and modal submissions can invoke your app’s Interactivity Request URL. Configure interactivity_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: accepts channel, message_ts, and a button selector (action_id, exact text, or url), plus the acting user ID 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: accepts view_id, optional user, and values keyed 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 return unsupported_element.
Callbacks are 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

Set socket_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; default xapp-1-<app_id>-baseline
Bolt passes 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 returns not_authed without one, not_allowed_token_type for a bot, user, or development token, and invalid_auth for an unknown token. The URL is ws:// (or wss:// when the base URL is https://), ends in /link/?ticket=...&app_id=..., and its ticket works once within 60 seconds.
  • On connect the emulator sends hello with num_connections, debug_info, and connection_info.app_id, and pings every 10 seconds. Add debug_reconnects=true to the URL to get a refresh_requested disconnect after 360 seconds.
  • Events arrive as events_api envelopes carrying the same event_callback payload and event_id an HTTP subscriber gets, plus retry_attempt and retry_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, and POST /_slack/simulate/event-retry resends it with retry_attempt: 1 and the given retry_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 as interactive envelopes, so no interactivity_url is needed. A view_submission ack’s payload is the modal response, e.g. { "response_action": "errors", "errors": { ... } }; Bolt’s ack({...}) sends it.
  • POST /_slack/simulate/slash-command — Run a slash command: runs a declared command as a user would type it. It accepts command, text, channel (ID or name), and optional user and app_id. Socket Mode apps get a slash_commands envelope; other apps need slash_commands[].url and get a signed form POST there. The payload carries trigger_id and response_url. A reply in the ack (or HTTP response body) with text, blocks, or attachments is posted ephemeral to the user, or in the channel with response_type: "in_channel". Errors: command_not_found, ambiguous_command, command_url_not_configured, app_not_installed, and command_delivery_failed.
  • POST /_slack/simulate/socket-mode-disconnect — Simulate a disconnect: sends disconnect to open connections, for one app_id or all apps. reason is refresh_requested (default), warning, or link_disabled. Slack clients reconnect when they get it; for refresh_requested and link_disabled the emulator also closes the connection after a second. GET /_slack/socket-mode/connections lists open connections, and the inspector has a Socket Mode tab.
  • Socket Mode works in the CLI and in createEmulator.
Not simulated: Slack’s limit of 10 connections per app, per-app event subscriptions (a Socket Mode app receives every event its installation would), shortcuts, and apps.connections.open rate limits.

Events API

For supported Slack writes that emit events, the emulator dispatches event_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.
  • message events on chat.postMessage
  • app_mention on chat.postMessage when channel message text mentions an installed member bot
  • app_uninstalled / tokens_revoked on successful apps.uninstall
  • message with subtype: message_changed on chat.update
  • message with subtype: message_deleted on chat.delete
  • rich message fields are included on posted message events when present
  • reaction_added / reaction_removed events on reactions.add / reactions.remove
  • pin_added / pin_removed events on pins.add / pins.remove
  • message with subtype: bot_message on incoming webhook posts
  • channel_archive / channel_unarchive for public lifecycle archive writes
  • group_archive / group_unarchive for private lifecycle archive writes
  • channel_rename / group_rename and matching name message subtypes on conversations.rename
  • message with public channel_topic / channel_purpose or private group_topic / group_purpose subtypes on topic and purpose writes
  • member_joined_channel / member_left_channel on invite, join, leave, and kick writes
  • channel_left / group_left when an installed bot leaves or is removed
  • im_created, im_open, im_close, im_marked, and group open/close/marked events for DM and MPIM writes
  • user_change on profile writes
  • presence_change on presence writes
  • file_created, file_shared, and file_deleted on file writes
  • message with subtype: file_share on shared file uploads
Set 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

Use POST /_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 callbacks
  • socket_mode_enabled (not set above, for example true): deliver over Socket Mode
  • slash_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, legacy files.upload, exact rate limiting, and paid-plan behavior are not implemented.

Conformance

See CONFORMANCE.md for every provider.