Allowed-email API¶
Keep your server's allowed-email list in sync from your own systems — a student information system, an HR export, a CRM, a nightly cron job — instead of uploading a CSV by hand every time someone joins or leaves.
Included with the Pro subscription
The API is available to servers on the Pro subscription. The one-time CSV unlock does not include it: the API is a live service we host and rate-limit, so access follows the subscription and stops when it lapses.
Everything the API does can also be done by hand with /emaillist upload,
/emaillist remove and /emaillist clear.
1. Get a token¶
In your Discord server, run:
The reply is ephemeral — only you can see it — and contains the token exactly once.
The token is shown once and never again
Only a hash of the token is stored, so nobody, including us, can read it back.
Lose it and you run /api token generate again, which issues a new token and
invalidates the old one.
Treat it like a password. Anyone holding it can change this server's allowed-email list. Keep it in a secret store or an environment variable, never in a committed file.
Related commands:
| Command | What it does |
|---|---|
/api token generate |
Issues a new token and invalidates the previous one |
/api token status |
Shows when the token was created and last used — never the token |
/api token revoke |
Disables the token; every request then returns 401 |
2. Check it works¶
Every request authenticates with the token in an Authorization header:
{
"guildId": "123456789012345678",
"guildName": "CHS Student Council",
"tier": "tier2",
"allowedEmails": 412
}
The token identifies your server, so no server ID is needed in any request.
Endpoints¶
Base URL: https://stats.getemailverified.com/api/v1
| Method | Path | Purpose |
|---|---|---|
GET |
/me |
Token and connection check |
GET |
/emails |
How many addresses are on the list |
POST |
/emails |
Add one address, or many |
DELETE |
/emails/{address} |
Remove one address |
DELETE |
/emails?confirm=true |
Remove every address |
Add addresses¶
One address and many addresses use the same endpoint — send email for one, or
emails for a batch of up to 10,000.
curl -X POST https://stats.getemailverified.com/api/v1/emails \
-H "Authorization: Bearer $EMAILVERIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'
curl -X POST https://stats.getemailverified.com/api/v1/emails \
-H "Authorization: Bearer $EMAILVERIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"emails": ["[email protected]", "[email protected]", "[email protected]"]}'
added— addresses newly written to the listskipped— already on the list; adding twice is harmlessinvalid— rejected as malformed, and listed back so you can log themtotal— size of the list afterwards
Adding is idempotent, so a nightly "push the full roster" job is safe to re-run.
Remove one address¶
curl -X DELETE \
-H "Authorization: Bearer $EMAILVERIFY_TOKEN" \
https://stats.getemailverified.com/api/v1/emails/[email protected]
An address that isn't on the list returns 404 not_in_list. If the address
contains characters that are awkward in a URL, percent-encode it (+ becomes
%2B).
Remove every address¶
Clearing the whole list requires ?confirm=true, so a mistyped DELETE against
the collection can't wipe it by accident:
curl -X DELETE \
-H "Authorization: Bearer $EMAILVERIFY_TOKEN" \
"https://stats.getemailverified.com/api/v1/emails?confirm=true"
An empty list is not the same as no list
While an allowed-email list exists, only addresses on it can verify. Clearing it to zero entries removes the list entirely, which returns the server to domain-only checking — not to "nobody can verify".
Count the list¶
curl -H "Authorization: Bearer $EMAILVERIFY_TOKEN" \
https://stats.getemailverified.com/api/v1/emails
Why you can't read the list back¶
Addresses are stored as MD5 hashes of the lowercased address — the same way
/emaillist upload has always stored them — so the bot never holds a readable
copy of your members' email addresses.
That means:
- Adding works: we hash what you send and store the hash.
- Removing works: we hash what you send and delete the matching hash.
- Counting works.
- Listing is impossible, by design. Your own system stays the source of truth.
Errors¶
Every error has the same shape:
| Status | code |
Meaning |
|---|---|---|
400 |
invalid_body |
Body wasn't {"email": ...} or {"emails": [...]} |
400 |
invalid_address |
The address in the path isn't a valid address |
400 |
no_valid_addresses |
Every address in the batch was malformed |
400 |
confirmation_required |
Bulk delete without ?confirm=true |
401 |
missing_token |
No Authorization: Bearer header |
401 |
invalid_token |
Token unknown or revoked — generate a new one |
403 |
tier2_required |
The Pro subscription isn't active on this server |
404 |
not_in_list |
That address wasn't on the list |
404 |
guild_not_configured |
The server has never run /setup |
413 |
payload_too_large |
Body over 1 MB — split the batch |
429 |
rate_limited |
Over 60 requests/minute; see Retry-After |
503 |
entitlement_check_failed |
We couldn't reach Discord to confirm the subscription; retry |
Rate limit¶
60 requests per minute per server. Responses carry X-RateLimit-Remaining,
and a 429 carries Retry-After in seconds.
Batch rather than loop: 5,000 addresses in one POST is a single request, whereas
5,000 single-address calls will spend most of an hour being rate-limited.
A worked example¶
Sync a roster nightly, so people who left lose access and new joiners gain it:
#!/usr/bin/env bash
set -euo pipefail
API="https://stats.getemailverified.com/api/v1"
AUTH="Authorization: Bearer ${EMAILVERIFY_TOKEN:?set EMAILVERIFY_TOKEN}"
# roster.txt: one address per line, exported from your source of truth.
emails=$(jq -R -s 'split("\n") | map(select(length > 0))' < roster.txt)
# Replace the list wholesale: clear, then add the current roster.
curl -fsS -X DELETE -H "$AUTH" "$API/emails?confirm=true" > /dev/null
curl -fsS -X POST -H "$AUTH" -H "Content-Type: application/json" \
-d "{\"emails\": $emails}" "$API/emails"
For an incremental sync instead, POST the additions and DELETE the departures
individually — both are cheaper than a full replace and leave the list usable
throughout, with no window where nobody can verify.
Self-hosting¶
If you run your own instance, the API is served by the same Express app as the
stats endpoints (port 8181) under /api/v1. Set apiBaseUrl in
config/config.json so /api token generate shows your own host in its examples.
When monetization.enabled is false, there is no subscription to check and the
API is available to every server on your instance.