Intermediate

Socket Mode implementation

Socket Mode gives your app a way to connect to Slack, without exposing a public HTTP endpoint.

Socket Mode is intended for internal apps in development or apps that need to be deployed behind a firewall. It is not intended for widely distributed apps.

Instead, when your app receives payloads—from the Events API and from interactive components—it happens through a private WebSocket URL that's generated at runtime.

Check out our introduction to Socket Mode to learn what to expect.

We recommend using our Bolt frameworks or SDKs for Java, Javascript, or Python to handle the details of Socket Mode. It's easier, and you get access to all the other pleasant features of our SDKs.

This guide will help you implement a Socket Mode connection without an SDK, so read on if that's your goal.

Overview

First, read our introduction article to get a feel for Socket Mode and whether it's the best choice for your app.

If you choose to implement a Socket Mode app without using our Bolt frameworks or SDKs for Java, Javascript, or Python to handle the details, this guide is for you.

As a reminder, Socket Mode is only available for apps using new, granular permissions. If you created your app on or after December of 2019, good news: your app already uses the new permissions. Otherwise, you may have to migrate your classic Slack app to use granular permissions before turning on Socket Mode.

You'll need a library or programming language that supports the WebSocket protocol.

Connections refresh regularly. You should be ready to receive and connect to new WebSocket URLs as quickly as possible to maintain service.

Once you've got all that, read on.


Initial setup

We covered initial app setup for Socket Mode in the introduction to Socket Mode.

As a refresher: create your Slack app, and turn the Socket Mode toggle on in your app config under Socket Mode.

Create an app-level token in your app config, which you can find at any time under Basic Information. You'll need the token in the next step to generate a URL for your WebSocket connection to Slack.


Call the apps.connections.open endpoint

Call the apps.connections.open endpoint with your app-level token to receive a WebSocket URL:

curl -X POST "https://slack.com/api/apps.connections.open" \
-H "Content-type: application/x-www-form-urlencoded" \
-H "Authorization: Bearer xapp-1-123"

Remember to send the token in the Authorization header, not as a parameter.

You'll receive a URL in response:

{
    "ok": true,
    "url": "wss:\/\/wss.slack.com\/link\/?ticket=1234-5678"
}

You'll notice that, with Socket Mode turned on, your app config doesn't require or even allow you to enter a Request URL. That's because this WebSocket URL replaces the public Request URL that Slack would have sent payloads to.

You can turn off Socket Mode at any time to return to the direct HTTP protocol for events and interactive components. If you've already set a Request URL, it'll be saved for later once you enter Socket Mode. When you turn Socket Mode off, the Request URL will be used once again to receive events.


Connect to the WebSocket

Use your WebSocket library to connect to the URL specified in the above response.

Here's an example in Javascript:

if (response.ok) {
  let wssUrl = response.url;
  let socket = new WebSocket(wssUrl);

  socket.onopen = function(e) {
    // connection established
  }

  socket.onmessage = function(event) {
    // application received message
  }
}

An app can have a maximum of 10 simultaneous connections.

One helpful trick: you can append &debug_reconnects=true to your WebSocket URL when you connect to it in order to make the connection time significantly shorter (360 seconds). That way, you can test and debug reconnects without waiting around.

After you connect to the WebSocket, Slack will send a hello message:

{
    "type": "hello",
    "connection_info": {
        "app_id": "A1234"
    },
    "num_connections": 1,
    "debug_info": {
        "host": "applink-….",
        "started" "2020-10-11 12:12:12.120",
        "build_number": 54,
        "approximate_connection_time": 3600
    }
}

The approximate_connection_time (in seconds) can be used to estimate how long the connection will persist until Slack refreshes it.


Gracefully handle disconnects

Expect disconnects to your WebSocket connection. These may happen when you toggle off Socket Mode in the app config, or for other reasons.

Here's a disconnect message you'll receive if you toggle off Socket Mode:

{
  "type": "disconnect",
  "reason": "link_disabled",
  "debug_info": {
    "host": "wss-111.slack.com"
  }
}

