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

How to See All Your DMs in Slack, Including the Hidden Ones

The DMs tab lists your direct messages with people. App DMs, Slackbot and DMs with deleted accounts sit elsewhere. We compared the tab with the full list the Slack API returned.

Slack Green Team
Guide

Slack Images Not Showing? 5 Causes We Reproduced

We uploaded images to a Slack test channel and then switched each image setting off, ran /collapse, and posted from a bot that was not in the channel. Five causes, each reproduced.

Slack Green Team
Guide

How to Message Yourself on Slack (and What It Notifies)

Your own name under Direct messages opens a private DM with yourself. We sent it a note, a file, a scheduled message and a reminder, and checked which ones raised a badge.

Slack Green Team