@hameddk/slack-user-client
v0.1.0
Published
Slack Web API client for user-token (xoxp-) flows. Send DMs as a real user, look up users by email, open conversations, paginate cursors. Pluggable auth via async token resolver, configurable rate-limit retry. Zero dependencies.
Maintainers
Readme
@hameddk/slack-user-client
Slack Web API client for user-token (xoxp-) flows.
- Send DMs as a real user, look up users by email, open conversations
- Pluggable auth via async token resolver (BYO OAuth library)
- Cursor pagination helper
- Rate-limit auto-retry honoring
Retry-After - Explicit
ok: falsehandling — Slack returns HTTP 200 for app errors - Zero dependencies, ESM, Node ≥ 18
Status: 0.1.0 — early. Public API is stable for the documented surface.
Why a user-token client (not a bot client)?
Slack has two primary token flavors:
- Bot tokens (
xoxb-) — message appears as your bot. Plenty of mature libraries handle these. - User tokens (
xoxp-) — message appears as a real user. Useful when you want notifications to land in chat as if a human sent them. Fewer libraries are tuned for this; this package is.
This package is API-only — it does not handle OAuth. Pair it with an OAuth library such as @hameddk/oauth-toolkit to obtain and refresh the user token.
Install
npm install @hameddk/slack-user-clientQuick start
import { createSlackUserClient } from '@hameddk/slack-user-client';
const client = createSlackUserClient({
// Async token resolver. Re-evaluated on every request.
getAccessToken: async () => loadUserTokenFromYourStore(),
options: {
autoRetryRateLimit: { maxRetries: 3, maxDelayMs: 60_000 },
onRateLimit: (sec, attempt) =>
console.warn(`Slack rate limited; sleeping ${sec}s (attempt ${attempt})`),
},
});
// Verify the token is valid.
const me = await client.testAuth(); // → { ok, user_id, team, url, ... }
// Find a user by email.
const lookup = await client.lookupUserByEmail({ email: '[email protected]' });
const slackUserId = lookup.user.id; // 'U1234567890'
// Open a DM channel.
const open = await client.openConversation({ users: slackUserId });
const channelId = open.channel.id; // 'D098...'
// Post the message.
await client.postMessage({
channel: channelId,
text: 'Heads up — please review JIRA-1234.',
});Public API
const client = createSlackUserClient(opts);
await client.testAuth();
await client.lookupUserByEmail({ email });
await client.openConversation({ users }); // single ID or comma-list / array
await client.postMessage(params); // full chat.postMessage params
await client.callMethod(method, params, { httpMethod });
await client.paginateAll(method, params, { itemsKey, pageLen, maxTotal });Generic callMethod
The Slack Web API has hundreds of methods. The four built-in convenience
methods cover the user-token DM flow; for everything else use callMethod:
const conv = await client.callMethod('conversations.info', { channel: 'D123' });
const list = await client.callMethod('users.list', { limit: 200 }, { httpMethod: 'GET' });Choosing httpMethod
callMethod defaults to POST with application/x-www-form-urlencoded —
this is what Slack recommends for the majority of methods, including all
write operations.
Use { httpMethod: 'GET' } for the small set of read-only methods that
expect query-string parameters. As of writing, the most common ones are
auth.test and users.lookupByEmail (which is why the built-in convenience
methods for those use GET internally).
This is guidance, not a hardcoded whitelist — Slack's API may evolve and
new GET endpoints may appear. Check the Slack method docs if in doubt;
endpoints that fail with HTTP 405 or not_allowed_token_type when you POST
are typical candidates for GET.
Cursor pagination
Slack uses cursor + response_metadata.next_cursor. The paginateAll
helper drives the loop and returns a flat array:
const allUsers = await client.paginateAll(
'users.list',
{ limit: 200 },
{ itemsKey: 'members', maxTotal: 5000 }
);itemsKey is required because Slack uses different field names per method
(members for users.list, channels for conversations.list, etc.).
Errors
import {
SlackError, // base
SlackConfigError, // missing config
SlackAuthError, // not_authed, invalid_auth, token_revoked, ...
SlackRateLimitError, // 429 after retries exhausted
SlackApiError, // every other ok:false (slackError preserved)
SlackRateLimitError, // 429 (or ok:false ratelimited) after retries exhausted
} from '@hameddk/slack-user-client';
try {
await client.openConversation({ users: 'U123' });
} catch (err) {
if (err instanceof SlackAuthError) {
// Token revoked, expired, or missing scope. Send the user through re-auth.
showReconnectBanner(err.slackError);
} else if (err instanceof SlackApiError && err.slackError === 'channel_not_found') {
// Channel was deleted, archived, or never existed.
showChannelMissingMessage();
} else if (err instanceof SlackApiError && err.slackError === 'users_not_found') {
// No Slack user matches that ID/email.
showUserMissingMessage();
} else if (err instanceof SlackRateLimitError) {
// Auto-retry already exhausted; back off and try later.
scheduleRetry(err.retryAfter);
} else {
throw err;
}
}SlackApiError.slackError carries the original Slack error code (e.g.
users_not_found, channel_not_found, is_archived). The toolkit does not
translate these to user-facing strings — that's caller territory.
Rate limiting
Slack returns HTTP 429 with a Retry-After header. The client auto-retries:
options: {
autoRetryRateLimit: true, // default
autoRetryRateLimit: { maxRetries: 3, maxDelayMs: 60_000 },
autoRetryRateLimit: false, // disable
}After exhausting retries (or when Retry-After exceeds maxDelayMs), the
client throws SlackRateLimitError with retryAfter (seconds).
Testing hooks
For testing only:
options: {
fetch: customFetch, // override fetch implementation
sleep: async (ms) => {}, // skip real sleeps in tests
}What this library does not do
- Doesn't handle OAuth — pair with @hameddk/oauth-toolkit.
- Doesn't handle bot tokens (
xoxb-) — many mature libraries cover that. - Doesn't handle Slack Events API or Webhooks.
- Doesn't translate Slack error codes to UI strings — caller's responsibility.
- Doesn't persist tokens — your token resolver does.
License
MIT © 2026 Hamed Sattari
