Slack Webhooks
All available Slack webhook events
The Slack plugin automatically handles incoming webhooks from Slack. Point your Slack app's Event Subscriptions URL to your Corsair webhook endpoint, and the plugin will automatically process events and update your database.
New to Corsair? Learn about core concepts like webhooks, hooks, and multi-tenancy before setting up webhook handlers.
Full Implementation: For the complete, up-to-date list of all webhook events and their implementations, see the Slack plugin source code on GitHub.
Setup
Configure your webhook endpoint to handle Slack events:
import { processWebhook } from "corsair";
import { corsair } from "@/server/corsair";
export async function POST(request: Request) {
const headers = Object.fromEntries(request.headers);
const body = await request.json();
const result = await processWebhook(corsair, headers, body);
return result.response;
}In your Slack app settings, set the Event Subscriptions URL to:
https://your-domain.com/api/webhookFor multi-tenancy, include the tenant ID:
https://your-domain.com/api/webhook?tenantId=tenant-123Available Webhooks
challenge
challenge.challenge
Event Type: URL Verification Challenge
Slack sends this event when you first configure your webhook URL. Corsair automatically handles the challenge response.
When it fires: During initial webhook URL setup
Payload:
{
type: "url_verification",
challenge: "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P",
token: "Jhj5dZrVaK7ZwHHjRyZWjbDl"
}Note: You don't need to handle this event manually - Corsair responds automatically.
message
messages.message
Event Type: Message
Fires when a message is posted to a channel your app has access to.
When it fires:
- New message posted
- Message edited
- Message posted in a thread
Payload Structure:
{
type: "message",
subtype?: "bot_message" | "me_message" | "message_changed",
channel: "C1234567890",
user: "U1234567890",
text: "Hello, world!",
ts: "1234567890.123456",
thread_ts?: "1234567890.123456", // Present if message is in a thread
event_ts: "1234567890.123456",
channel_type: "channel" | "group" | "im" | "mpim"
}Example Usage:
slack({
webhookHooks: {
messages: {
message: {
after: async (ctx, result) => {
// Skip bot messages
if (result.data.bot_id) {
return;
}
console.log(`New message: ${result.data.text}`);
// Process the message
await processMessage(result.data);
},
},
},
},
})Key Fields:
| Field | Type | Description |
|---|---|---|
type | string | Always "message" |
subtype | string? | Message subtype (bot_message, etc.) |
channel | string | Channel ID where message was posted |
user | string | User ID who posted the message |
text | string? | Message text |
ts | string | Message timestamp (unique ID) |
thread_ts | string? | Parent message timestamp if in thread |
bot_id | string? | Bot ID if posted by a bot |
blocks | array? | Block Kit blocks |
attachments | array? | Message attachments |
created
channels.created
Event Type: Channel Created
Fires when a new channel is created.
When it fires: A new public or private channel is created
Payload Structure:
{
type: "channel_created",
channel: {
id: "C1234567890",
name: "new-channel",
name_normalized: "new-channel",
created: 1234567890,
creator: "U1234567890",
is_channel: true,
is_private: false,
is_archived: false
},
event_ts: "1234567890.123456"
}Example Usage:
slack({
webhookHooks: {
channels: {
created: {
after: async (ctx, result) => {
const channel = result.data.channel;
console.log(`New channel created: #${channel.name}`);
// Send welcome message
await ctx.api.messages.post({
channel: channel.id,
text: `Welcome to #${channel.name}!`,
});
},
},
},
},
})Key Fields:
| Field | Type | Description |
|---|---|---|
type | string | Always "channel_created" |
channel.id | string | New channel ID |
channel.name | string | Channel name |
channel.creator | string | User ID of creator |
channel.is_private | boolean | Whether channel is private |
event_ts | string | Event timestamp |
added
reactions.added
Event Type: Reaction Added
Fires when a reaction emoji is added to a message.
When it fires: A user adds an emoji reaction to a message, file, or file comment
Payload Structure:
{
type: "reaction_added",
user: "U1234567890",
reaction: "thumbsup",
item_user: "U0987654321",
item: {
type: "message",
channel: "C1234567890",
ts: "1234567890.123456"
},
event_ts: "1234567890.123456"
}Example Usage:
slack({
webhookHooks: {
reactions: {
added: {
after: async (ctx, result) => {
const { user, reaction, item } = result.data;
console.log(`${user} reacted with :${reaction}:`);
// Track reactions for analytics
await analytics.track("reaction_added", {
reaction,
channel: item.channel,
user,
});
},
},
},
},
})Key Fields:
| Field | Type | Description |
|---|---|---|
type | string | Always "reaction_added" |
user | string | User who added the reaction |
reaction | string | Emoji name (without colons) |
item_user | string | User who created the item |
item.type | string | Item type (message, file, file_comment) |
item.channel | string | Channel ID |
item.ts | string | Message timestamp |
event_ts | string | Event timestamp |
teamJoin
users.teamJoin
Event Type: Team Join
Fires when a new user joins the workspace.
When it fires: A new user is added to the workspace
Payload Structure:
{
type: "team_join",
user: {
id: "U1234567890",
name: "john.doe",
real_name: "John Doe",
email: "john@example.com",
profile: {
display_name: "John",
real_name: "John Doe",
email: "john@example.com",
image_72: "https://...",
// ... more profile fields
},
is_bot: false,
is_admin: false
},
cache_ts: 1234567890,
event_ts: "1234567890.123456"
}Example Usage:
slack({
webhookHooks: {
users: {
teamJoin: {
after: async (ctx, result) => {
const user = result.data.user;
// Send welcome DM
const dm = await ctx.api.channels.open({
users: user.id,
});
await ctx.api.messages.post({
channel: dm.channel.id,
text: `Welcome to the team, ${user.real_name}! 👋`,
});
},
},
},
},
})Key Fields:
| Field | Type | Description |
|---|---|---|
type | string | Always "team_join" |
user.id | string | User ID |
user.name | string | Username |
user.real_name | string | Full name |
user.email | string? | Email address |
user.profile | object | User profile information |
user.is_bot | boolean | Whether user is a bot |
event_ts | string | Event timestamp |
userChange
users.userChange
Event Type: User Change
Fires when a user's profile information changes.
When it fires:
- User updates their profile
- User changes their display name
- User updates their status
- Admin updates user information
Payload Structure:
{
type: "user_change",
user: {
id: "U1234567890",
name: "john.doe",
real_name: "John Doe",
profile: {
status_text: "In a meeting",
status_emoji: ":calendar:",
display_name: "John",
email: "john@example.com"
}
},
cache_ts: 1234567890,
event_ts: "1234567890.123456"
}Example Usage:
slack({
webhookHooks: {
users: {
userChange: {
after: async (ctx, result) => {
const user = result.data.user;
// Sync user data to your database
await syncUserProfile(user);
console.log(`User ${user.name} updated their profile`);
},
},
},
},
})Key Fields:
| Field | Type | Description |
|---|---|---|
type | string | Always "user_change" |
user | object | Updated user object |
user.profile | object | User profile with changes |
event_ts | string | Event timestamp |
created
files.created
Event Type: File Created
Fires when a file is uploaded to Slack.
When it fires: A user uploads a new file
Payload Structure:
{
type: "file_created",
file_id: "F1234567890",
user_id: "U1234567890",
file: {
id: "F1234567890"
},
event_ts: "1234567890.123456"
}Example Usage:
slack({
webhookHooks: {
files: {
created: {
after: async (ctx, result) => {
const fileId = result.data.file_id;
// Fetch full file details
const file = await ctx.api.files.get({
file: fileId,
});
console.log(`New file uploaded: ${file.name}`);
},
},
},
},
})Key Fields:
| Field | Type | Description |
|---|---|---|
type | string | Always "file_created" |
file_id | string | File ID |
user_id | string | User who uploaded the file |
event_ts | string | Event timestamp |
public
files.public
Event Type: File Made Public
Fires when a file is made publicly accessible.
When it fires: A file's visibility changes to public
Payload Structure:
{
type: "file_public",
file_id: "F1234567890",
user_id: "U1234567890",
file: {
id: "F1234567890"
},
event_ts: "1234567890.123456"
}Key Fields:
| Field | Type | Description |
|---|---|---|
type | string | Always "file_public" |
file_id | string | File ID |
user_id | string | User who made the file public |
event_ts | string | Event timestamp |
shared
files.shared
Event Type: File Shared
Fires when a file is shared to a channel.
When it fires: A file is posted to a channel
Payload Structure:
{
type: "file_shared",
file_id: "F1234567890",
user_id: "U1234567890",
file: {
id: "F1234567890"
},
channel_id: "C1234567890",
event_ts: "1234567890.123456"
}Example Usage:
slack({
webhookHooks: {
files: {
shared: {
after: async (ctx, result) => {
const { file_id, channel_id } = result.data;
// Fetch file details
const file = await ctx.api.files.get({
file: file_id,
});
console.log(`File shared in channel: ${file.name}`);
// Process or scan the file
await processSharedFile(file);
},
},
},
},
})Key Fields:
| Field | Type | Description |
|---|---|---|
type | string | Always "file_shared" |
file_id | string | File ID |
user_id | string | User who shared the file |
channel_id | string | Channel where file was shared |
event_ts | string | Event timestamp |
Webhook Hooks
Webhook hooks let you add custom logic that runs when Corsair processes a webhook event.
Before Hooks
Run before the webhook is processed. Use them to validate, filter, or modify incoming webhooks.
slack({
webhookHooks: {
messages: {
message: {
before: async (ctx, payload) => {
// Skip bot messages
if (payload.bot_id) {
throw new Error("Skipping bot message");
}
// Modify payload before processing
return { ctx, payload };
},
},
},
},
})After Hooks
Run after the webhook is processed and the database is updated. Use them for side effects, notifications, or triggering workflows.
slack({
webhookHooks: {
messages: {
message: {
after: async (ctx, result) => {
// This always runs after the message is saved
await notifyTeam(result.data);
await updateAnalytics(result.data);
},
},
},
},
})Multiple Webhook Hooks
You can add hooks for multiple webhook types:
slack({
webhookHooks: {
messages: {
message: {
after: async (ctx, result) => {
await processMessage(result.data);
},
},
},
channels: {
created: {
after: async (ctx, result) => {
await notifyChannelCreated(result.data);
},
},
},
reactions: {
added: {
after: async (ctx, result) => {
await trackReaction(result.data);
},
},
},
},
})See Webhooks for more details on webhook concepts and Hooks for the complete hooks documentation.
Configuring Events in Slack
To receive webhook events, you need to configure your Slack app:
-
Go to your app's settings at api.slack.com/apps
-
Navigate to Event Subscriptions
-
Enable events and set your Request URL
-
Under Subscribe to bot events, add the events you want to receive:
message.channels- Messages in public channelsmessage.groups- Messages in private channelsmessage.im- Direct messagesmessage.mpim- Group direct messageschannel_created- New channelsreaction_added- Reactions addedteam_join- New team membersuser_change- User profile changesfile_created- Files uploadedfile_public- Files made publicfile_shared- Files shared
-
Save your changes
Required OAuth Scopes:
Make sure your app has the necessary OAuth scopes:
channels:history- Read messages from public channelschannels:read- View basic channel informationusers:read- View usersreactions:read- View emoji reactionsfiles:read- View files
See the Slack API documentation for a complete list of events and required scopes.