downloader
Free Discord tool
Discord PFP Downloader
View and download anyone's Discord profile picture and banner in full size HD. Discord shows avatars at 128 pixels. The original is stored at up to 4096, and this gets you that file.
17 to 20 digits. A profile link works too, the ID is read out of it.
- No login or account
- Avatars and banners
- Animated GIFs supported
- Free API included
Avatar sizes
Response body
How to find a Discord user ID
Discord hides IDs behind a developer setting. It takes about fifteen seconds to turn on, and you only do it once.
-
1
Open user settings
Click the gear icon next to your name, bottom left of the Discord window.
-
2
Turn on Developer Mode
Go to Advanced in the sidebar and switch Developer Mode on.
-
3
Copy the ID
Right click any user, or long press on mobile, and choose Copy User ID.
-
4
Paste it above
You will get the avatar, the banner, and the account creation date.
What this Discord profile picture viewer does
Full resolution avatars
Discord's client renders profile pictures at 128 pixels. The original upload sits on Discord's CDN at up to 4096. Every size in between is one click away.
Animated avatars
Nitro users can set animated avatars. Those are detected automatically and displayed as animated WebP, then saved as a GIF file, so they never arrive as a frozen still frame.
Profile banners
If the account has a banner, you get that too, at full width and in its original format.
Account creation date
Every Discord ID has a timestamp built into it. That means the exact date and time an account was made, down to the millisecond.
A free API
Everything on this page is available as a plain GET request with no key. Drop it into a bot, a website, or a script.
Works on any device
Phone, tablet, or desktop. Nothing to install and no account to create.
Frequently asked questions
How do I download someone's Discord profile picture?
Copy their user ID using the four steps above, paste it into the box, and press Look up. Use the Download avatar button to save the largest available version to your device.
Can I see a Discord PFP in full size HD?
Yes. Discord stores the original upload and serves it at any power of two from 16 up to 4096 pixels. The client just requests a small one to save bandwidth. This tool asks for the large one.
Can I find someone by their Discord username?
No. Discord offers no public endpoint that turns a username into an ID, on purpose, because that would let anyone walk the entire user base. This tool takes an ID and nothing else, and keeps no directory of names. Anything claiming to do username search is querying a scraped database.
Does this work with animated avatars?
Yes. Animated avatar hashes begin with a_.
Those are displayed as animated WebP, which Discord
recommends and which is several times smaller than the same
GIF, and the download button saves them as a GIF file.
Will the person know I looked them up?
No. Nothing is sent to that account. The request goes to Discord's public API and the person receives no notification of any kind.
Can I download a Discord banner?
Yes, when the account has one. Banners are a Nitro feature, so most accounts will show Not set instead.
Is it free, and can I use the API in my own project?
Both free. The API needs no key and allows cross origin requests. See the API reference for JavaScript and Python examples. Please keep to the published rate limit of 60 requests per minute, or run your own copy.
Why does it need a user ID instead of a name?
Usernames can change at any time. IDs never do. Discord's own API is built around IDs for that reason, and every avatar URL contains one.
API reference
A free Discord avatar API. Five endpoints, every one a plain GET, no key and no auth header. Responses allow cross origin requests, so you can call them straight from a browser.
Start here
If you read one section, read this one. There is a single pattern that makes this API fast for your users and free for everyone, and it is three steps:
-
1
Ask for the profile once
Call
/api/v1/users/:idand keep two things: the user's ID andavatar.hash. -
2
Store the hash, not the URL
A hash is stable and lets you rebuild the image at any size later. Comparing hashes also tells you when someone changed their picture.
-
3
Point your image tag at Discord
Render
avatar.urldirectly. Discord's CDN serves the bytes, so this server is never in the path again.
Why it matters: a URL containing the avatar hash can never change contents, so Discord serves it with Cache-Control: public, max-age=31536000 and the visitor's browser keeps it for a year. Your page keeps rendering avatars even if this service is down, and neither of us pays for the image bytes.
Do this
- Fetch the profile once per user, then cache it
- Put avatar.url in your image tag
- Store avatar.hash in your database
- Ask for the size you actually display
- Use the batch route for a whole member list
- Call from the browser so limits are per visitor
Avoid this
- Calling the API on every page render
- Putting this domain in a long lived image tag
- Storing whole URLs that go stale
- Requesting 4096 for a 40 pixel avatar
- Looping one request per user
- Proxying images through your own server
That last one is worth spelling out. Discord's CDN sends Access-Control-Allow-Origin: * and applies no hotlink protection, so a browser anywhere can load or read those images with no proxy of your own. If you were planning to pipe avatars through your backend, you do not need to.
Quick start
curl https://pfpgrab.com/api/v1/users/992511112223294201
Every response is JSON. Every failure is JSON in the same
shape, with a stable error.code you can branch
on. Image bytes always come from Discord's CDN, never from
this server.
Which route to use
Two ways to get to an image. They are not equivalent, and picking the right one is most of what separates a fast integration from a slow one.
| Question | Profile JSON | Avatar redirect |
|---|---|---|
| Route | /users/:id | /users/:id/avatar |
| Hits this server | Once per user, then cache | Every single image render |
| Cacheable forever | Yes, the URL holds the hash | No, only 5 minutes |
| Works if we are down | Yes, once you have the hash | No |
| Extra data | Name, banner, created date | None |
| Best for | Apps, sites, bots, anything real | A quick one liner, a README badge |
The redirect exists because it is genuinely convenient when you have an ID and nowhere to put a hash. It is not the route to build a product on.
Endpoints
/api/v1/users/:id
The endpoint to build on. Returns the full profile with CDN URLs you can use directly.
| Name | In | Description |
|---|---|---|
| id | path | Discord user ID. 17 to 20 digits. |
| size | query | Power of two from 16 to 4096. Sets the width of avatar.url and banner.url. Defaults to 1024. Anything else returns invalid_size. |
{
"id": "992511112223294201",
"username": "exampleuser",
"display_name": "Example User",
"is_bot": false,
"created_at": "2022-07-01T19:24:49.118Z",
"accent_color": "#5865f2",
"avatar": {
"hash": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"url": "https://cdn.discordapp.com/avatars/992511112223294201/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.webp?size=1024",
"static_url": "https://cdn.discordapp.com/avatars/992511112223294201/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.png?size=1024",
"full_size_url": "https://cdn.discordapp.com/avatars/992511112223294201/a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6.webp?size=4096",
"size": 1024,
"animated": false,
"is_default": false
},
"banner": null
}
url is WebP. Measured against a real avatar at 1024 that is 5.4 KB where the PNG is 41 KB, for a picture the eye cannot tell apart, and every browser in use accepts it. static_url is the PNG if you need one.
An animated avatar adds animated=true to the url and carries an extra gif_url for saving:
"avatar": {
"hash": "a_1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e",
"url": "https://cdn.discordapp.com/avatars/.../a_1b2c....webp?size=1024&animated=true",
"static_url": "https://cdn.discordapp.com/avatars/.../a_1b2c....png?size=1024",
"full_size_url": "https://cdn.discordapp.com/avatars/.../a_1b2c....webp?size=4096&animated=true",
"gif_url": "https://cdn.discordapp.com/avatars/.../a_1b2c....gif?size=4096",
"size": 1024,
"animated": true,
"is_default": false
}
/api/v1/users?ids=a,b,c
Up to 25 users in one request. Use this instead of a loop: one call for a whole member list is faster for you and much cheaper for this server.
| Name | In | Description |
|---|---|---|
| ids | query | Comma separated user IDs. Duplicates are collapsed. Maximum 25. Repeating the parameter works too, so ?ids=1&ids=2 and ?ids=1,2 are the same request. |
| size | query | Applies to every profile in the response. |
One bad ID does not fail the batch. The status stays 200 and each ID lands in either data or errors. A batch consumes one unit of rate limit per ID, so the cost lands on whoever creates it.
{
"requested": 3,
"returned": 2,
"data": [
{ "id": "992511112223294201", "username": "exampleuser" },
{ "id": "80351110224678912", "username": "anotheruser" }
],
"errors": [
{
"id": "000000000000000000",
"error": {
"code": "user_not_found",
"message": "No Discord account has that ID."
}
}
]
}
/api/v1/users/:id/avatar
A 302 to the image on Discord's CDN, so it works inside an image tag as is. Convenient, but see Which route to use before you reach for it.
| Name | In | Description |
|---|---|---|
| id | path | Discord user ID. |
| size | query | Power of two from 16 to 4096. Defaults to 1024. |
<img src="https://pfpgrab.com/api/v1/users/992511112223294201/avatar?size=256">
Both Access-Control-Allow-Origin: * and Cross-Origin-Resource-Policy: cross-origin are set, which is what lets that tag work on any domain. Setting only the first is a common mistake and it is invisible until someone tries to embed it: CORS never applies to a no-cors load, and only the second does.
/api/v1/users/:id/banner
The same idea for banners, with the same size parameter. Returns 404 no_banner when the account has not set one, which is most accounts, so handle that case rather than letting an image tag break.
/api/health
Status, version, and uptime. A working health check for any platform that wants one. Never cached.
Response fields
| Field | Notes |
|---|---|
| display_name | Falls back to username when the account has no display name set. |
| created_at | Decoded from the ID itself. No extra request was needed to produce it. |
| accent_color | Hex string or null. Discord sends an integer, we convert it. |
| avatar.hash | The raw CDN hash. Store this. Compare it between calls to detect a change without downloading anything. |
| avatar.url | Ready for an image tag. Always WebP, with animated=true when the avatar is animated. This is the one to use. |
| avatar.static_url | Always PNG, so never animated. Use it for a list that should not have twenty things moving in it, or anywhere WebP is not accepted. |
| avatar.full_size_url | The original at 4096 as WebP, ignoring size. Note the CDN never upscales, so on a small source picture this is the same bytes as a smaller size. |
| avatar.gif_url | Present only when animated is true. Use it for saving a file. The CDN rejects a GIF request on a static avatar, which is why the field is absent rather than null. |
| avatar.size | Echoes the size you asked for, or null for default avatars. |
| avatar.is_default | True when the account has set no picture. Discord serves its six built in avatars at one fixed size, so size is ignored for those. |
| banner | Same shape as avatar, or null. Banners are a Nitro feature. |
Why WebP for animated images: Discord recommends it, because an avatar originally uploaded as WebP or AVIF does not convert cleanly to GIF, while WebP works whatever the source was. It is also far smaller. A typical avatar at 256 pixels is around 40 KB as PNG and around 5 KB as WebP.
This shape is deliberately not Discord's own, so a change on their side does not become a breaking change here. v1 is additive: new fields may appear, so parse defensively and ignore what you do not recognise. Removing or renaming a field would mean v2.
Recipes
Show a Discord avatar next to a username on your site. This is the most common use, and the whole job is four lines. Fetch once when the user links their account, store the pair, and render from your own database from then on.
// Once, when the user gives you their Discord ID
const profile = await getProfile(discordId);
await db.users.update(userId, {
discordId,
avatarHash: profile.avatar.hash,
avatarAnimated: profile.avatar.animated,
});
// Every render after that. No call to this API at all.
function avatarSrc(user, size = 64) {
if (!user.avatarHash) return "https://cdn.discordapp.com/embed/avatars/0.png";
const extension = user.avatarAnimated ? "webp" : "png";
const animated = user.avatarAnimated ? "&animated=true" : "";
return `https://cdn.discordapp.com/avatars/${user.discordId}/${user.avatarHash}.${extension}?size=${size}${animated}`;
}
Ask for the size you display. ?size=64 is about 4 KB and ?size=1024 is about 40 KB, and a room full of 40 pixel avatars at 1024 is a lot of wasted load on your own visitors. Refresh the stored hash occasionally, since people change their picture.
Render a member list. One batch call, not twenty five lookups. Split anything longer than 25 into chunks.
const response = await fetch(
`${BASE}/api/v1/users?ids=${ids.slice(0, 25).join(",")}&size=64`,
);
const { data, errors } = await response.json();
// data holds the profiles that resolved, errors the ones that did not.
// The status is 200 either way, so check errors rather than response.ok.
Let someone save an avatar. Fetch the CDN URL in the browser and save the blob. Nothing touches this server, so it costs nothing however many people press the button.
async function saveAvatar(profile) {
// Animated avatars get saved as GIF, which is what people expect from a file.
// Everything else keeps the format the URL already names.
const url = profile.avatar.gif_url ?? profile.avatar.full_size_url;
const extension = new URL(url).pathname.split(".").pop();
const blob = await (await fetch(url)).blob();
const objectUrl = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = objectUrl;
link.download = `${profile.username}.${extension}`;
link.click();
setTimeout(() => URL.revokeObjectURL(objectUrl), 30_000);
}
Code examples
Works in the browser and in Node 18 or newer.
const BASE = "https://pfpgrab.com";
class ProfileError extends Error {
constructor({ code, message, retry_after }) {
super(message);
this.code = code;
this.retryAfter = retry_after ?? 0;
}
}
async function getProfile(userId, size = 1024) {
const response = await fetch(`${BASE}/api/v1/users/${userId}?size=${size}`);
const body = await response.json();
// Every failure is { "error": { "code", "message" } }.
// Branch on code, never on the wording of message.
if (!response.ok) throw new ProfileError(body.error);
return body;
}
// Codes that mean "busy right now" rather than "you asked for the wrong
// thing". Those are the only ones worth trying again.
const RETRYABLE = new Set([
"server_busy",
"upstream_rate_limited",
"upstream_timeout",
"upstream_error",
]);
async function getProfileWithRetries(userId, attempts = 3) {
for (let attempt = 1; ; attempt += 1) {
try {
return await getProfile(userId);
} catch (err) {
if (attempt >= attempts || !RETRYABLE.has(err.code)) throw err;
const wait = err.retryAfter || 2 ** (attempt - 1);
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
}
}
try {
const profile = await getProfileWithRetries("992511112223294201");
console.log(profile.display_name, profile.avatar.hash);
} catch (err) {
if (err.code === "user_not_found") console.log("No such account.");
else if (err.code === "rate_limited") console.log("Slow down.");
else console.error(err.code, err.message);
}
Uses requests. Install it with
pip install requests.
import requests
BASE = "https://pfpgrab.com"
class ProfileError(RuntimeError):
def __init__(self, code: str, message: str, retry_after: int = 0):
super().__init__(message)
self.code = code
self.retry_after = retry_after
def get_profile(user_id: str, size: int = 1024) -> dict:
"""Fetch one Discord profile."""
response = requests.get(
f"{BASE}/api/v1/users/{user_id}",
params={"size": size},
timeout=10,
)
body = response.json()
if not response.ok:
error = body.get("error", {})
raise ProfileError(
error.get("code", "unknown"),
error.get("message", ""),
error.get("retry_after", 0),
)
return body
def get_profiles(user_ids: list[str]) -> dict:
"""Up to 25 profiles in one request."""
response = requests.get(
f"{BASE}/api/v1/users",
params={"ids": ",".join(user_ids[:25])},
timeout=15,
)
response.raise_for_status()
return response.json()
def save_avatar(profile: dict, path: str) -> None:
"""Download straight from Discord's CDN, not through the API."""
avatar = profile["avatar"]
url = avatar.get("gif_url") or avatar["full_size_url"]
with requests.get(url, stream=True, timeout=30) as response:
response.raise_for_status()
with open(path, "wb") as file:
for chunk in response.iter_content(8192):
file.write(chunk)
if __name__ == "__main__":
try:
profile = get_profile("992511112223294201")
except ProfileError as err:
print(f"Lookup failed ({err.code}): {err}")
else:
print(profile["display_name"], profile["created_at"])
save_avatar(profile, "avatar.png")
A hook you can drop into any component.
import { useEffect, useState } from "react";
const BASE = "https://pfpgrab.com";
export function useDiscordProfile(userId, size = 1024) {
const [profile, setProfile] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!userId) return;
// Cancel the request if the component unmounts or userId changes,
// so a slow response cannot overwrite a newer one.
const controller = new AbortController();
setLoading(true);
setError(null);
fetch(`${BASE}/api/v1/users/${userId}?size=${size}`, {
signal: controller.signal,
})
.then(async (response) => {
const body = await response.json();
if (!response.ok) throw body.error;
setProfile(body);
})
.catch((err) => {
// { code, message } from the API, or a DOMException on abort.
if (err.name !== "AbortError") setError(err.code ? err : null);
})
.finally(() => setLoading(false));
return () => controller.abort();
}, [userId, size]);
return { profile, error, loading };
}
Errors
Every failure returns the matching HTTP status and a body shaped like this:
{
"error": {
"code": "user_not_found",
"message": "No Discord account has that ID."
}
}
code is stable and safe to branch on. message is for humans and may be reworded at any time, so do not match on it. Some codes carry extra fields alongside those two, such as retry_after on a rate limit or valid_sizes on a bad size. Anything with a retry_after also sends it as a Retry-After header.
Errors that state a fact about the request, such as user_not_found or invalid_size, are sent Cache-Control: public, max-age=60, because they will say the same thing a second later. Everything else is no-store, so one bad moment at Discord is never frozen into a cache between here and your users.
| Code | HTTP | Meaning and what to do |
|---|---|---|
| invalid_user_id | 400 | Not 17 to 20 digits. Fix the input, do not retry. |
| invalid_size | 400 | size is not a power of two from 16 to 4096. The allowed list comes back in valid_sizes. |
| missing_ids | 400 | A batch call arrived with no ids parameter. |
| too_many_ids | 400 | More than 25 IDs in one batch. Split the list. |
| user_blocked | 403 | The owner of that ID asked to be excluded from this service. Permanent, and nothing to retry. |
| user_not_found | 404 | No account matches. Tell the user, do not retry. |
| no_banner | 404 | The account exists but has set no banner. Expected for most accounts. |
| endpoint_not_found | 404 | No route matches that path. Check the spelling. |
| method_not_allowed | 405 | Everything here is a GET. |
| rate_limited | 429 | You have used your budget. Wait retry_after seconds. Do not retry sooner, it only spends more of it. |
| upstream_rate_limited | 429 | Discord is throttling this service, not you. Retry with backoff. |
| upstream_error | 502 | Discord replied with something unexpected. Retry once. |
| server_busy | 503 | Too many lookups are already queued for Discord. Retry after the wait it gives you. |
| upstream_timeout | 503 | Discord did not respond in time. Retry with backoff. |
| internal_error | 500 | A bug on this end. Retrying will probably not help. |
Limits
- 60 requests per minute per client. A batch call costs one unit per ID, so 25 IDs in one request costs 25.
- Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Read them and throttle yourself rather than waiting for a 429.
- Treat those three as advisory. Successful responses are cacheable, and a CDN that serves you one from its cache hands you the header values that were current when it was stored, which may be another caller's. A 429 with Retry-After is the authoritative signal.
- Successful lookups are cached for 10 minutes, so repeat calls for the same ID are instant and cost nothing.
- Identical requests arriving at the same moment are merged into a single call to Discord.
- Call from the browser where you can, so the limit applies per visitor. Calling from your own backend means every one of your users shares a single bucket.
- Set a timeout on your side. 10 seconds is plenty.
- The real ceiling is not this server. Discord allows roughly 50 requests per second on a single bot token, which is why caching a hash on your side matters more than any limit here.
Run your own
This project is open source and MIT licensed. If you are building something that needs its own limits, or you would rather not depend on someone else's uptime, run your own copy. It is a single Node process with four dependencies and no build step.
git clone https://github.com/mohdmahmodi/discord-pfp-api.git
cd discord-pfp-api
npm install
cp .env.example .env # paste your own bot token in
npm start
You bring your own Discord bot token. Create an application in
the
developer portal, open the Bot tab, press Reset Token, and put it in
.env. The bot needs no permissions, no privileged
intents, and no server memberships, because
/users/{user_id} reads public
profile data only.
Two settings to get right before you expose it. Set TRUST_PROXY_HOPS to match how many proxies sit in front of the process, since it decides which address the rate limiter counts against. And leave TRUST_CLOUDFLARE off until your origin refuses connections that did not arrive through Cloudflare. The README covers both, and the startup banner prints what is in use.
Terms and privacy
Plain language, no clauses designed to be skipped. If something here is unclear, assume the reading that gives you more privacy.
What this service can see
- User ID
- Username and display name
- Avatar image and hash
- Banner image, if set
- Profile accent colour
- Account creation date
- Whether the account is a bot
What it cannot see
- Email address
- Phone number
- Messages or direct messages
- Servers the account is in
- Friend lists
- IP address or location
- Online status or activity
01. What we can see
This service reads public profile data through Discord's official API using a bot token with no permissions, no privileged intents, and no server memberships. The fields in the table above are the entire response. The private fields are not withheld by policy, they are simply not present in the data Discord returns.
Looking someone up sends nothing to that account. They receive no notification and have no way to tell it happened.
A lookup needs a user ID. There is no username search here, and no list of names is kept, because Discord deliberately provides no way to turn a name into an ID and building one would mean assembling a directory of people. Sites advertising username search are querying scraped databases put together without consent.
02. Your data
There is no account system, so there is nothing to sign up for and nothing to delete. Specifically:
- Lookup results are cached in memory for 10 minutes, stay usable for up to 50 minutes more while they refresh, and are dropped after that. Nothing about a looked up profile is written to disk.
- Your recent lookups are stored in your own browser, not on the server. Clearing them removes them completely.
- Avatar and banner images are served to you by Discord's CDN directly. Downloads are assembled in your browser, so no image ever passes through this server.
- No cookies are set. No analytics or advertising scripts run.
- Request logs are kept only as long as needed to operate and secure the service.
03. Acceptable use
Do not use this to harass, impersonate, stalk, or compile records on other people. Doing so breaks Discord's Terms of Service as well as these, and access may be blocked without notice.
Automated use is limited to 60 requests per minute per client. Stay within it and you will never see a problem. Scraping at volume is blocked. If you need more, the project is open source and you can run your own copy on your own bot token.
04. Image rights
Avatars and banners belong to the people who uploaded them, not to this service and not to you. Downloading an image here does not give you a licence to republish it, use it commercially, or pass it off as your own. You are responsible for what you do with anything you save.
05. Legal
This project is not affiliated with, endorsed by, or sponsored by Discord Inc. Discord is a trademark of Discord Inc.
The service is provided as is, with no warranty. It depends on Discord's API, which can change or become unavailable at any time and without notice.
The full privacy policy, including who else your browser talks to while this page is open, is in #privacy.
Privacy policy
This is the full policy for pfpgrab.com and the API it serves. It is written to be read, not to be skipped. Where a sentence could be read two ways, take the one that gives you more privacy, because that is the one that was meant.
01. The short version
There are no accounts, no cookies set by this site, no analytics, no advertising, and no tracking of any kind. Nothing about a profile you look up is written to disk. Everything this service holds lives in the memory of one process and is gone within an hour, or immediately if the process restarts.
What this service does
- Reads public Discord profile fields by user ID
- Holds the result in memory for up to an hour
- Counts requests per IP address to enforce a rate limit
- Returns links that point at Discord's own servers
- Stores your recent lookups in your own browser
What it never does
- Set a cookie, or run an analytics or advertising script
- Write profile data, images, or IP addresses to disk
- Build a searchable directory of Discord users
- Sell, licence, or share data with anyone
- Tell the person you looked up that it happened
- Store or proxy a single byte of any avatar image
02. Who runs this
pfpgrab is run by Mohd Mahmodi, an individual, as a free and non-commercial project. There is no company behind it and no revenue attached to it. For anything in this policy, including a deletion request, the contact is [email protected].
The entire server is open source. If you would rather verify a claim in
this policy than trust it, the code that makes each one true is at
github.com/mohdmahmodi/discord-pfp-api. Every statement below about what is stored and for how long is a line
in server.js you can read for yourself.
03. If your profile was looked up
Somebody entering your Discord user ID here causes this server to ask Discord's official API for your public profile. That is the same data Discord shows to anyone who can see your profile in the app. Discord is the source; this service is a reader.
The complete set of fields involved:
| Field | What it is |
|---|---|
| id | Your Discord user ID, which is what was entered |
| username | Your Discord username |
| display_name | Your display name, if you have set one |
| avatar | The hash of your avatar, which forms its image URL |
| banner | The hash of your banner, if you have one |
| accent_color | Your profile accent colour |
| bot | Whether the account is a bot |
| created_at | Not from Discord. Decoded from the first 42 bits of your ID, which are a timestamp |
Your email address, phone number, messages, servers, friends, IP address, online status, and activity are not in that response. They are not withheld here by policy: Discord simply does not return them to this kind of request, and this service holds no permissions, no privileged intents, and no server memberships that would change that.
You receive no notification, because nothing is sent to your account. A lookup is a read of Discord's public API and is invisible from your side.
There is no way to search by username here, and no list of names is kept. Discord deliberately provides no way to turn a name into an ID, and building one would mean assembling a directory of people. Sites that advertise username search are querying scraped databases put together without consent, and this is not one of them.
04. If you are visiting the site
Your IP address reaches this server the way it reaches any website, as a property of the connection. It is used for exactly one thing: counting requests so that one client cannot exhaust the service for everyone else. The counter lives in memory, resets every 60 seconds, and is never written anywhere.
- No cookies are set by this site. There is no session, no login, and nothing to consent to.
- No analytics. There is no Google Analytics, no Plausible, no Fathom, no pixel, and no error reporting service.
-
Your recent lookups stay in your browser. They are
kept in
localStorageunder the keydpa:recent, never sent to the server, and cleared for good by the Clear button in the sidebar or by clearing site data. - Downloads never touch this server. When you save an avatar, your browser fetches the image from Discord's CDN and assembles the file locally. No image passes through this machine, which is also why the service costs almost nothing to run.
05. How long anything is kept
Everything below is memory only. This service runs no database and writes no data files. Restarting the process erases all of it, and deployments restart the process.
| What | Where | How long |
|---|---|---|
| A looked up profile | Server memory | 10 minutes fresh, then up to 50 more while it refreshes |
| An ID that matched no account | Server memory | 60 seconds |
| Rate limit counter for an IP | Server memory | 60 seconds, then reset |
| Your recent lookups | Your browser | Until you clear them |
| Avatar and banner images | Never held here at all | — |
Operational logs record errors and the fact that the server is running. They do not record user IDs or IP addresses, and they are rotated by the operating system rather than kept.
06. Who else sees you when you use this site
This is the part most privacy policies leave vague, so here it is plainly. Loading this page causes your browser to talk to a few other companies, and each of them sees your IP address because that is how the web works. Nothing is shared with them by this service; they see you because your browser connects to them directly.
| Who | Why your browser contacts them |
|---|---|
| cdn.discordapp.com | Serves every avatar and banner image. Discord sees the request for each image you view or save |
| cloudflare.com |
Sits in front of this site as a CDN and firewall. Cloudflare sets a
__cf_bm bot management cookie of its own and keeps
request logs under its own policy
|
| cdnjs.cloudflare.com | Serves the icon font and the code highlighter used on this page. Each file is pinned to a hash, so a modified file is refused by your browser |
| fonts.googleapis.com | Serves the typefaces the page is set in. Google sees the request |
Those four are the complete list. There is no fifth. If you want none of them, the project is open source and a self hosted copy can drop the fonts and the icon set entirely.
07. Using the API in your own project
If you build on this API, you become responsible for what you do with what it returns. Two things are worth stating clearly.
The URLs returned point at Discord's CDN, not at this service. When your visitors load one, their browsers talk to Discord directly and Discord sees them. That is the correct design and it is why the images stay fast and free, but it does mean an avatar you embed is a request to Discord made by your visitor.
An avatar URL contains a hash that changes whenever the person changes their picture. If you store one, refresh it periodically rather than keeping it forever, so that somebody who changes or removes their avatar stops being shown the old one by your site.
Do not use this API to compile records on people, to enumerate accounts by walking through IDs, or to build a directory. Those break Discord's Developer Policy as well as these terms, and access can be blocked without notice.
08. Your rights
Under the GDPR, the UK GDPR, the CCPA, and similar laws you have rights of access, correction, deletion, restriction, portability, and objection. This service is unusual in how easy those are to satisfy: there is no account to close, no profile to export, and nothing about you stored for longer than an hour.
- Deletion. Email [email protected] with your Discord user ID and the cached copy will be dropped immediately. In practice it will have expired on its own before the message arrives.
-
Blocking future lookups. Ask, and your ID will be
refused by this service from then on. You do not have to explain why.
It is added to the
BLOCKED_USER_IDSlist the server reads at startup, after which that ID returns403 user_blockedand is never sent to Discord again. - Access. What is held is exactly the table in section 03 and nothing else. There is no additional profile to disclose.
- The source. Changing what this service can see about you is done at Discord, not here. Changing your avatar changes its hash, and the old URL stops describing you.
The lawful basis for processing is legitimate interest: showing a person the public profile picture belonging to an ID they already have, at their own request. No profiling, automated decision making, or advertising is involved. Requests are answered within 30 days and normally within a day.
09. Children
This service is not directed at children under 13, and Discord itself requires users to be at least 13. No age is collected, because no account exists to attach one to. If you believe a child's data is being handled here in a way it should not be, write to [email protected] and it will be dealt with straight away.
10. Changes to this policy
The date at the top changes whenever this document does. Because the policy lives in the repository, every edit is a commit with a diff you can read, so there is no version of it you cannot check. A change that makes this service collect more than it does today will be described here rather than quietly folded in.
11. Contact
Getting in touch
Privacy, deletion, blocking, and anything else in this document:
[email protected].
Public or informal:
@mohdmahmodi on X.
Security problems: please read
SECURITY.md
first, then email rather than opening a public issue.
This project is not affiliated with, endorsed by, or sponsored by Discord Inc. Discord is a trademark of Discord Inc. Avatars and banners belong to the people who uploaded them, and nothing here grants a licence to republish them.