Thumbnails, generated PNG cards, name search and full level data for any GD level. No API key, no rate limits, no sign-up.
Generated on the fly from real GD data — fetched live, cached 1 hour.
All endpoints use GET. CORS is fully open (*) — call from any browser, bot, or server. Responses are either JSON or PNG. No authentication or API key required.
https://gd-level-api.liamt.xyz
/api/search.idNumeric level ID.
nameLevel name.
authorCreator username.
downloadsTotal download count.
likesNet like count (likes minus dislikes).
lengthLevel length in English: Tiny, Short, Medium, Long, XL, Platformer.
lengthEsSame length label in Spanish.
difficultyDifficulty string: Auto, Easy, Normal, Hard, Harder, Insane, Easy Demon … Extreme Demon, NA.
starsStar rating (0 = unrated).
coinsNumber of coins (0–3).
verifiedCoinstrue if coins are verified (silver), false if user coins (bronze).
featuredWhether the level is featured.
epic / legendary / mythicSpecial rating tiers (mutually exclusive, highest wins).
extrasArray of human-readable badge strings derived from the above flags, e.g. ["Epic", "3 coins ✓"]. Useful to display badges directly.
song.nameBackground song title.
song.authorSong artist name.
urls.thumbnailDirect URL to the level thumbnail image (WebP, served via this API's own proxy, cached 24 h).
urls.diffFaceDirect URL to the difficulty face PNG matching this level's exact tier (difficulty + coins + rating).
urls.cardURL to the auto-generated PNG card for this level — ready to embed in a Discord message or a webpage.
400 if id is missing or non-numeric · 404 if the level doesn't exist.
curl "https://gd-level-api.liamt.xyz/api/level?id=128"const level = await fetch("https://gd-level-api.liamt.xyz/api/level?id=128").then(r => r.json()); console.log(level.name, level.difficulty);
import requests level = requests.get("https://gd-level-api.liamt.xyz/api/level?id=128").json() print(level["name"], level["difficulty"])
var level map[string]interface{} resp, _ := http.Get("https://gd-level-api.liamt.xyz/api/level?id=128") json.NewDecoder(resp.Body).Decode(&level)
$level = json_decode(file_get_contents("https://gd-level-api.liamt.xyz/api/level?id=128"), true); echo $level["name"];
require "net/http"; require "json" level = JSON.parse(Net::HTTP.get(URI("https://gd-level-api.liamt.xyz/api/level?id=128")))
var level = await new HttpClient() .GetFromJsonAsync<JsonElement>("https://gd-level-api.liamt.xyz/api/level?id=128");
var body = HttpClient.newHttpClient().send( HttpRequest.newBuilder().uri(URI.create("https://gd-level-api.liamt.xyz/api/level?id=128")).build(), HttpResponse.BodyHandlers.ofString()).body();
let level: serde_json::Value = reqwest::get("https://gd-level-api.liamt.xyz/api/level?id=128").await?.json().await?;
Response
{
"id": 128,
"name": "1st level",
"author": "real storm",
"downloads": 4012851,
"likes": 298040,
"length": "Medium",
"lengthEs": "Medio",
"difficulty": "Hard",
"stars": 0,
"coins": 0,
"verifiedCoins": false,
"featured": false,
"epic": false,
"legendary": false,
"mythic": false,
"extras": [],
"song": {
"name": "Base After Base",
"author": "DJVI"
},
"urls": {
"thumbnail": "https://gd-level-api.liamt.xyz/thumbnail/128",
"diffFace": "https://autonick.github.io/diff-faces/levels/none/hard/none/none.png",
"card": "https://gd-level-api.liamt.xyz/api/card?id=128"
}
}
/api/level. If an individual ID is not found, that item will contain { "id": …, "error": "No encontrado" } instead of being skipped — so you always get one item per requested ID in the same order.
?ids=128,1,2. Duplicates are removed automatically. Max 10 IDs per call.400 if ids is missing or contains no valid numeric IDs.
curl "https://gd-level-api.liamt.xyz/api/levels?ids=128,1,2"const levels = await fetch("https://gd-level-api.liamt.xyz/api/levels?ids=128,1,2").then(r => r.json());
import requests levels = requests.get("https://gd-level-api.liamt.xyz/api/levels?ids=128,1,2").json()
$levels = json_decode(file_get_contents("https://gd-level-api.liamt.xyz/api/levels?ids=128,1,2"), true);
var levels []map[string]interface{} resp, _ := http.Get("https://gd-level-api.liamt.xyz/api/levels?ids=128,1,2") json.NewDecoder(resp.Body).Decode(&levels)
var body = HttpClient.newHttpClient().send( HttpRequest.newBuilder().uri(URI.create("https://gd-level-api.liamt.xyz/api/levels?ids=128,1,2")).build(), HttpResponse.BodyHandlers.ofString()).body();
/api/level plus description. All URL fields (thumbnail, diffFace, card) are fully resolved — no extra calls needed.
10, max 20.[] when there are no results. The response includes a description field (the in-game level description) which is not present in the single-level endpoint.
400 if q is missing · 502 if the upstream search failed.
curl "https://gd-level-api.liamt.xyz/api/search?q=Bloodbath&count=5"const res = await fetch(`https://gd-level-api.liamt.xyz/api/search?q=${encodeURIComponent("Bloodbath")}`).then(r => r.json()); res.forEach(l => console.log(l.id, l.name));
import requests results = requests.get("https://gd-level-api.liamt.xyz/api/search", params={"q": "Bloodbath"}).json()
$r = json_decode(file_get_contents("https://gd-level-api.liamt.xyz/api/search?q=Bloodbath"), true);
const q = interaction.options.getString("query"); const res = await fetch(`https://gd-level-api.liamt.xyz/api/search?q=${encodeURIComponent(q)}`).then(r => r.json()); await interaction.reply(res.map(l => `**${l.name}** by ${l.author}`).join("\n"));
@tree.command(name="search") async def search(i: discord.Interaction, name: str): async with aiohttp.ClientSession() as s: async with s.get(f"https://gd-level-api.liamt.xyz/api/search?q={name}") as r: data = await r.json() await i.response.send_message("\n".join(f"**{l['name']}** by {l['author']}" for l in data))
X-Cache: HIT). Use the url.card field returned by /api/level to get the pre-built URL directly.
normal (default) — outputs a 1600×520 px PNG · small — outputs a 1200×320 px PNG. Both are @2x renders of their logical canvas.400 if id is invalid · 404 if the level doesn't exist.
<img src="https://gd-level-api.liamt.xyz/api/card?id=128" style="width:100%;max-width:800px" />
const blob = await fetch("https://gd-level-api.liamt.xyz/api/card?id=128").then(r => r.blob()); document.querySelector("img").src = URL.createObjectURL(blob);
with open("card.png", "wb") as f: f.write(requests.get("https://gd-level-api.liamt.xyz/api/card?id=128").content)
echo '<img src="https://gd-level-api.liamt.xyz/api/card?id=128">';
const data = await fetch(`https://gd-level-api.liamt.xyz/api/level?id=${id}`).then(r => r.json()); const card = new AttachmentBuilder(data.urls.card, { name: 'card.png' }); const embed = new EmbedBuilder().setTitle(data.name).setImage('attachment://card.png'); await interaction.reply({ embeds: [embed], files: [card] });
async with s.get(data["urls"]["card"]) as r: png = await r.read() file = discord.File(io.BytesIO(png), filename="card.png") embed = discord.Embed(title=data["name"]).set_image(url="attachment://card.png") await i.response.send_message(embed=embed, file=file)
var png = await http.GetByteArrayAsync($"https://gd-level-api.liamt.xyz/api/card?id={id}"); var embed = new EmbedBuilder().WithImageUrl("attachment://card.png").Build(); await cmd.RespondWithFileAsync(new MemoryStream(png), "card.png", embed: embed);
byte[] png = HttpClient.newHttpClient().send( HttpRequest.newBuilder().uri(URI.create("https://gd-level-api.liamt.xyz/api/card?id=" + id)).build(), HttpResponse.BodyHandlers.ofByteArray()).body(); event.replyFiles(FileUpload.fromData(png, "card.png")).queue();
const png = await fetch(data.urls.card).then(r => r.arrayBuffer()); await interaction.createMessage({ embeds: [{ image: { url: "attachment://card.png" } }], files: [{ name: "card.png", file: Buffer.from(png) }], });
levelthumbs.prevter.me) and forwards it through this API. Cached for 24 hours via Cloudflare CDN. Use this instead of the upstream URL to avoid CORS issues, get consistent caching, and stay on your own domain.
noThumb placeholder used in the card generator.
/thumbnail/128400 if ID is omitted · 404 if no thumbnail exists for that level.
<img src="https://gd-level-api.liamt.xyz/thumbnail/128" style="width:320px;border-radius:8px"/>
// Fetch and display thumbnail as blob URL const res = await fetch("https://gd-level-api.liamt.xyz/thumbnail/128"); const blob = await res.blob(); document.querySelector("img").src = URL.createObjectURL(blob); // Or load by dynamic ID function getThumb(id) { return `https://gd-level-api.liamt.xyz/thumbnail/${id}`; } document.querySelector("img").src = getThumb(128);
export default function Thumb({ id }) { return <img src={`https://gd-level-api.liamt.xyz/thumbnail/${id}`} style={{width:"320px"}}/>; }
// next.config.js const nextConfig = { images: { remotePatterns: [{ hostname: "gd-level-api.liamt.xyz" }] } }; // Component import Image from "next/image"; <Image src={`https://gd-level-api.liamt.xyz/thumbnail/${id}`} width={320} height={180} alt=""/>
<template><img :src="`https://gd-level-api.liamt.xyz/thumbnail/${id}`"/></template> <script setup>defineProps({ id: Number })</script>
<script>export let id;</script> <img src={`https://gd-level-api.liamt.xyz/thumbnail/${id}`} style="width:320px"/>
--- const { id } = Astro.props; --- <img src={`https://gd-level-api.liamt.xyz/thumbnail/${id}`}/>
@Component({ template: `<img [src]="'https://gd-level-api.liamt.xyz/thumbnail/'+id"/>` }) export class ThumbComponent { @Input() id!: number; }
echo "<img src='https://gd-level-api.liamt.xyz/thumbnail/{$id}'>";
byte[] img = HttpClient.newHttpClient().send( HttpRequest.newBuilder().uri(URI.create("https://gd-level-api.liamt.xyz/thumbnail/" + id)).build(), HttpResponse.BodyHandlers.ofByteArray()).body();
curl "https://gd-level-api.liamt.xyz/thumbnail/128" --output thumb.webp/api/level. Great for bots that show a "level of the day" or showcase random content.
502 if the upstream featured search fails.curl "https://gd-level-api.liamt.xyz/api/random"const level = await fetch("https://gd-level-api.liamt.xyz/api/random").then(r => r.json()); console.log(level.name, level.difficulty);
import requests level = requests.get("https://gd-level-api.liamt.xyz/api/random").json() print(level["name"], level["difficulty"])
const level = await fetch("https://gd-level-api.liamt.xyz/api/random").then(r => r.json()); const card = new AttachmentBuilder(level.urls.card, { name: 'card.png' }); const embed = new EmbedBuilder().setTitle(level.name).setImage('attachment://card.png'); await channel.send({ embeds: [embed], files: [card] });
level = (await session.get("https://gd-level-api.liamt.xyz/api/random")).json() async with session.get(level["urls"]["card"]) as r: png = await r.read() file = discord.File(io.BytesIO(png), filename="card.png") embed = discord.Embed(title=level["name"]).set_image(url="attachment://card.png") await ctx.send(embed=embed, file=file)
usernameExact display name.
rankGlobal leaderboard rank, or null if not ranked.
stars / diamonds / coins / userCoins / demonsPlayer stats.
creatorPointsCreator points (rated levels).
socialsYouTube, Twitter/X, Twitch links (null if not set).
urls.avatarPNG icon rendered with the player's colors and glow — use directly in an <img> or Discord embed.
400 if name is missing · 404 if player not found.curl "https://gd-level-api.liamt.xyz/api/user?name=RobTop"const user = await fetch("https://gd-level-api.liamt.xyz/api/user?name=RobTop").then(r => r.json()); console.log(user.username, user.stars);
import requests user = requests.get("https://gd-level-api.liamt.xyz/api/user", params={"name": "RobTop"}).json()
const user = await fetch(`https://gd-level-api.liamt.xyz/api/user?name=${name}`).then(r => r.json()); const embed = new EmbedBuilder() .setTitle(user.username) .setThumbnail(user.urls.avatar) .addFields( { name: 'Stars', value: `★ ${user.stars}`, inline: true }, { name: 'Demons', value: `👹 ${user.demons}`, inline: true }, { name: 'Rank', value: `#${user.rank ?? 'unranked'}`, inline: true }, ); await interaction.reply({ embeds: [embed] });
user = (await session.get(f"https://gd-level-api.liamt.xyz/api/user?name={name}")).json() embed = discord.Embed(title=user["username"]) embed.set_thumbnail(url=user["urls"]["avatar"]) embed.add_field(name="Stars", value=f"★ {user['stars']}", inline=True) embed.add_field(name="Demons", value=f"👹 {user['demons']}", inline=True) await i.response.send_message(embed=embed)
Response
{
"username": "RobTop",
"playerID": "71", "accountID": "71",
"rank": 1, "stars": 0, "diamonds": 0,
"coins": 0, "userCoins": 0, "demons": 0,
"creatorPoints": 0,
"socials": { "youtube": null, "twitter": null, "twitch": null },
"urls": { "avatar": "https://gdbrowser.com/icon/RobTop?form=cube&col1=0&col2=3&glow=0" }
}
Interact with the API directly — no code needed.
When you log in with Discord we store your Discord user ID, username and avatar URL in a session cookie valid for 7 days. Comments you post are saved in our database linked to your Discord ID, username and avatar.
We never store your email address, password, phone number or Discord access tokens. We do not use analytics, trackers or third-party advertising scripts of any kind.
We use a single session cookie (gd_session) strictly to keep you logged in. No tracking cookies, no fingerprinting.
API requests are rate-limited per IP address to prevent abuse. We do not log, store or analyze individual API requests beyond temporary in-memory counters that reset every 2 minutes.
Login is handled through Discord OAuth2. We only request the identify scope (username + avatar). We do not access your messages, servers or friends list.
You can delete any of your own comments at any time. To request full deletion of your data, contact us on Discord. We will process your request within 7 days.
GD Level API is free to use for personal, educational and non-commercial projects. No attribution is required to use the API endpoints.
You may use this API to build bots, websites, apps or tools that display Geometry Dash data. Please be respectful of rate limits — they exist to keep the service free for everyone.
You may not resell API access, use it to build a competing commercial service, scrape in bulk beyond the rate limits, or intentionally attempt to disrupt or overload the service.
The source code of this project is licensed under CC BY-NC 4.0. You may fork and adapt it for non-commercial purposes with attribution. Commercial use of the source code requires explicit written permission.
The API is provided "as-is" without any guarantee of uptime or accuracy. We reserve the right to modify, limit or discontinue the service at any time without prior notice.
GD Level API is an independent fan project. It is not affiliated with, endorsed by or sponsored by RobTop Games. Geometry Dash is a trademark of RobTop Games AB.