Full Blog TOC

Full Blog Table Of Content with Keywords Available HERE

Monday, September 7, 2026

Slack Integration in GO



In this post we will review how to integrate a GO application with Slack.

There are two flows we want to support. The first is sending a notification to a channel, for example when an operation completes. The second is receiving a message from a user and replying in a thread.

These flows use different Slack features and require different configuration. Before writing the code, let's understand what each one needs.

Notifications and Chat

First notice that an application is represented in Slack as a bot.

For notifications I've used an incoming webhook. Slack creates a URL associated with a channel, and the application sends an HTTP POST to this URL. The word incoming is from Slack's point of view: the message goes from our application into Slack.

For chat I've used the Events API to receive bot mentions, and the Web API method chat.postMessage to send replies. Slack sends an HTTP POST to our application when someone mentions the bot. Our application processes the event and uses a bot token to reply.

A webhook is enough for the notification flow. It does not subscribe the application to user messages. Chat requires a bot token, a signing secret, and a public callback URL. Both flows can use the same Slack app.

A. Slack Setup

1. Create a Slack App

Open Slack Apps, select Create New App → From scratch, and choose an app name and workspace. The following settings are all under this app.

2. Notifications: Enable Incoming Webhooks

Open Incoming Webhooks and enable Activate Incoming Webhooks. Select Add New Webhook to Workspace, choose the channel that should receive notifications, and approve access.

Copy the generated webhook URL. In the GO application this value is called WebhookUrl. The selected channel is associated with the URL, so our notification request only needs to contain the message text.

This setting is required only for webhook notifications. If all you need is sending notifications, you can skip the remaining chat setup.

3. Chat: Add Bot Permissions

Open OAuth & Permissions → Scopes → Bot Token Scopes and add:

  • app_mentions:read — receive events when someone mentions the bot.
  • chat:write — send messages using the bot token.

The first permission is used for receiving chat events. The second is used for sending the reply. Adding permissions does not enable event delivery; we will configure the event subscription separately.

4. Chat: Install the App and Copy the Bot Token

Under OAuth & Permissions, select Install to Workspace and approve the requested access. Copy the Bot User OAuth Token, which starts with xoxb-.

In the application this is BotToken. It is sent in the Authorization header when calling chat.postMessage. If you change scopes after installation, reinstall the app to approve the updated permissions.

5. Chat: Copy the Signing Secret

Open Basic Information → App Credentials → Signing Secret and copy the value. In the application this is SigningSecret.

The signing secret is used to validate requests coming from Slack. The bot token is used for requests going to Slack. These are two different credentials with different purposes.

6. Chat: Configure Event Subscriptions

Create a public TLS valid endpoint in you application. This is a public callback URL that Slack will call. The receiving service must be running and reachable over HTTPS before Slack can verify it. This is our application's incoming event URL. It is different from WebhookUrl, which belongs to Slack and receives outgoing notifications.

Open Event Subscriptions, enable Enable Events, and paste that URL into Request URL. Slack sends a verification challenge to the application. Once our application returns the challenge, Slack marks the URL as Verified.

Under Subscribe to bot events, add app_mention and save the changes. Reinstall the app if Slack asks for updated permissions.

7. Chat: Invite the Bot to a Channel

In the Slack channel, use /invite @YourBotName. Users can now address the application by mentioning the bot.

This setup receives bot mentions. Direct messages and channel messages without a mention require additional event subscriptions and permissions.

B. GO Implementation

Now that we have the Slack settings, we can connect them to the application. 

1. Save the Integration Configuration in our application

{

  "WebhookUrl": "https://hooks.slack.com/services/your-webhook",
  "BotToken": "xoxb-your-bot-token",
  "SigningSecret": "your-signing-secret"
}

Notice that the application also need to listen on the 
public TLS valid endpoint.

2. Send Notifications Using a Webhook

The webhook payload is simple:

type SlackNotification struct {
    MessageText string `json:"text"`
}

The Slack wrapper checks that the integration is enabled and that WebhookUrl is configured. It then sends the payload to that URL:

payload := &SlackNotification{MessageText: message}
var responseBody string
_, _, err := s.webClient.SendRequestWrapped(
    http.MethodPost, webhookUrl, payload, nil, &responseBody,
)
kiterr.RaiseIfError(err)

No bot token or callback URL is required for this request. The webhook URL already identifies the destination and authorizes the notification.

3. Receive Chat Events

The application listening on the public TLS valid endpoint reads the original body and headers, validates and processes the Slack request, then returns the HTTP response information.

Keeping the original request body is important. Slack signs the exact body bytes. Parsing the JSON and serializing it again could change those bytes and cause signature validation to fail.

4. Validate the Signature and Timestamp

Slack supplies X-Slack-Request-Timestamp and X-Slack-Signature. Our application requires one value for each header and checks the timestamp. This rejects requests outside the allowed time window, including timestamps too far in the future.

The signature is an HMAC-SHA256 calculated using SigningSecret and the following input:

v0:<request timestamp>:<original request body>

The relevant code is:

mac := hmac.New(sha256.New, []byte(secret))
_, err := mac.Write([]byte("v0:" + timestamp + ":"))
kiterr.RaiseIfError(err)
_, err = mac.Write(c.requestMessage.RequestBody)
kiterr.RaiseIfError(err)

expectedSignature := "v0=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expectedSignature), []byte(signature))

Invalid requests receive HTTP 401. The timestamp check limits how long a signed request can be reused; it does not detect duplicate events within that window.

5. Handle URL Verification

Slack sends an event of type url_verification when we configure the Request URL. After validating the request, our application returns its challenge value as plain text with HTTP 200.

Reply in a Thread

The current implementation replies by echoing the received prompt. If the message already belongs to a thread, we keep its thread_ts. Otherwise we use the message's ts to start a thread under it.

threadTimestamp := event.ThreadTimestamp
if threadTimestamp == "" {
    threadTimestamp = event.MessageTimestamp
}

The reply uses this structure:

type SlackReplyRequest struct {
    ChannelId       string `json:"channel"`
    ThreadTimestamp string `json:"thread_ts"`
    MessageText     string `json:"text"`
    MarkdownEnabled bool   `json:"mrkdwn"`
}

Before including the user's text in the reply, the code escapes &, <, and > so Slack does not interpret those characters as special references.

The application sends the reply to https://slack.com/api/chat.postMessage with a JSON body and these headers:

Authorization: Bearer xoxb-your-bot-token
Content-Type: application/json; charset=utf-8

Here we use BotToken, copied from Slack's OAuth & Permissions page. The channel and thread come from the event, so this flow does not use WebhookUrl.

Slack can return HTTP 200 with ok: false in the JSON response. The wrapper checks both the HTTP status and the ok field to identify a failed reply.

Acknowledge Events and Reply Asynchronously

Slack expects an event acknowledgement within three seconds. Our application starts the outgoing reply in a goroutine and returns HTTP 200 without waiting for the Web API call to finish:

kitparallel.RunAsGoRoutineSafe(func() {
    slackApi.ReplyToMessage(reply)
})

The asynchronous reply keeps the outgoing Slack call out of that path. It is not a durable queue, and the current implementation does not deduplicate Slack retries.

Final Note

We now have two Slack interaction flows. Webhook notifications need a channel webhook URL. Chat needs event subscriptions, a public callback URL, a signing secret for incoming requests, and a bot token for outgoing replies.

The current chat implementation echoes the prompt in a thread. Once the integration is in place, we can replace this reply with the application's actual processing logic.

No comments:

Post a Comment