Slack Events API to a Private Endpoint Using Skooh
Handle Slack's URL verification challenge and respond to events quickly using Skooh.
The Slack Challenge
Slack's Events API has two quirks that trip up many developers:
- URL Verification Challenge: When you add a webhook URL, Slack sends a challenge you must echo back
- 3-Second Timeout: Slack expects a response within 3 seconds or it retries
Skooh handles both gracefully.
Step 1: Set Up Skooh
- Create an endpoint in Skooh
- Copy your webhook URL and hook token
Step 2: Run the Skooh Proxy
Using the Desktop App:
- Download the Skooh Desktop App
- Enter your API key and hook token
- Set forwarding URL to your Slack bot (e.g., `http://localhost:3000/events\`)
- Click Connect
Using Docker:
docker run -d \\
--name skooh-slack \\
-e SKOOH_TOKEN=your_token \\
-e SKOOH_API_KEY=your_api_key \\
-e FORWARD_URL=http://slack-bot:3000/events \\
--restart unless-stopped \\
skooh/proxy
Step 3: Handle the URL Verification Challenge
Note: Slack's API evolves frequently. See Slack's Events API documentation for current setup instructions.
When you add your Skooh URL to a Slack app, Slack sends:
{
"type": "url_verification",
"token": "...",
"challenge": "abc123xyz"
}
Your handler must respond with the challenge value:
app.post('/events', express.json(), (req, res) => {
// Handle URL verification
if (req.body.type === 'url_verification') {
return res.json({ challenge: req.body.challenge });
}
// Handle other events...
res.status(200).send('OK');
});
Step 3: Respond Fast, Process Async
Slack's 3-second timeout is strict. Use this pattern:
const { Queue } = require('bull');
const eventQueue = new Queue('slack-events');
app.post('/events', express.json(), async (req, res) => {
// URL verification (must be immediate)
if (req.body.type === 'url_verification') {
return res.json({ challenge: req.body.challenge });
}
// Verify the request is from Slack
if (!verifySlackRequest(req)) {
return res.status(401).send('Invalid signature');
}
// Queue for async processing
await eventQueue.add({
type: req.body.event.type,
event: req.body.event,
team_id: req.body.team_id
});
// Respond immediately (within 3 seconds!)
res.status(200).send('OK');
});
// Process events asynchronously
eventQueue.process(async (job) => {
const { type, event } = job.data;
switch (type) {
case 'message':
await handleMessage(event);
break;
case 'app_mention':
await handleMention(event);
break;
case 'reaction_added':
await handleReaction(event);
break;
}
});
Verifying Slack Requests
Slack signs requests with a signature. Verify it:
const crypto = require('crypto');
function verifySlackRequest(req) {
const timestamp = req.headers['x-slack-request-timestamp'];
const signature = req.headers['x-slack-signature'];
// Reject old timestamps (replay protection)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
return false;
}
const sigBaseString = \`v0:\${timestamp}:\${req.rawBody}\`;
const mySignature = 'v0=' + crypto
.createHmac('sha256', process.env.SLACK_SIGNING_SECRET)
.update(sigBaseString)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(mySignature),
Buffer.from(signature)
);
}
Event Types to Handle
Common Slack events:
switch (event.type) {
case 'message':
// New message in channel
break;
case 'app_mention':
// Bot was @mentioned
break;
case 'reaction_added':
// Emoji reaction added
break;
case 'member_joined_channel':
// User joined a channel
break;
case 'channel_created':
// New channel created
break;
}
Testing with Replay
- Trigger an event in Slack (send a message, add a reaction)
- Find the webhook in Skooh dashboard
- Click Replay to test your handler again
- Debug without spamming your Slack workspace
Troubleshooting
"URL verification failed"
- Ensure your handler returns `{ challenge: req.body.challenge }`
- Check the Skooh proxy is forwarding correctly
- View the request in Skooh dashboard
"dispatch_failed" errors in Slack
- Your handler is taking >3 seconds
- Process events async and respond immediately
- Check Skooh dashboard for timing info
Duplicate events
- Slack retries if response is slow
- Use `event_id` for deduplication:
const processedEvents = new Set();
if (processedEvents.has(req.body.event_id)) {
return res.status(200).send('OK'); // Already processed
}
processedEvents.add(req.body.event_id);
Written by Skooh Team