No matter what, you'll need to handle connection refreshes once every few hours.

You may receive a warning about 10 seconds before the disconnect:

{
  "type": "disconnect",
  "reason": "warning",
  "debug_info": {
    "host": "wss-111.slack.com"
  }
}

Even if you don't receive a warning, you'll still want to expect a refresh_requested message:

{
  "type" : "disconnect",
  "reason": "refresh_requested",
  "debug_info": {
    "host": "wss-111.slack.com"
  }
}

You may want to use multiple connections in order to maintain uptime during a connection restart. Check out the section on multiple connections for more details.


Receive events

Event payloads sent to your app via Socket Mode are identical to the typical Events API payloads, with some additional metadata:

{
  "payload": <event_payload>,
  "envelope_id": <unique_identifier_string>,
  "type": <event_type_enum>,
  "accepts_response_payload": <accepts_response_payload_bool>
}

You can use the accepts_response_payload to determine whether a response can include additional information.

Your app still needs to acknowledge receiving each event so that Slack knows whether to retry. Read on to see how to send acknowledgments. While acknowledging each event is required, there's no need to verify or validate inbound events, because you're receiving the events over a pre-authenticated websocket. Note that this is a different pattern from receiving events directly over HTTP, where validation is required for each event.

Acknowledge

Use the envelope_id field in the object you receive from your websocket to send a response back to Slack acknowledging that you've received the event:

{
    "envelope_id": <$unique_identifier_string>,
    "payload": <$payload_shape> // optional
}

Multiple connections

Socket Mode allows your app to maintain up to 10 open WebSocket connections at the same time.

When multiple connections are active, each payload may be sent to any of the connections. It's best not to assume any particular pattern for how payloads will be distributed across multiple open connections.

There are a few reasons to make use of multiple active connections:

  • If you'd like to handle a scheduled connection restart gracefully, you can generate an additional connection before the restart occurs.
  • If you're having trouble keeping up with a large throughput of events from one connection, multiple connections can allow you to load balance.
  • If you'd like to gracefully restart your app's services, you can use multiple connections for temporary active-active redundancy.

Use interactive components

Interactive components, like events, send payloads as normal to your app—along with additional metadata specific to Socket Mode. Here's the general structure to expect:

{
  "payload": <interactive_component_payload>,
  "envelope_id": <unique_identifier_string>,
  "type": <event_type_enum>,
  "accepts_response_payload": <accepts_response_payload_bool>
}

We've got examples of interactive components sent to your app via Socket Mode below:

Slash commands

Here's what's sent:

{
  "payload": {
    "token": "bHKJ2n9AW6Ju3MjciOHfbA1b",
    "team_id": "T0SNL8S4S",
    "team_domain": "maria",
    "channel_id": "C15SASXJ6",
    "channel_name": "general",
    "user_id": "U0SNL8SV8",
    "user_name": "rainer",
    "command": "/randorilke",
    "text": "",
    "response_url": "https://rilke.slack.com/commands/T0SNL8S4S/37053613554/YMB2ZESDLNjNLqSFZ1quhNAh",
    "trigger_id": "37053613634.26768298162.440952c06ef4de2653466a48fe495f93"
  },
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545",
  "type": "slash_commands",
  "accepts_response_payload": true
}

Here's an example response from your app:

{
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545",
  "payload": {
    "blocks": [
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "Book of Hours"
        }
      },
      {
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "Duino Elegies"
        }
      }
    ]
  }
}

Block Kit buttons

Here's what your app receives:

