Back to Blog
Guide

Slack users.list API: What It Returns, Tested on a Real Workspace

We called users.list with a bot token in a one-person workspace and got 14 members back: Slackbot, 1 person, 4 live bots and 8 deleted ones. The script, the curl call, cursor paging to the end, and what users:read.email changes.

Slack Green Team
October 2, 2026
October 2, 2026
4 min read
Share:
slack api
developers
users

To list every user in a Slack workspace with the API, call users.list with a token that has the users:read scope, and follow response_metadata.next_cursor until it comes back empty. The list is longer than the people you see in Slack. On 2 October 2026 we ran it with a bot token in our own test workspace, which has one person in its directory. users.list returned 14 members: Slackbot, that one person, 4 bots, and 8 bots from apps we had deleted.

Slack web Directories > People tab showing a single card, "sieun (you)", under an "Invite your team to Slack" banner

List all users in Python

This is the script we ran with slack_sdk 3.45.0. Iterating over users_list() follows the cursor for you:

import os
from slack_sdk import WebClient

client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
members = []
for page in client.users_list(limit=200):          # slack_sdk follows next_cursor for you
    members.extend(page["members"])

def kind(m):
    if m["id"] == "USLACKBOT":
        return "slackbot"
    if m.get("deleted"):
        return "deleted bot" if m.get("is_bot") else "deactivated person"
    if m.get("is_bot"):
        return "bot"
    return "person"

for m in members:
    print(f'{m["id"]:12} {kind(m):18} {m["name"]:22} email={m.get("profile", {}).get("email", "-")}')
print(len(members), "members returned")

Output, after we added users:read.email (the one email is redacted here):

USLACKBOT    slackbot           slackbot               email=-
U0B7L4YK420  person             sieun                  email=<redacted>
U0C5DA22W79  deleted bot        w1test                 email=-
U0C5FA6FSJ3  deleted bot        bklab                  email=-
U0C5FEA81N3  deleted bot        sglabw2http            email=-
U0C5L6Q2EAJ  deleted bot        w10930test             email=-
U0C5QDUGPV1  deleted bot        sglabw2                email=-
U0C5UH0S0E6  deleted bot        apilab                 email=-
U0C5WG3PXHQ  deleted bot        sglabw1-x              email=-
U0C61H3JW5N  deleted bot        api_error_lab          email=-
U0C63GB2THU  bot                w2rotatebot            email=-
U0C65AARWAW  bot                w2labbot               email=-
U0C695L6YVA  bot                sglabw1                email=-
U0C6ZSGEKRN  bot                w2optinbot             email=-
14 members returned

What the output shows:

  • • Slackbot is in the list as USLACKBOT, and its is_bot is false. A filter on is_bot alone counts Slackbot as a person. Check the ID too.
  • • Deleted apps leave their bot users behind with "deleted": true and "is_bot": true. Each one still carries profile.bot_id and profile.api_app_id, so you can tell which app it belonged to.
  • • Deactivated people come back the same way, with "deleted": true and "is_bot": false. Our workspace had none, which is why the deactivated person branch never printed. To list only deactivated accounts, keep the members where deleted is true and is_bot is false.
  • • Our own app's bot (sglabw1) is a member like any other.

To count only active people, keep members where deleted and is_bot are false and the ID is not USLACKBOT. In our workspace that left 1, which matches the directory above.

The same call with curl

Send the token in the Authorization header. limit=2 and jq keep the output short:

curl -s -H "Authorization: Bearer $SLACK_BOT_TOKEN" \
  "https://slack.com/api/users.list?limit=2" \
  | jq '{ok, members: [.members[] | {id, name, is_bot, deleted}], response_metadata}'
{
  "ok": true,
  "members": [
    {
      "id": "USLACKBOT",
      "name": "slackbot",
      "is_bot": false,
      "deleted": false
    },
    {
      "id": "U0B7L4YK420",
      "name": "sieun",
      "is_bot": false,
      "deleted": false
    }
  ],
  "response_metadata": {
    "next_cursor": "dXNlcjpVMEM1REEyMlc3OQ=="
  }
}

The cursor is base64. dXNlcjpVMEM1REEyMlc3OQ== decodes to user:U0C5DA22W79, the ID of the next member in the list. Treat it as opaque anyway: a cursor we built ourselves from a real-looking ID returned invalid_cursor.

Never appear "away" on Slack again

Cloud-based. No downloads. Works 24/7 even when your laptop is off.

Paging with next_cursor

We paged through the list one member at a time with limit=1, passing each next_cursor back as cursor:

RequestMembers returnednext_cursor
11dXNlcjpVMEI3TDRZSzQyMA==
2 to 131 eacha new cursor each time
141"" (empty string)

The last page still had a member in it. The loop ends when next_cursor is an empty string, not when a page comes back empty, so check the cursor before you decide you are done.

Other values we sent:

RequestResult
limit=0ok, all 14 members, empty cursor
limit=1000, 1001 and 5000ok, all 14 members, no error and no warning
cursor=bogus{"ok": false, "error": "invalid_cursor"}
A base64 cursor we built for a user ID that does not exist{"ok": false, "error": "invalid_cursor"}

Our workspace is small, so every limit fit in one page. Slack's docs say a page can hold fewer members than the limit you asked for, and recommend a limit of 200 or less, so write the loop around the cursor, not around the page size. users.list is rate limited per method; our rate limit tests show what a ratelimited reply looks like and how long Retry-After was.

Getting emails: users:read.email

With only users:read, no member had an email key in profile. The key was missing, not empty. We added users:read.email to the app's bot scopes and called users.list again before reinstalling: still no emails. After we reinstalled the app, the same bot token string returned the person's email. Bots never have one.

A user token with only users:read also returned all 14 members and no emails. To look up one person by address instead of listing everyone, use users.lookupByEmail. If you skip the scope, the missing_scope error names the one you need in its needed field.

FAQ

How do I get a user's ID from their name with the API?

List the members and match on name, real_name or profile.display_name. Names are not unique, so check that you got exactly one match. profile.display_name is empty for anyone who never set one, which our users.info test shows. For finding one ID in the Slack app, see how to find a Slack member ID. For the people in one channel, start from conversations.members instead; get users in a channel has the script.

Does users.list return guests and members from other workspaces?

Every active member object we got had is_restricted and is_ultra_restricted fields (the deleted bots had neither); Slack's docs use them to mark multi-channel and single-channel guests. Our test workspace had no guests and no Slack Connect channels, so we could not see either case in the output.

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