slack
Slack API Emulator
Fully stateful Slack Web API emulation with channels, messages, threads, reactions, OAuth v2, and incoming webhooks. State changes dispatch event_callback payloads to configured webhook URLs.
Start
# Slack only
npx emulate --service slack
# Default port (when run alone)
# http://localhost:4000
Or programmatically:
import { createEmulator } from 'emulate'
const slack = await createEmulator({ service: 'slack', port: 4003 })
// slack.url === 'http://localhost: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.
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' },
},
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
- name: designer
real_name: Designer
email: designer@example.com
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
name: My Slack App
redirect_uris:
- http://localhost:3000/api/auth/callback/slack
incoming_webhooks:
- channel: general
label: CI Notifications
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
# Test authentication
curl -X POST http://localhost:4003/api/auth.test \
-H "Authorization: Bearer $TOKEN"
Chat
# Post message
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!"}'
# Post threaded reply
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"}'
# Update message
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"}'
# Delete 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"}'
# /me message
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
# List channels (cursor pagination)
curl -X POST http://localhost:4003/api/conversations.list \
-H "Authorization: Bearer $TOKEN"
# Get channel info
curl -X POST http://localhost:4003/api/conversations.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Create channel
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}'
# Channel history (top-level messages only)
curl -X POST http://localhost:4003/api/conversations.history \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# Thread replies
curl -X POST http://localhost:4003/api/conversations.replies \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "ts": "1234567890.123456"}'
# Join / leave channel
curl -X POST http://localhost:4003/api/conversations.join \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
# List members
curl -X POST http://localhost:4003/api/conversations.members \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001"}'
Users
# List users (cursor pagination)
curl -X POST http://localhost:4003/api/users.list \
-H "Authorization: Bearer $TOKEN"
# Get user info
curl -X POST http://localhost:4003/api/users.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"user": "U000000001"}'
# Lookup by email
curl -X POST http://localhost:4003/api/users.lookupByEmail \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "dev@example.com"}'
Reactions
# Add reaction
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"}'
# Remove reaction
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"}'
# Get reactions
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
# Get workspace info
curl -X POST http://localhost:4003/api/team.info \
-H "Authorization: Bearer $TOKEN"
Bots
# Get bot info
curl -X POST http://localhost:4003/api/bots.info \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"bot": "B000000001"}'
Incoming Webhooks
# Post via incoming webhook
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Deployment complete!"}'
# Post to a specific channel
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Alert!", "channel": "C000000002"}'
# Post threaded webhook message
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Thread update", "thread_ts": "1234567890.123456"}'
OAuth
# Authorize (browser flow, shows user picker)
# GET /oauth/v2/authorize?client_id=...&redirect_uri=...&scope=...&state=...
# Token exchange
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",
"bot_user_id": "B000000001",
"team": { "id": "T000000001", "name": "Emulate" },
"authed_user": { "id": "U000000001" }
}
Event Dispatching
When messages are posted or reactions are added/removed, the emulator dispatches event_callback payloads to configured webhook URLs. These payloads match Slack's Events API format:
messageevents onchat.postMessage,chat.update,chat.deletereaction_added/reaction_removedevents onreactions.add/reactions.removemessagewithsubtype: bot_messageon incoming webhook posts
Common Patterns
Post Messages and React
TOKEN="test_token_admin"
BASE="http://localhost:4003"
# Post a message
curl -X POST $BASE/api/chat.postMessage \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"channel": "C000000001", "text": "Hello!"}'
# React to it (use the ts from the response)
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
CI Notifications via Webhook
# Use the default incoming webhook
curl -X POST http://localhost:4003/services/T000000001/B000000001/X000000001 \
-H "Content-Type: application/json" \
-d '{"text": "Build passed on main"}'