Library / SDK
slackapi/bolt-js avatar
slackapi/bolt-js

bolt-js: the Request URL has to be /slack/events or nothing arrives

A framework to build Slack apps using JavaScript

2,943 stars444 forksTypeScriptMIT

At a glance

What is it?
Bolt for JavaScript wraps Slack's events, actions, shortcuts, commands and options behind one listener object, and its documentation is more specific about failure modes than about features. The path requirement, the mandatory acknowledgement, and the two different Web API clients are the three things a first app gets wrong.
Who is it for?
Bolt is the right choice for a JavaScript team building an interactive Slack app, because the listener signature puts the acknowledgement, the reply function and the Web API client on the same object and the framework is MIT licensed with a Node 20 floor. It is a poor choice if you do not want Express in your dependency tree, since express is a runtime dependency rather than an optional receiver, and it is the wrong shape for anything that is not an event-driven app.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 7 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 6, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The Request URL must be /slack/events or no request is handled

The first requirement in the readme is a path, and the failure mode it guards against is total. The Slack Request URL for a Bolt app must have the path set to /slack/events, for example https://my-slack-app.example.com/slack/events. Otherwise, all incoming requests from Slack won't be handled. There is no partial behaviour and no warning described: the app starts, the listeners are registered, and Slack's deliveries go to a route the framework does not recognise. That makes it the one thing to check before debugging anything else, because a misrouted path and a broken listener produce the same silence. It also means a Bolt app cannot be mounted behind a framework that owns its own routing without that path being arranged for it, which is where the custom receiver examples in the repository come in.

ack is the one argument that must always be called

Of the nine arguments a listener receives, exactly one is described as mandatory. ack is the function that must be called to acknowledge that an incoming event was received by the app, and it exists for all actions, shortcuts, view submissions, slash commands and options requests. It returns a promise that resolves when the acknowledgement completes. A separate section explains why: some types of events need acknowledging to ensure a consistent user experience inside the Slack client across the web, mobile and desktop apps, and the list is all action, shortcut, view, command and options requests. The section then begins to state the deadline and stops mid-sentence, at the point where the platform expects an acknowledgement within 3. So the requirement is unambiguous and the tolerance is not stated on this page.

express is a runtime dependency and routing is path-to-regexp

The dependency list decides what a Bolt app is made of. express is there at a caret range on the 5.x line, in dependencies rather than in an optional receiver, alongside path-to-regexp at ^8.1.0 which is what resolves the incoming request to a listener. Also present are raw-body, because a webhook signature check needs the unparsed bytes rather than a parsed object, and tsscmp, a constant-time string comparison, which is what that check compares against. The rest are Slack's own packages: the logger, oauth, socket-mode, types and web-api. So a project that installs this framework is committing to an HTTP server whether it wanted one or not, and the request path requirement in the previous section is enforced by that stack rather than by the framework's own router.

There are two Web API clients and they are not interchangeable

An app is created by calling the constructor, which is a top-level export, and the token is one of its arguments:

js
import { App } from '@slack/bolt';

const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  token: process.env.SLACK_BOT_TOKEN,
});

(async () => {
  // Start the app
  await app.start(process.env.PORT || 3000);

  app.logger.info('Bolt app is running!');
})();

The listener argument table and the section after it describe two different client objects, and the difference is about which workspace the call is billed to. The client passed to a listener is a Web API client that uses the token associated with that event, which for a single workspace installation is the token given to the constructor and for a multi workspace installation is the token returned by the authorize function. Separately, each app has a top-level client that can be used to call methods, and unlike the listener one it must be passed a token. So a listener already knows whose workspace it is acting for, while a top-level call site has to say so explicitly. The page does not say which workspace the top-level client belongs to when a token is supplied, and does not describe the authorize function's shape beyond naming it.

payload arrives twice under two names, and body is the superset

Two of the listener arguments overlap in a way that is worth understanding before writing anything. payload holds the contents of the incoming event, and its structure depends on the listener: for an Events API event it is the event type structure, for a block action it is the action from inside the actions array. The same object is also reachable through an alias named after the listener, one of message, event, action, shortcut, view, command or options, so in a message() listener the payload and message arguments are interchangeable. The page's advice on inspecting it is practical rather than documentary: an easy way to understand what is in a payload is to log it, or use TypeScript. body is the other overlap, described as an object containing the entire body of the request and a superset of payload, because some accessory data is only available outside the payload, naming trigger_id and authorizations as the examples.

Four of the nine listener arguments only exist for particular event types