{
  "payload": {
    "type": "block_actions",
    "team": {
      "id": "T9TK3CUKW",
      "domain": "Duino"
    },
    "user": {
      "id": "UA8RXUSPL",
      "username": "RMR",
      "team_id": "T9TK3CUKW"
    },
    "api_app_id": "AABA1ABCD",
    "token": "9s8d9as89d8as9d8as989",
    "container": {
      "type": "message_attachment",
      "message_ts": "1548261231.000200",
      "attachment_id": 1,
      "channel_id": "CBR2V3XEX",
      "is_ephemeral": false,
      "is_app_unfurl": false
    },
    "trigger_id": "12321423423.333649436676.d8c1bb837935619ccad0f624c448ffb3",
    "channel": {
      "id": "CBR2V3XEX",
      "name": "review-updates"
    },
    "message": {
      "bot_id": "BAH5CA16Z",
      "type": "message",
      "text": "Who if I cried out would hear me.",
      "user": "UAJ2RU415",
      "ts": "1548261231.000200",
      ...
    },
    "response_url": "https://hooks.slack.com/actions/AABA1ABCD/1232321423432/D09sSasdasdAS9091209",
    "actions": [{
      "action_id": "WaXA",
      "block_id": "=qXel",
      "text": {
        "type": "plain_text",
        "text": "View",
        "emoji": true
      },
      "value": "click_me_123",
      "type": "button",
      "action_ts": "1548426417.840180"
    }]
  },
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545",
  "type": "interactive",
  "accepts_response_payload": true
}

Here's an example response from your app:

{
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545"
}

App home interaction

Here's what your app receives:

{
  "payload": {
    "type": "app_home_opened",
    "user": "U061F7AUR",
    "channel": "D0LAN2Q65",
    "event_ts": "1515449522000016",
    "tab": "home",
    "view": {
      "id": "VPASKP233",
      "team_id": "T21312902",
      "type": "home",
      "blocks": [
        ...
      ],
      "private_metadata": "",
      "callback_id": "",
      "state":{
        ...
      },
      "hash":"1231232323.12321312",
      "clear_on_close": false,
      "notify_on_close": false,
      "root_view_id": "VPASKP233",
      "app_id": "A21SDS90",
      "external_id": "",
      "app_installed_team_id": "T21312902",
      "bot_id": "BSDKSAO2"
    }
  }
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545",
  "type": "events_api",
  "accepts_response_payload": false
}

Here's an example response from your app:

{
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545"
}

Here's what your app receives:

{
  "payload": {
    "type": "view_submission",
    "team": { ... },
    "user": { ... },
    "view": {
      "id": "VNHU13V36",
      "type": "modal",
      "title": { ... },
      "submit": { ... },
      "blocks": [ ... ],
      "private_metadata": "shhh-its-secret",
      "callback_id": "modal-with-inputs",
      "state": {
        "values": {
          "multi-line": {
            "ml-value": {
              "type": "plain_text_input",
              "value": "Archaic torso of Apollo"
            }
          }
        }
      },
      "hash": "156663117.cd33ad1f"
    }
  },
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545",
  "type": "interactive",
  "accepts_response_payload": true
}

Here's an example response from your app:

{
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545",
  "payload": {
    "response_action": "update",
    "view": {
      "type": "modal",
      "callback_id": "updated-view-id",
      "title": {
        "type": "plain_text",
        "text": "Updated view"
      },
      "blocks": [{
        "type": "section",
        "text": {
          "type": "plain_text",
          "text": "You must change your life."
        }
      }]
    }
  }
}

Here's what your app receives:

{
  "payload": {
    "type": "block_suggestion",
    "user": {
      "id": "U012A1BCJ",
      "name": "panther"
    },
    "team": {
      "id": "T012AB0A1",
      "domain": "rilke"
    },
    "block_id": "search-block",
    "action_id": "seach-action",
    "value": "an",
    "view": {"id": "V111", "type": "modal", "callback_id": "view-id"}
  },
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545",
  "type": "interactive",
  "accepts_response_payload": true
}

Here's an example response from your app:

{
  "envelope_id": "dbdd0ef3-1543-4f94-bfb4-133d0e6c1545",
  "payload": {
    "options": [
      {
        "text": {
          "type": "plain_text",
          "text": "Give me your hand"
        },
        "value": "AI-2323"
      },
      {
        "text": {
          "type": "plain_text",
          "text": "Beauty and terror"
        },
        "value": "SUPPORT-42"
      }
    ]
  }
}
Recommended reading

Was this page helpful?