Documentation Index
Fetch the complete documentation index at: https://docs.getcore.me/llms.txt
Use this file to discover all available pages before exploring further.
Webhook-Based Integrations
Webhook-based integrations receive HTTP callbacks from external services for real-time synchronization. This reference guide uses Slack as the detailed example.
When to Use Webhooks
Choose webhook-based integrations when:
- The service provides real-time webhook events
- You need instant synchronization (chat messages, notifications)
- Events are user-triggered and need immediate capture
- The service has robust webhook infrastructure
Architecture Overview
External Service (Slack)
↓
Webhook Event
↓
Core Webhook Endpoint
↓
Integration IDENTIFY → Extract user ID
↓
Integration PROCESS → Create activity
↓
Knowledge Graph
Event Flow
1. SETUP Event
When a user connects their account via OAuth:
// account-create.ts
export async function integrationCreate(data: any) {
const { oauthResponse } = data;
// Get user info from service
const user = await getSlackUserInfo(oauthResponse.access_token);
return [{
type: 'account',
data: {
accountId: user.user_id,
config: {
access_token: oauthResponse.access_token,
refresh_token: oauthResponse.refresh_token,
// Pass token to MCP tools
mcp: { tokens: { access_token: oauthResponse.access_token } }
},
settings: {
username: user.user,
team_id: user.team_id
}
}
}];
}
2. IDENTIFY Event
When a webhook event arrives, extract the user identifier for routing:
// index.ts
case IntegrationEventType.IDENTIFY:
// Extract user ID from Slack event
return [{
type: 'identifier',
data: eventPayload.eventBody.event.event.user ||
eventPayload.eventBody.event.event.message.user
}];
This tells Core which user’s knowledge graph to update with the activity.
3. PROCESS Event
Process the webhook event and create activity messages:
// create-activity.ts
export const createActivityEvent = async (eventData: any, config: any) => {
const event = eventData.event;
const accessToken = config.access_token;
// Handle different event types
if (event.type === 'reaction_added' && event.reaction === 'eyes') {
// User reacted with 👀 emoji - capture for knowledge graph
// 1. Get the original message
const message = await getMessage(accessToken, event.item.channel, event.item.ts);
// 2. Get user details and channel info
const [userDetails, channelInfo] = await Promise.all([
getUserDetails([message.user], accessToken),
getConversationInfo(accessToken, event.item.channel)
]);
// 3. Build contextual description
const text = `User ${userDetails[0].real_name} reacted with eyes emoji ` +
`in channel ${channelInfo.name}. Content: '${message.text}'`;
// 4. Get permalink for deep linking
const permalink = await getPermalink(accessToken, event.item.channel, event.item.ts);
// 5. Return activity message
return [{
type: 'activity',
data: {
text,
sourceURL: permalink.data.permalink
}
}];
}
if (event.type === 'message') {
// Handle message events
const text = `Message in channel: '${event.text}'`;
const permalink = await getPermalink(accessToken, event.channel, event.ts);
return [{
type: 'activity',
data: {
text,
sourceURL: permalink.data.permalink
}
}];
}
// Return empty array for unhandled events
return [];
};
Full Example: Slack Integration
spec.json
{
"name": "Slack extension",
"key": "slack",
"description": "Connect your workspace to Slack",
"icon": "slack",
"auth": {
"OAuth2": {
"token_url": "https://slack.com/api/oauth.v2.access",
"authorization_url": "https://slack.com/oauth/v2/authorize",
"scopes": [
"channels:read",
"channels:history",
"chat:write",
"reactions:read",
"reactions:write",
"users:read",
"users.profile:read"
],
"scope_identifier": "user_scope",
"scope_separator": ","
}
},
"mcp": {
"type": "stdio",
"url": "https://integrations.heysol.ai/slack/mcp/slack-mcp-server",
"args": [],
"env": {
"SLACK_MCP_XOXP_TOKEN": "${config:access_token}",
"SLACK_MCP_ADD_MESSAGE_TOOL": true
}
}
}
index.ts
import { integrationCreate } from './account-create';
import { createActivityEvent } from './create-activity';
import {
IntegrationCLI,
IntegrationEventPayload,
IntegrationEventType,
Spec,
} from '@redplanethq/sdk';
export async function run(eventPayload: IntegrationEventPayload) {
switch (eventPayload.event) {
case IntegrationEventType.SETUP:
return await integrationCreate(eventPayload.eventBody);
case IntegrationEventType.IDENTIFY:
// Extract user ID from Slack event
return [{
type: 'identifier',
data: eventPayload.eventBody.event.event.user ||
eventPayload.eventBody.event.event.message.user,
}];
case IntegrationEventType.PROCESS:
// Process webhook event
return createActivityEvent(
eventPayload.eventBody.eventData,
eventPayload.config
);
default:
return [{ type: 'error', data: `Unknown event type: ${eventPayload.event}` }];
}
}
class SlackCLI extends IntegrationCLI {
constructor() {
super('slack', '1.0.0');
}
protected async handleEvent(eventPayload: IntegrationEventPayload): Promise<any> {
return await run(eventPayload);
}
protected async getSpec(): Promise<Spec> {
// Return spec configuration
return {
name: 'Slack extension',
key: 'slack',
// ... rest of spec
};
}
}
function main() {
const slackCLI = new SlackCLI();
slackCLI.parse();
}
main();
create-activity.ts
import axios from 'axios';
interface SlackActivityCreateParams {
text: string;
sourceURL: string;
}
function createActivityMessage(params: SlackActivityCreateParams) {
return {
type: 'activity',
data: {
text: params.text,
sourceURL: params.sourceURL,
},
};
}
async function getMessage(accessToken: string, channel: string, ts: string) {
const result = await axios.get('https://slack.com/api/conversations.history', {
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
params: {
channel,
latest: ts,
inclusive: true,
limit: 1,
},
});
return result.data.messages?.[0];
}
async function getConversationInfo(accessToken: string, channel: string) {
const result = await axios.get('https://slack.com/api/conversations.info', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
params: { channel },
});
return result.data.channel;
}
async function getPermalink(accessToken: string, channel: string, ts: string) {
return await axios.get(
`https://slack.com/api/chat.getPermalink?channel=${channel}&message_ts=${ts}`,
{
headers: { Authorization: `Bearer ${accessToken}` },
}
);
}
export const createActivityEvent = async (eventData: any, config: any) {
const event = eventData.event;
if (!config) {
throw new Error('Integration configuration not found');
}
const accessToken = config.access_token;
// Handle reaction_added events
if (event.type === 'reaction_added' && event.reaction === 'eyes') {
const channel = event.item.channel;
const ts = event.item.ts;
// Fetch message and context
const [eventMessage, conversationInfo] = await Promise.all([
getMessage(accessToken, channel, ts),
getConversationInfo(accessToken, channel)
]);
// Build activity text
const text = `User reacted with eyes emoji in channel ${conversationInfo.name}. ` +
`Content: '${eventMessage.text}'`;
// Get permalink
const permalinkResponse = await getPermalink(accessToken, channel, ts);
return [createActivityMessage({
text,
sourceURL: permalinkResponse.data.permalink
})];
}
// Handle message events
if (event.type === 'message') {
const text = `Message in channel: '${event.text}'`;
const permalinkResponse = await getPermalink(accessToken, event.channel, event.ts);
return [createActivityMessage({
text,
sourceURL: permalinkResponse.data.permalink
})];
}
// Return empty array for unhandled events
return [];
};
Best Practices
1. Event Filtering
Only process events that are relevant to the user’s knowledge graph:
// Good: Filter for specific reactions that indicate importance
if (event.type === 'reaction_added' && event.reaction === 'eyes') {
// This is important, capture it
}
// Good: Filter for direct messages
if (conversationInfo.is_im) {
// Direct message is usually important
}
// Bad: Capture every message (too noisy)
if (event.type === 'message') {
// This creates too much noise
}
2. Context Enrichment
Provide rich context by fetching related data:
// Fetch related data in parallel for efficiency
const [userDetails, channelInfo, message] = await Promise.all([
getUserDetails([userId], accessToken),
getConversationInfo(accessToken, channelId),
getMessage(accessToken, channelId, timestamp)
]);
// Build descriptive activity
const text = `${userDetails[0].real_name} mentioned you in ` +
`${channelInfo.is_private ? 'private' : 'public'} channel ` +
`${channelInfo.name}: "${message.text}"`;
3. Deep Linking
Always provide sourceURL for navigation back to the original content:
const permalinkResponse = await axios.get(
`https://slack.com/api/chat.getPermalink?channel=${channel}&message_ts=${ts}`,
{ headers: { Authorization: `Bearer ${accessToken}` } }
);
return [{
type: 'activity',
data: {
text,
sourceURL: permalinkResponse.data.permalink // Essential!
}
}];
4. Error Handling
Handle errors gracefully without breaking the integration:
export const createActivityEvent = async (eventData: any, config: any) => {
try {
// Process event
const activities = await processEvent(eventData, config);
return activities;
} catch (error) {
console.error('Error processing webhook:', error);
// Return empty array instead of throwing
return [];
}
};
5. User Identification
Properly extract user IDs for different event types:
case IntegrationEventType.IDENTIFY:
// Handle different event structures
const userId = eventPayload.eventBody.event.event.user || // reactions, messages
eventPayload.eventBody.event.event.message.user || // threaded messages
eventPayload.eventBody.event.event.item.user; // other events
return [{ type: 'identifier', data: userId }];
Testing Webhooks Locally
To test webhooks during development:
-
Use ngrok for local tunneling:
-
Configure webhook URL in service (Slack):
https://your-ngrok-url.ngrok.io/webhooks/slack
-
Test events by triggering actions in Slack
-
Check logs for event payloads and processing
Common Event Types
Slack
message - New message posted
reaction_added - Emoji reaction added
reaction_removed - Emoji reaction removed
member_joined_channel - User joined channel
app_mention - Bot was mentioned
Discord
MESSAGE_CREATE - New message
MESSAGE_REACTION_ADD - Reaction added
GUILD_MEMBER_ADD - Member joined server
Linear
Issue - Issue created/updated
Comment - Comment added
IssueLabel - Label changed
Next Steps