# Discord Server Member Count Tracker - Online & Total Members (`neverempty/discord-server-member-count-tracker`) Actor

For community managers and growth teams: total members, online members, boosts, name, tag, icon and creation date for every Discord server you name by its invite link. One measured server: 431,725 members, 45,600 online. Monitoring returns a server only when its member count moves.

- **URL**: https://apify.com/neverempty/discord-server-member-count-tracker.md
- **Developed by:** [NeverEmpty](https://apify.com/neverempty) (community)
- **Categories:** Social media, Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.65 / 1,000 server row returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Discord Server Member Count Tracker - Online & Total Members

For community managers and growth teams: total members, online members, boosts, server name, tag, description, icon, verification level and creation date for every Discord server you name by its invite link or code, plus a monitoring mode that returns a server only when its member count moves. Measured on 2026-09-23 from the four servers in `test/fixtures/`: Python 431,725 members / 45,600 online, Discord Developers 304,393 / 80,142, Reactiflux 76,420 / 11,273, Node.js 24,642 / 2,804. Point it at the servers you actually care about - yours, your competitors', the communities you sponsor - and get a daily growth line instead of a directory dump.

Export as JSON, CSV or Excel.

Unofficial. Public data only. Not affiliated with, endorsed by or connected to Discord Inc.

***

### What it does

You give it invite codes or invite links. It returns one row per **server**, with the numbers Discord publishes on the public invite endpoint.

```json
{
  "invites": ["python", "https://discord.gg/discord-developers", "discord.com/invite/nodejs"],
  "maxServers": 100
}
```

A returned row (shortened - the real row has every column listed below):

```json
{
  "status": "ok",
  "inputInvite": "python",
  "inviteCode": "python",
  "inviteUrl": "https://discord.gg/python",
  "serverId": "267624335836053506",
  "serverName": "Python",
  "serverDescription": "We're a large community focused around the Python programming language. We believe that anyone can learn to code.",
  "memberCount": 431725,
  "onlineCount": 45600,
  "countsAreApproximate": true,
  "boostCount": 28,
  "boostTier": 3,
  "serverCreatedAt": "2017-01-08T12:03:33.883Z",
  "serverAgeDays": 3544,
  "vanityUrlCode": "python",
  "serverTag": "snek",
  "isVerified": false,
  "isPartnered": false,
  "isCommunity": true,
  "isDiscoverable": true,
  "verificationLevel": 2,
  "verificationLevelName": "MEDIUM",
  "nsfwLevel": 0,
  "nsfwLevelName": "DEFAULT",
  "iconUrl": "https://cdn.discordapp.com/icons/267624335836053506/95a69567efa60a48b8d9840cb38b106b.png",
  "inviteChannelName": "welcome",
  "inviteExpiresAt": null,
  "inviteIsPermanent": true,
  "scrapedAt": "2026-09-23T03:00:00.000Z"
}
```

### Why this one

Most Discord Actors on the store are built to **find** servers: you give them a keyword or a category and they return whatever the public directories list. A couple of them do also resolve invite codes you supply, through the same public endpoint this one uses. What none of them does is **remember between runs** - run them again tomorrow and you pay full price for every server on your list, changed or not, and you diff the two exports yourself. That is the gap this Actor fills:

- **You name the servers.** Invite code, `discord.gg/code` or `https://discord.com/invite/code` - all three work, and invite codes are case sensitive, so they are sent exactly as you type them.
- **Monitoring mode returns only what changed**, with `previousMemberCount`, `memberCountChange` and `previousCheckedAt` in the row. Servers that did not move cost the check fee instead of the row price.
- **Two invites to the same server cost one row.** Servers are joined on the id Discord returns, not on the code you typed.
- **It never says a server is empty or gone when it could not read it.** Rate limits, blocks, unknown invites and expired invites each get their own free row with their own reason.

### Input

| Field | What it does |
|---|---|
| `invites` | The servers to check, named by an invite: `python`, `discord.gg/python` or `https://discord.com/invite/python`. Several per line is fine - the list is split on spaces, commas and semicolons. The same code is checked once. Up to 1,000 per run, one request each, 600 ms apart. Server URLs such as `discord.com/channels/...` are not invites and get a free `invalid-input` row. Leave it empty with monitoring off and the example servers `python`, `discord-developers` and `nodejs` are checked, with `inputInvite` saying so; leave it empty with monitoring on and nothing is checked and nothing is charged. |
| `maxServers` | How many charged rows to return when monitoring is off (default 100, max 1,000). Reading stops once that many servers have been read, and a free row says how many invites were not checked. **It does not limit spending in monitoring mode**: there every server you list is checked and every change comes back, and the run's maximum total charge is the cap. Cutting changes by this number would mean the servers at the end of your list never came back at all. |
| `monitoringMode` | Off: every invite comes back with the server's counts right now. On: the Actor remembers each server and later runs return it only when the watched count changed. The first run returns every server once to set the baseline. |
| `monitorOn` | What counts as a change: `member-count` (default) or `any-count` (members, online or boosts). See the measurement below for why the default is the total member count. |
| `resetMonitoringState` | Clears every remembered count for this Actor, so the next monitoring run returns each server once again. The counts are stored per server, not per list, so this affects all your monitoring runs. **Turn it off again after that one run** - left on in a schedule, every run starts from scratch and charges a full server row for every server. |
| `useProxy` | Requests go out directly. This only covers the case where a request does not reach Discord at all (a connection failure): it then retries through an Apify proxy. Blocks, bot checks and rate limits are never retried from another address either way; they come back as free rows. |

### Output columns

`source` `status` `scrapedAt` `inputInvite` `inviteCode` `inviteUrl` `serverId` `serverName` `serverDescription` `memberCount` `onlineCount` `countsAreApproximate` `boostCount` `boostTier` `serverCreatedAt` `serverAgeDays` `vanityUrlCode` `serverTag` `isVerified` `isPartnered` `isCommunity` `isDiscoverable` `verificationLevel` `verificationLevelName` `nsfwLevel` `nsfwLevelName` `isNsfw` `features` `iconUrl` `bannerUrl` `splashUrl` `inviteChannelName` `inviteChannelId` `inviteExpiresAt` `inviteIsPermanent`

In monitoring mode, also: `change` `isFirstCheck` `previousCheckedAt` `previousMemberCount` `previousOnlineCount` `previousBoostCount` `memberCountChange` `onlineCountChange` `boostCountChange`.

`change` is one of `first-check`, `members-up`, `members-down`, `online-count-changed` or `boost-count-changed`. `previousCheckedAt` is when that server was last **checked**, not when it last changed: every server read in a monitoring run has its record refreshed, changed or not.

### Rows that are not charged

Every row that is not a server carries a `status` and a `note` saying why, and none of them is charged:

| `status` | When |
|---|---|
| `no-such-invite` | Discord answered `Unknown Invite` (error code 10006). A code that never existed, one that was deleted or revoked, and one that expired all get this same answer, so the row does not claim which. |
| `invite-expired` | Discord returned the invite but its expiry time has already passed, so the counts attached to it may be out of date. Nothing is returned for it. |
| `blocked` | A block, an error page or an empty response instead of the invite data. The server is not reported as gone or empty. |
| `rate-limited` | Discord rate-limited the run and the invite was still not readable after the retries. |
| `unreadable` | Discord answered with something else that could not be read: a body that is not the invite data, a redirect (never followed), or the invite without any member count. The reason is in the note. A server is never returned with an empty member count. |
| `invalid-input` | The text is not an invite code or invite link, or the `invites` field is not a list, or monitoring is on with an empty list. Nothing is requested. |
| `duplicate-invite` | The invite leads to a server this run already returned. |
| `no-change` | Monitoring mode: nothing among the servers checked has changed. |
| `not-checked` | Invites past `maxServers`, or past the 1,000-per-run ceiling. |
| `budget-reached` | The run's maximum total charge was reached. The row says how many of the ready rows fit inside the limit and how many invites were not checked at all. |

### Pricing

| Event | Price |
|---|---|
| `server-returned` - one server row | **$5.00 per 1,000 rows** |
| `server-checked` - one server checked in monitoring mode | **$0.30 per 1,000 checks** |

There is no start fee. With monitoring off you pay only for the rows you get. With monitoring on, **every server checked costs the check fee, changed or not**, plus the row price for the rows that come back; invites Discord returns nothing for, blocks and rate limits are free. Twenty servers checked every hour is 14,400 checks a month = **$4.32**, plus $0.005 for each row where a count moved.

The Actor reads only as many servers as the run's maximum total charge can pay for **with a check and a change row each**, so a low limit never spends the whole budget on check fees and returns nothing. Invites it did not read are named in a free `budget-reached` row.

### What was measured, and when

Everything below was measured on 2026-09-23 against the live endpoint. The four responses are in `test/fixtures/` so the numbers in this README can be counted again from this repository.

- **On every server measured, the counts appear twice in the response** - as `approximate_member_count` / `approximate_presence_count`, and again inside `profile` as `member_count` / `online_count`. On all four servers measured the two pairs were identical, so the row carries one pair. Discord itself calls them *approximate*; `countsAreApproximate` says so in every row.
- **Discord caches the answer for up to five minutes** (`Cache-Control: public, max-age=300`, and repeats inside that window came back as a cache hit). Checking a server more often than every five minutes does not get a fresher number.
- **Both counts move; the online count moves more.** The same four servers were read every 5.5 minutes for 33 minutes, 7 reads each, on 2026-09-22 between 18:03 and 18:36 UTC (`test/fixtures/counts-jitter-2026-09-23.json`, so you can count this again). Across the 24 intervals, the **total member count changed 13 times and reversed direction 4 times**; the **online count changed 18 times and reversed direction 10 times** (counting a reversal against the last interval that moved at all) (Python's online count went 45,600 -> 45,786 -> 45,674 while its member count went 431,725 -> 431,728 -> 431,729). Neither number is perfectly steady, but the member count moves less often and swings back less, which is why `monitorOn` watches it by default.
- **On a large server, expect a row on about half of five-minute checks.** That is real movement, not a defect, but it is not what this is priced for: hourly or daily monitoring is. Discord caches the answer for five minutes anyway, so checking more often than that only costs check fees.
- **The creation date is calculated, not returned.** Discord ids carry their creation time, so `serverCreatedAt` comes from the server id (Reactiflux 2015-10-11, Python 2017-01-08, Node.js 2018-03-21, Discord Developers 2019-08-20).
- **Unknown invites answer HTTP 404 with `{"message": "Unknown Invite", "code": 10006}`** - a 44-byte body.

### How the data is obtained, and what is not touched

- The Actor requests exactly one address: `https://discord.com/api/v10/invites/<code>?with_counts=true&with_expiration=true`. That is the only address it ever requests; the `discord.gg` and `cdn.discordapp.com` links in the rows are built as text and never fetched. Redirects are not followed, so it cannot be sent anywhere else. A test in this repository re-reads `discord.com/robots.txt` as fetched on 2026-09-23, applies the longest-match rule and fails the build if that path is not the one `Allow: /api/v*/invite` permits.
- No login, no bot token, no OAuth, no authorization header of any kind. Three request headers go out and nothing else: a browser `User-Agent`, `Accept: application/json` and `Accept-Encoding: identity`.
- Blocks, bot checks and rate limits are **not** worked around, and a blocked request is never retried from another address. A rate limit is waited out only for as long as Discord asks, up to 30 seconds, from the same address; anything longer comes back as a free `rate-limited` row. An Apify proxy is used for one case only: a request that does not reach Discord at all (a connection failure).
- **No personal data.** Discord's answer can include the account that created the invite; that object is never read into the row, and a test feeds a response containing one and asserts nothing about it reaches the output. Members, messages, channels and user profiles are not requested at all.

### Notes and limits

- The counts are the ones Discord publishes for the invite. They are approximate by Discord's own naming, and this Actor does not adjust, round or estimate them.
- A server with no invite cannot be tracked. You need a working invite code, and a permanent one (no expiry) is what you want for monitoring.
- Remembered counts are stored per server, in a key-value store named `discord-server-monitoring`. Do not put the same server in two schedules that can run at the same time: overlapping runs merge their records, but Apify's key-value store has no atomic update, so this cannot be prevented completely.
- If a run is restarted by Apify mid-way, servers already returned in that run are not returned or charged again, and checks already paid for are not paid for again.
- Discord can change this endpoint at any time. It is not a documented product surface for scrapers, so there is no promise that it keeps working unchanged.

# Actor input Schema

## `invites` (type: `array`):

The servers to check, named by an invite: the code on its own (python), discord.gg/python, or https://discord.com/invite/python. Invite codes are case sensitive, so they are sent exactly as you type them. The same code is checked once, and if two invites lead to the same server it is returned once. Up to 1,000 per run, one request each. Server URLs such as discord.com/channels/... are not invites and are rejected with a free row. If you leave this empty and monitoring is off, the example servers python, discord-developers and nodejs are checked and every row says so in inputInvite; if monitoring is on, nothing is checked and nothing is charged.

## `maxServers` (type: `integer`):

How many charged rows to return when monitoring is off: reading stops once this many servers have been read, and a free row says how many invites were not checked. **It does not limit spending in monitoring mode** - there, every server you list is checked and every change among them comes back, and the run's maximum total charge is what caps the cost. Cutting changes here would mean the servers at the end of your list never came back at all.

## `monitoringMode` (type: `boolean`):

Off = every invite you listed comes back with the server's counts right now, charged per row. On = the Actor remembers each server and, on later runs, returns it only when the watched count changed, with the previous value and the difference in the row. The first run returns every server once to set the baseline. **In monitoring mode every server checked costs $0.30 per 1,000 checks, changed or not** (invites Discord returns nothing for, blocks and rate limits are free), plus the row price for the rows returned. Example: 20 servers every hour = 14,400 checks a month = $4.32. The Actor reads only as many servers as the run's maximum total charge can pay for with a check and a change row each. Counts are remembered per server, so do not put the same server in two schedules that can run at the same time.

## `monitorOn` (type: `string`):

Which numbers make a server come back in monitoring mode. Total member count only is the default because Discord's answer is cached for up to 5 minutes and the online count moves almost every hour, so watching it would return nearly every server on nearly every check. Any count also returns a server when the online count or the boost count changed.

## `resetMonitoringState` (type: `boolean`):

Clears every remembered count for this Actor, so the next monitoring run returns each server once again. The counts are stored per server, not per list, so this affects all your monitoring runs. **Turn it off again after that one run**: while it is on, every run starts from scratch and charges a full server row for every server instead of only the ones that changed.

## `useProxy` (type: `boolean`):

Requests go out directly, which returned the invite data on every attempt measured on 2026-09-23. This option only covers the case where a request does not reach Discord at all (a connection failure): it then retries through an Apify proxy. **Blocks and bot checks are never retried from another address**, and rate limits are never worked around - the Actor waits the time Discord asks for, up to 30 seconds, and otherwise returns a free row. With this off, a connection failure is reported as a free row and no proxy is paid for.

## Actor input object example

```json
{
  "invites": [
    "python",
    "discord-developers",
    "nodejs"
  ],
  "maxServers": 100,
  "monitoringMode": false,
  "monitorOn": "member-count",
  "resetMonitoringState": false,
  "useProxy": true
}
```

# Actor output Schema

## `results` (type: `string`):

One row per Discord server: the invite you asked for, the invite code and link, the server id, name, description, tag, icon, banner and splash URLs, the approximate total member count and online member count, the boost count and boost tier, the date the server was created and its age in days, the vanity URL code, whether it is verified, partnered, a community or discoverable, its verification and NSFW levels, its feature list, and the channel the invite points at with the invite's expiry; in monitoring mode also what changed since the last run (members up, members down, online count changed or boost count changed), the previous counts and the difference. Invite codes Discord returns nothing for, expired invites, blocks, rate limits, invites that lead to a server another invite already returned, and anything cut short by the run's charge limit come back as their own free rows. In monitoring mode unchanged servers are not returned.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "invites": [
        "python",
        "discord-developers",
        "nodejs"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("neverempty/discord-server-member-count-tracker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "invites": [
        "python",
        "discord-developers",
        "nodejs",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("neverempty/discord-server-member-count-tracker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "invites": [
    "python",
    "discord-developers",
    "nodejs"
  ]
}' |
apify call neverempty/discord-server-member-count-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,neverempty/discord-server-member-count-tracker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/61YJkZ1j1dphqdDfd/builds/PH7eBNDbCfJzcu3b8/openapi.json
