Skip to main content
This guide shows you how to set up webhooks to automatically post Slack notifications whenever a model is updated in your Speckle project.
Adaptable to Other Services: While this guide uses Slack as an example, the same pattern works for any service that accepts webhooks or HTTP POST requests, such as:
  • Microsoft Teams
  • Discord
  • Email services (SendGrid, Mailgun)
  • Project management tools (Jira, Asana, Trello)
  • Custom APIs and databases
  • CI/CD systems
Simply replace the Slack webhook URL and message format with your target service’s API.

Overview

You’ll create a webhook endpoint that receives Speckle webhook events and forwards them to Slack’s Incoming Webhooks API. This creates a bridge between Speckle events and Slack notifications. The same approach can be adapted for any other service that accepts HTTP POST requests.

Prerequisites

  • A Speckle project where you have admin or edit permissions
  • A Slack workspace where you can create webhooks (or another service that accepts HTTP POST requests)
  • A web endpoint to receive webhook events (can be a serverless function, cloud function, or traditional server)

Step 1: Create a Slack Incoming Webhook

  1. Go to your Slack workspace
  2. Navigate to Slack Apps
  3. Click Create New AppFrom scratch
  4. Name your app (e.g., “Speckle Notifications”) and select your workspace
  5. Go to Incoming Webhooks in the left sidebar
  6. Toggle Activate Incoming Webhooks to On
  7. Click Add New Webhook to Workspace
  8. Select the channel where you want notifications (e.g., #speckle-updates)
  9. Click Allow
  10. Copy the Webhook URL (it looks like https://hooks.slack.com/services/...)
Keep your Slack webhook URL secure. Anyone with this URL can post messages to your Slack channel.

Step 2: Create Your Webhook Endpoint

Create an endpoint that receives Speckle webhooks and forwards them to Slack. Here’s an example implementation:
const express = require('express');
const axios = require('axios');
const app = express();

app.use(express.json());

// Your Slack webhook URL from Step 1
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;

app.post('/webhook', async (req, res) => {
  try {
    const payload = req.body.payload;
    const event = payload.event;
    
    // Only process model_update events (branch_update in payload)
    if (event.event_name === 'branch_update') {
      const modelName = payload.stream?.name || 'Unknown Project';
      const branchName = event.data?.branchName || 'Unknown Model';
      const userName = payload.user?.name || 'Unknown User';
      
      // Format the Slack message
      const slackMessage = {
        text: `📦 Model Updated in ${modelName}`,
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: `*Model Updated*\n*Project:* ${modelName}\n*Model:* ${branchName}\n*Updated by:* ${userName}`
            }
          },
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: `<${payload.server.canonicalUrl}/projects/${payload.streamId}|View in Speckle>`
            }
          }
        ]
      };
      
      // Send to Slack
      await axios.post(SLACK_WEBHOOK_URL, slackMessage);
    }
    
    // Always return 200 to acknowledge receipt
    res.status(200).send('OK');
  } catch (error) {
    console.error('Error processing webhook:', error);
    // Still return 200 to prevent retries for Slack errors
    res.status(200).send('OK');
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Webhook server running on port ${PORT}`);
});

Serverless Function Example (AWS Lambda)

Here’s the same functionality as a serverless Lambda function:
// AWS Lambda function handler
const axios = require('axios');

const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;

exports.handler = async (event) => {
  try {
    const payload = JSON.parse(event.body).payload;
    const webhookEvent = payload.event;
    
    // Only process model_update events (branch_update in payload)
    if (webhookEvent.event_name === 'branch_update') {
      const modelName = payload.stream?.name || 'Unknown Project';
      const branchName = webhookEvent.data?.branchName || 'Unknown Model';
      const userName = payload.user?.name || 'Unknown User';
      
      const slackMessage = {
        text: `📦 Model Updated in ${modelName}`,
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: `*Model Updated*\n*Project:* ${modelName}\n*Model:* ${branchName}\n*Updated by:* ${userName}`
            }
          }
        ]
      };
      
      await axios.post(SLACK_WEBHOOK_URL, slackMessage);
    }
    
    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'OK' })
    };
  } catch (error) {
    console.error('Error:', error);
    return {
      statusCode: 200,
      body: JSON.stringify({ message: 'OK' })
    };
  }
};

Step 3: Deploy Your Webhook Endpoint

Your webhook endpoint doesn’t need to be a full server - it just needs to be a publicly accessible HTTP endpoint that can receive POST requests. You can use: AWS Lambda:
  • Create a Lambda function with an API Gateway trigger
  • Deploy your code and get a public URL
  • Pay only for requests (very cost-effective)
Google Cloud Functions:
  • Deploy as an HTTP function
  • Automatically gets a public HTTPS URL
  • Scales automatically
Azure Functions:
  • Create an HTTP-triggered function
  • Get a public endpoint URL
  • Serverless scaling
Vercel / Netlify Functions:
  • Deploy as a serverless function
  • Automatic HTTPS and scaling
  • Great for simple integrations

Traditional Servers

  • Cloud platforms: Heroku, Railway, Render, DigitalOcean
  • Your own server: Any server with a public IP
  • Local testing: Use ngrok to create a temporary public URL
Why Serverless? Serverless functions are ideal for webhooks because:
  • They only run when receiving requests (cost-effective)
  • Automatic scaling handles traffic spikes
  • No server management required
  • Built-in HTTPS support
  • Pay-per-use pricing model
For production use, make sure your endpoint:
  • Uses HTTPS (automatic with most serverless platforms)
  • Verifies webhook signatures (see Webhook Security)
  • Handles errors gracefully
  • Returns 200 OK quickly (process Slack requests asynchronously if needed)

Step 4: Configure the Speckle Webhook

  1. Navigate to your Speckle project
  2. Go to SettingsWebhooks
  3. Click ADD WEBHOOK
  4. Fill in the configuration:
    • URL: Your webhook endpoint URL (e.g., https://your-server.com/webhook)
    • Events: Select model_update (this maps to branch_update in the payload)
    • Webhook name: “Slack Notifications”
    • Secret: (Optional but recommended) Set a secret for signature verification
  5. Click Create

Step 5: Test the Integration

  1. Update a model in your Speckle project (e.g., rename it or modify its description)
  2. Check your Slack channel - you should see a notification
  3. Check your webhook endpoint logs to verify it received the event

Customizing the Message

You can customize the Slack message format to include more information or different styling:
// Example: Include more details
const slackMessage = {
  text: `📦 Model Updated: ${branchName}`,
  blocks: [
    {
      type: 'header',
      text: {
        type: 'plain_text',
        text: 'Model Updated'
      }
    },
    {
      type: 'section',
      fields: [
        {
          type: 'mrkdwn',
          text: `*Project:*\n${modelName}`
        },
        {
          type: 'mrkdwn',
          text: `*Model:*\n${branchName}`
        },
        {
          type: 'mrkdwn',
          text: `*Updated by:*\n${userName}`
        },
        {
          type: 'mrkdwn',
          text: `*Time:*\n${new Date().toLocaleString()}`
        }
      ]
    },
    {
      type: 'actions',
      elements: [
        {
          type: 'button',
          text: {
            type: 'plain_text',
            text: 'View in Speckle'
          },
          url: `${payload.server.canonicalUrl}/projects/${payload.streamId}`
        }
      ]
    }
  ]
};

Extending to Other Events

You can extend this to notify on other events by checking different event_name values:
  • commit_create - New version created
  • commit_update - Version updated
  • comment_created - New comment added
  • issue_created - New issue created
Example:
if (event.event_name === 'branch_update') {
  // Handle model update
} else if (event.event_name === 'commit_create') {
  // Handle version creation
  const versionMessage = {
    text: `✨ New Version Created: ${event.data.message || 'No message'}`
  };
  await axios.post(SLACK_WEBHOOK_URL, versionMessage);
}

Security Considerations

Important Security Notes:
  1. Verify Webhook Signatures: Always verify that webhooks are actually coming from Speckle. See Webhook Security for signature verification code.
  2. Protect Your Slack Webhook URL: Keep your Slack webhook URL in environment variables, never commit it to version control.
  3. Use HTTPS: Always use HTTPS for your webhook endpoint in production.
  4. Handle Errors Gracefully: Return 200 OK even if Slack posting fails to prevent infinite retries.

Troubleshooting

  • Verify your webhook endpoint is accessible (test with a simple curl request)
  • Check that you selected model_update in the Speckle webhook configuration
  • Verify your Slack webhook URL is correct
  • Check your endpoint logs for errors
  • Ensure your endpoint returns 200 OK quickly
Use ngrok to create a public URL that forwards to your local server:
  1. Run ngrok http 3000 (or your local port)
  2. Copy the ngrok URL (e.g., https://abc123.ngrok.io)
  3. Use this URL in your Speckle webhook configuration
  4. Your local server will receive webhook events
Alternatively, you can deploy to a serverless function in a development environment for testing.
No! If you use serverless functions (AWS Lambda, Google Cloud Functions, Azure Functions, Vercel, etc.), you don’t need a running server. The function only executes when it receives a webhook request, making it very cost-effective. You only pay for the requests you receive.
Yes! Add conditional logic in your webhook handler:
// Only notify for specific models
const allowedModels = ['production', 'staging'];
if (allowedModels.includes(branchName)) {
  // Send Slack notification
}