Back to Blog
Guide

@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.

Slack Green Team
October 4, 2026
October 4, 2026
4 min read
Share:
slack api
slack developer
node.js

@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.code is the library's error kind, not Slack's. A Slack ok: false reply is slack_webapi_platform_error, and Slack's own code (channel_not_found) is in err.data.error. err.data.response_metadata.acceptedScopes names the scope the method takes.
  • • paginate yields a plain WebAPICallResult, so our first version, which read page.messages, failed tsc with error 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:

ClientProxy settingResult
v7.19.0agent: new HttpsProxyAgent(...)slack_webapi_request_error, connect ECONNREFUSED 127.0.0.1:9
v8.2.0the same agent optionok: true: the request skipped the proxy and went straight to Slack
v8.2.0fetch using undici's ProxyAgentslack_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.

Always Active

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

Guide

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 Green Team
Guide

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 Green Team
Guide

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.

Slack Green Team