The argument table is longer than it looks, because four entries are conditional and the conditions are stated. say sends a message to the channel associated with the incoming event, and is only available when the listener is triggered for events containing a channel_id, message events being named as the most common case. It accepts strings for plain text and objects for block content, and returns a promise resolving with a chat.postMessage response. respond replies to an incoming event if it contains a response_url, which the table places on actions, shortcuts, view submissions and slash commands. complete and fail are both narrow still: they signal successful completion and failure of a custom step execution, telling Slack to proceed with the next workflow steps or to stop, and they are only available with the .function and .action listener when handling custom workflow step executions. context and client are unconditional.

Ten examples, and most of them are about receiving events rather than building them

The examples directory has ten entries and their names describe a framework whose hard part is transport and authorisation rather than app logic. There are two deployment examples, deploy-aws-lambda and deploy-heroku. There are three involving OAuth: oauth, oauth-express-receiver and socket-mode-oauth. There are two Socket Mode examples, socket-mode and socket-mode-oauth. Then custom-receiver, custom-properties, message-metadata and getting-started-typescript. Only the TypeScript starter looks like an app rather than a receiving mechanism, and message-metadata is about the Slack message metadata feature. That ratio is a reasonable signal about where the difficulty sits: getting Slack's deliveries to reach your code at all, whether over HTTP or over Socket Mode, and getting an installation through an OAuth flow.

A release regenerates the API docs and npm has a floor of 9.6.4

The manifest and the scripts describe a repository with a documented release process. The version script is changeset version, then npm install, then npm run docs, so publishing a release regenerates the TypeDoc output rather than leaving it to be rebuilt by hand, and typedoc.json plus a docs script using typedoc are in the tree for that purpose. Changesets are configured under .changeset/. The test script is a chain rather than a single command: npm run build, then lint, then test:types, then test:coverage, with coverage collected by c8 using .c8rc.json, type assertions run by tsd over test/types, and unit tests run by mocha through test/unit/.mocharc.json. Linting is biome, invoked over docs, src, test and examples. The engines field is the other detail worth noting: node at 20 or newer and npm at 9.6.4 or newer, so the npm floor is specific enough to be a decision rather than an oversight.

Editorial conclusion

Bolt is the right choice for a JavaScript team building an interactive Slack app, because the listener signature puts the acknowledgement, the reply function and the Web API client on the same object and the framework is MIT licensed with a Node 20 floor. It is a poor choice if you do not want Express in your dependency tree, since express is a runtime dependency rather than an optional receiver, and it is the wrong shape for anything that is not an event-driven app. Before the first deploy, confirm the Request URL ends at /slack/events, decide deliberately between Socket Mode and HTTP, and read which listener arguments you actually get: say only arrives with a channel_id, respond only with a response_url, and complete and fail exist only for custom workflow steps.

Frequently asked questions

what is bolt js

It is Bolt for JavaScript, a framework for building Slack apps, published as the npm package @slack/bolt and MIT licensed. Apps are created by calling the App constructor, which is a top-level export, and functionality is added by registering listeners for events, actions, shortcuts, slash commands and options requests.

How do I install Bolt for JavaScript?

Run npm install @slack/bolt. The manifest requires Node 20 or newer and npm 9.6.4 or newer, and the package depends on express, raw-body, tsscmp, path-to-regexp and the Slack logger, oauth, socket-mode, types and web-api packages.

What Request URL does a Bolt app need?

The Slack Request URL must have the path set to /slack/events, for example https://my-slack-app.example.com/slack/events. The page states that otherwise all incoming requests from Slack won't be handled.

Which listener methods does Bolt for JavaScript provide?

app.action with an actionId or with a callback_id, app.command with a command name, app.event with an event type, app.function with a callbackId for custom workflow step executions, and app.message with a string or RegExp pattern to listen only to message events. There is also a listener for options requests from select menus with an external data source.

What arguments does a Bolt listener receive?

payload, say, ack, client, respond, context, body, complete and fail, all grouped into one object so a listener can destructure only what it needs. say is available only when the event carries a channel_id, respond only when it carries a response_url, and complete and fail only with the .function and .action listeners for custom workflow steps.

Is Bolt for JavaScript free to use?

The package is MIT licensed, with the author given as Slack Technologies, LLC. The page says nothing about what Slack itself charges for the workspace or the API on the other side of the framework, so the licence covers the library only.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. slackapi/bolt-js on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/slackapi-bolt-js.svg)](https://hysenlabs.com/projects/slackapi-bolt-js)