| name | slack |
| description | Emulated Slack API for local development and testing. Use when the user needs to interact with Slack API endpoints locally, test Slack integrations, emulate channels/messages/users, set up Slack OAuth flows, test incoming webhooks, or work with the Slack Web API without hitting the real Slack API. Triggers include "Slack API", "emulate Slack", "mock Slack", "test Slack OAuth", "Slack bot", "incoming webhook", "local Slack", or any task requiring a local Slack API. |
| allowed-tools | Bash(npx emulate:*), Bash(curl:*) |
Slack API Emulator
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, user profiles, presence, OAuth v2, and incoming webhooks. 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. 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.
Start
npx emulate --service slack
Or programmatically:
import { createEmulator } from 'emulate'
const slack = await createEmulator({ service: 'slack', port: 4003 })
Auth
Pass tokens as Authorization: Bearer <token>. All Web API endpoints require authentication.
curl -X POST http://localhost:4003/api/auth.test \
-H "Authorization: Bearer test_token_admin"
When no token is provided, requests fall back to the first seeded user.
Scope checks are relaxed by default for local development. Set slack.strict_scopes: true in seed config when you need supported Web API methods to return Slack-style missing_scope errors with needed and provided fields. Supported user and presence checks include users:read, users:read.email, users.profile:read, users.profile:write, and users:write.
Pointing Your App at the Emulator
Environment Variable
SLACK_EMULATOR_URL=http://localhost:4003
Slack SDK / Bolt
import { WebClient } from '@slack/web-api'
const client = new WebClient(token, {
slackApiUrl: `${process.env.SLACK_EMULATOR_URL}/api/`,
})
OAuth URL Mapping
| Real Slack URL | Emulator URL |
|---|
https://slack.com/oauth/v2/authorize | $SLACK_EMULATOR_URL/oauth/v2/authorize |
https://slack.com/api/oauth.v2.access | $SLACK_EMULATOR_URL/api/oauth.v2.access |
Auth.js / NextAuth.js
{
id: 'slack',
name: 'Slack',
type: 'oauth',
authorization: {
url: `${process.env.SLACK_EMULATOR_URL}/oauth/v2/authorize`,
params: { scope: 'chat:write,channels:read,users:read,users.profile:read' },
},
token: {
url: `${process.env.SLACK_EMULATOR_URL}/api/oauth.v2.access`,
},
clientId: process.env.SLACK_CLIENT_ID,
clientSecret: process.env.SLACK_CLIENT_SECRET,
}
Seed Config
slack:
team:
name: My Workspace
domain: my-workspace
users:
- name: developer
real_name: Developer
email: dev@example.com
is_admin: true
profile:
title: Local Developer
status_text: Testing locally
status_emoji: ":computer:"
presence: active
- name: designer
real_name: Designer
email: designer@example.com
profile:
title: Designer
presence: away
channels:
- name: general
topic: General discussion
- name: engineering
topic: Engineering discussions
is_private: true
bots:
- name: my-bot
oauth_apps:
- client_id: "12345.67890"
client_secret: example_client_secret
app_id: A000000001
name: My Slack App
redirect_uris:
- http://localhost:3000/api/auth/callback/slack
scopes:
- chat:write
- channels:read
- users.profile:read
- users.profile:write
- users:write
user_scopes:
- users:read
- users.profile:read
bot_name: my-bot
tokens:
- token: xoxb-local-test
user: developer
scopes:
- chat:write
- channels:read
- users.profile:read
- users.profile:write
- users:write
incoming_webhooks:
- channel: general
label: CI Notifications
strict_scopes: false
signing_secret: my_signing_secret
When no OAuth apps are configured, the emulator accepts any client_id. With apps configured, strict validation is enforced for client_id, client_secret, and redirect_uri.
API Endpoints
Auth
curl -X POST http://localhost:4003/api/auth.test \
-H "Authorization: Bearer $TOKEN"
Chat
chat.postMessage, chat.update, conversations.history, and conversations.replies round trip text plus common rich message fields: blocks, attachments, metadata, mrkdwn, parse, link_names, unfurl_links, unfurl_media, username, icon_url, icon_emoji, bot_id, app_id, client_msg_id, and reply_broadcast. chat.postMessage can also post to opened DM conversations or supported Slack user IDs.
chat.postEphemeral stores ephemeral messages outside channel history. chat.scheduleMessage, chat.deleteScheduledMessage, and chat.scheduledMessages.list keep scheduled messages pending until deleted or inspected.
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Hello from the emulator!"}'
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Thread reply", "thread_ts": "1234567890.123456"}'
curl -X POST http://localhost:4003/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Deploy complete", "blocks": [{"type": "section", "text": {"type": "mrkdwn", "text": "*Deploy* complete"}}], "metadata": {"event_type": "deploy_complete", "event_payload": {"deploy_id": "dep_123"}}, "unfurl_links": false}'
curl -X POST http://localhost:4003/api/chat.update \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456", "text": "Updated message"}'
curl -X POST http://localhost:4003/api/chat.delete \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
curl -X GET 'http://localhost:4003/api/chat.getPermalink?channel=C000000001&message_ts=1234567890.123456' \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4003/api/chat.postEphemeral \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "user": "U000000001", "text": "Only you can see this"}'
curl -X POST http://localhost:4003/api/chat.scheduleMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"C000000001\", \"text\": \"Reminder\", \"post_at\": $(($(date +%s) + 3600))}"
curl -X POST http://localhost:4003/api/chat.scheduledMessages.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
curl -X POST http://localhost:4003/api/chat.deleteScheduledMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "scheduled_message_id": "Q123456789"}'
curl -X POST http://localhost:4003/api/chat.meMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "is thinking..."}'
Conversations
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"types": "im,mpim"}'
curl -X POST http://localhost:4003/api/conversations.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
curl -X POST http://localhost:4003/api/conversations.create \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "new-channel", "is_private": false}'
curl -X POST http://localhost:4003/api/conversations.archive \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000002"}'
curl -X POST http://localhost:4003/api/conversations.unarchive \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000002"}'
curl -X POST http://localhost:4003/api/conversations.rename \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "name": "new-channel-name"}'
curl -X POST http://localhost:4003/api/conversations.setTopic \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "topic": "Release coordination"}'
curl -X POST http://localhost:4003/api/conversations.setPurpose \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "purpose": "Coordinate release work"}'
curl -X POST http://localhost:4003/api/conversations.history \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
curl -X POST http://localhost:4003/api/conversations.replies \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
curl -X POST http://localhost:4003/api/conversations.join \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
curl -X POST http://localhost:4003/api/users.list \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"types": "public_channel,private_channel"}'
CHANNEL_ID="<channel-id-from-conversations.list>"
USER_ID="<user-id-from-users.list>"
curl -X POST http://localhost:4003/api/conversations.invite \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$CHANNEL_ID\", \"users\": \"$USER_ID\"}"
curl -X POST http://localhost:4003/api/conversations.kick \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$CHANNEL_ID\", \"user\": \"$USER_ID\"}"
curl -X POST http://localhost:4003/api/conversations.open \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"users\": \"$USER_ID\", \"return_im\": true}"
DM_CHANNEL_ID="<channel.id-from-conversations.open>"
curl -X POST http://localhost:4003/api/conversations.close \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"channel\": \"$DM_CHANNEL_ID\"}"
curl -X POST http://localhost:4003/api/conversations.mark \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
curl -X POST http://localhost:4003/api/conversations.members \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
Users
curl -X POST http://localhost:4003/api/users.list \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4003/api/users.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user": "U000000001"}'
curl -X POST http://localhost:4003/api/users.lookupByEmail \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com"}'
curl -X GET 'http://localhost:4003/api/users.profile.get?user=U000000001' \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4003/api/users.profile.set \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user": "U000000001", "profile": {"display_name": "Developer", "status_text": "Testing locally", "status_emoji": ":computer:"}}'
curl -X GET 'http://localhost:4003/api/users.getPresence?user=U000000001' \
-H "Authorization: Bearer $TOKEN"
curl -X POST http://localhost:4003/api/users.setPresence \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"presence": "away"}'
curl -X POST http://localhost:4003/api/users.setPresence \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"presence": "auto"}'
Reactions
curl -X POST http://localhost:4003/api/reactions.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456", "name": "thumbsup"}'
curl -X POST http://localhost:4003/api/reactions.remove \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456", "name": "thumbsup"}'
curl -X POST http://localhost:4003/api/reactions.get \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "1234567890.123456"}'
Team
curl -X POST http://localhost:4003/api/team.info \
-H "Authorization: Bearer $TOKEN"
Bots
curl -X POST http://localhost:4003/api/bots.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"bot": "B000000001"}'
Incoming Webhooks
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Deployment complete!"}'
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Alert!", "channel": "C000000002"}'
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Thread update", "thread_ts": "1234567890.123456"}'
OAuth
curl -X POST http://localhost:4003/api/oauth.v2.access \
-H "Content-Type: application/json" \
-d '{"client_id": "12345.67890", "client_secret": "example_client_secret", "code": "<code>"}'
Returns a Slack-style response:
{
"ok": true,
"access_token": "xoxb-...",
"token_type": "bot",
"scope": "chat:write,channels:read",
"app_id": "A000000001",
"bot_user_id": "U000000099",
"team": { "id": "T000000001", "name": "Emulate" },
"authed_user": { "id": "U000000001" }
}
Event Dispatching
When messages are posted, updated, deleted, or reactions are added/removed, the emulator dispatches event_callback payloads to configured webhook URLs. These payloads match Slack's Events API format:
message events on chat.postMessage
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
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
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
Common Patterns
Post Messages and React
TOKEN="test_token_admin"
BASE="http://localhost:4003"
curl -X POST $BASE/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Hello!"}'
curl -X POST $BASE/api/reactions.add \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "timestamp": "<ts>", "name": "wave"}'
OAuth Flow
- Redirect user to
$SLACK_EMULATOR_URL/oauth/v2/authorize?client_id=...&redirect_uri=...&scope=chat:write,channels:read&state=...
- User picks a seeded user on the emulator's UI
- Emulator redirects back with
?code=...&state=...
- Exchange code for token via
POST /api/oauth.v2.access
- Use
xoxb- token to call Web API endpoints
When user_scope is included in the authorize URL and callback, the exchange response includes authed_user.access_token, authed_user.scope, and authed_user.token_type.
CI Notifications via Webhook
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Build passed on main"}'