@slack/web-api v8: A Tested Node.js and TypeScript Example
@slack/web-api 8.2.0 runs on Node 20 or newer and sends requests with fetch. We posted, paginated, caught a Slack error and a rate limit in TypeScript, and found that the v7 agent option for proxies is silently ignored in v8.
On this page
@slack/web-api is Slack's official Node.js client for the Web API. The current version, 8.2.0, needs Node 20 or newer and sends requests with the built-in fetch instead of axios. Its TypeScript types ship in the package. We installed it on 4 October 2026, ran the script below with Node 26 against a free-plan test workspace, and compared one behaviour with v7.19.0. The bot token had chat:write, channels:history and channels:read.
Install
npm install @slack/web-api@8
npm install -D typescript tsx @types/node
npm view @slack/web-api engines printed node: ">= 20" and npm: ">=9.6.4". The package's dependencies in 8.2.0 are @slack/logger, @slack/types, eventemitter3, p-queue, p-retry and retry, with no HTTP library: requests go through globalThis.fetch.
Post, paginate and catch errors in TypeScript
import { WebClient, ErrorCode, LogLevel } from '@slack/web-api';
import type { WebAPICallError, ConversationsHistoryResponse } from '@slack/web-api';
const client = new WebClient(process.env.SLACK_BOT_TOKEN, { logLevel: LogLevel.ERROR });
const channel = process.env.CHANNEL_ID!;
async function main() {
const posted = await client.chat.postMessage({ channel, text: 'Hello from @slack/web-api v8' });
console.log('posted', posted.ok, posted.ts);
// paginate: one API call per page
let pages = 0;
let messages = 0;
for await (const page of client.paginate('conversations.history', { channel, limit: 3 })) {
pages += 1;
messages += (page as ConversationsHistoryResponse).messages?.length ?? 0;
}
console.log(`paginate: ${messages} messages in ${pages} pages`);
// a Slack error
try {
await client.chat.postMessage({ channel: 'C000BADID00', text: 'x' });
} catch (e) {
const err = e as WebAPICallError;
if (err.code === ErrorCode.PlatformError) {
console.log('bad channel:', err.code, JSON.stringify(err.data));
}
}
// a rate limit, thrown instead of retried
const strict = new WebClient(process.env.SLACK_BOT_TOKEN, { rejectRateLimitedCalls: true, logLevel: LogLevel.ERROR });
const start = Date.now();
let ok = 0;
const results = await Promise.allSettled(
Array.from({ length: 400 }, () => strict.conversations.info({ channel }).then(() => { ok += 1; })),
);
const rejected = results.filter((r) => r.status === 'rejected') as PromiseRejectedResult[];
const first = rejected[0]?.reason as WebAPICallError & { retryAfter?: number };
console.log(`400 calls in ${((Date.now() - start) / 1000).toFixed(1)} s: ${ok} ok, ${rejected.length} rejected`);
if (first) console.log('first rejection:', first.code, 'retryAfter =', first.retryAfter, '|', first.message);
await client.chat.delete({ channel, ts: posted.ts! });
console.log('deleted');
}
main();
npx tsx main.ts printed this (we cut the long scopes list):
posted true 1791077574.849919
paginate: 15 messages in 5 pages
bad channel: slack_webapi_platform_error {"ok":false,"error":"channel_not_found","response_metadata":{"scopes":["chat:write","channels:join", ...],"acceptedScopes":["chat:write"]}}
400 calls in 1.4 s: 19 ok, 381 rejected
first rejection: slack_webapi_rate_limited_error retryAfter = 10 | A rate-limit has been reached, you may retry this request in 10 seconds
deleted
What to take from it:
- •
err.codeis the library's error kind, not Slack's. A Slackok: falsereply isslack_webapi_platform_error, and Slack's own code (channel_not_found) is inerr.data.error.err.data.response_metadata.acceptedScopesnames the scope the method takes. - •
paginateyields a plainWebAPICallResult, so our first version, which readpage.messages, failedtscwitherror TS2339: Property 'messages' does not exist on type 'WebAPICallResult'. Cast each page to the method's response type, as above. The cursor rules are on our Slack API pagination page. - • The rate-limit numbers depend on what else used the method that minute. Our first run of the same script let 130 of the 400 calls through; the run above came a minute later and got 19.
Never appear "away" on Slack again
Cloud-based. No downloads. Works 24/7 even when your laptop is off.
Rate limits: wait or throw
By default the client waits and retries a rate-limited call. We used up the limit with 400 parallel calls, then made one call with a default client that listened for the rate_limited event:
normal.on('rate_limited', (sec: number) => { limitedEvents += 1; console.log(`rate_limited event: retry in ${sec} s`); });
// ...
const t = Date.now();
const r = await normal.conversations.info({ channel });
console.log(`default client: ok=${r.ok} after ${((Date.now() - t) / 1000).toFixed(1)} s, rate_limited events=${limitedEvents}`);
rate_limited event: retry in 10 s
default client: ok=true after 12.0 s, rate_limited events=1
So a default client hides a 429 by pausing. In a web request handler that is a 12-second stall with no error. Set rejectRateLimitedCalls: true if you would rather get slack_webapi_rate_limited_error with retryAfter and decide yourself. Our other measured limits are on the Slack API rate limits page.
Proxies: the v7 agent option no longer works
In v7 you passed an HTTP agent to reach Slack through a proxy. v8 has no agent option, and plain JavaScript gives no warning when you pass one. We pointed both versions at a proxy address where nothing listens (http://127.0.0.1:9) and called auth.test:
| Client | Proxy setting | Result |
|---|---|---|
| v7.19.0 | agent: new HttpsProxyAgent(...) | slack_webapi_request_error, connect ECONNREFUSED 127.0.0.1:9 |
| v8.2.0 | the same agent option | ok: true: the request skipped the proxy and went straight to Slack |
| v8.2.0 | fetch using undici's ProxyAgent | slack_webapi_request_error, fetch failed |
The v8 agent call worked only because this machine can reach Slack directly. Behind a firewall that allows only the proxy, the same code fails, and the reason is not in the error. In TypeScript the compiler catches it: error TS2353: Object literal may only specify known properties, and 'agent' does not exist in type 'WebClientOptions'. The v8 way is a custom fetch:
import { WebClient } from '@slack/web-api';
import { fetch as undiciFetch, ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent('http://127.0.0.1:9'); // your proxy URL here
const client = new WebClient(process.env.SLACK_BOT_TOKEN, {
fetch: ((url: any, init: any) => undiciFetch(url, { ...init, dispatcher })) as any,
});
We cast with as any to keep the test short. With the dead proxy this client failed, which shows the proxy is in use. For a full app with events and slash commands on top of this client, see our Bolt for JavaScript v5 page.
Stop Jiggling Your Mouse.
Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.
Related Articles
Slack API in Go With slack-go/slack: A Tested Example
slack-go/slack v0.29.0 is the Go client most people use for the Slack API. We ran a program that posts, reads back and updates a Block Kit message, a Socket Mode bot that answers a mention, and a loop that hit the rate limit, with the output and error types we got.
slack-ruby-client: A Tested Ruby Example, Errors and Limits
slack-ruby-client 3.2.0 is the Ruby gem for the Slack Web API. We installed it, posted, read back, reacted and paginated against a real workspace, and recorded the error classes it raised, including the rate-limit error that a rescue of SlackError does not catch.
Slack Status Change Notification: The Events an App Gets, Timed
Slack does not tell anyone when you change your status. An app can find out within a second through the user_status_changed and user_change events. We changed a status 7 ways and timed each event, including statuses that expired.