Geometry Dash API

Level data,
ready to ship.

Thumbnails, generated PNG cards, name search and full level data for any GD level. No API key, no rate limits, no sign-up.

PNG card generation
Search by name
KV cached, fast
CORS enabled
https://gd-level-api.liamt.xyz
gd-level-api.liamt.xyz/api/card?id=128
Live card preview
card
Examples

Cards in the wild

Generated on the fly from real GD data — fetched live, cached 1 hour.

Reference

API Endpoints

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.

Base URL
https://gd-level-api.liamt.xyz
Rate limits
None enforced — fair use assumed.
Card caching
KV-cached per level · 1 hour TTL.
Thumbnail caching
CDN-cached · 24 hour TTL.
GET/api/level?id={levelId}
Returns a JSON object with all resolved data for a single Geometry Dash level: name, author, download and like counts, difficulty, star rating, coins, song info, and ready-to-use URLs for the thumbnail, difficulty face image, and generated card.
idrequiredNumeric level ID. You can find it in-game or via /api/search.
Response fields
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.
Errors400 if id is missing or non-numeric · 404 if the level doesn't exist.
curl
JavaScript
Python
Go
PHP
Ruby
C#
Java
Rust
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"
  }
}
GET/api/levels?ids={id1,id2,...}
Fetches up to 10 levels in a single request. Returns a JSON array where each item has the same fields as /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.
idsrequiredComma-separated level IDs, e.g. ?ids=128,1,2. Duplicates are removed automatically. Max 10 IDs per call.
Errors400 if ids is missing or contains no valid numeric IDs.
curl
JavaScript
Python
PHP
Go
Java
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();
GET/api/card?id={levelId}
Generates a PNG image on the fly containing the level thumbnail, difficulty face icon, name, author, download/like counts, length, star rating, coin count, and special badges (Epic, Legendary, Mythic, Featured, coins). Rendered with Satori + Resvg directly in the Cloudflare Worker.
The card is cached in Cloudflare KV for 1 hour per ID + size combination. The first request for a level takes ~1–2 s to generate; all subsequent requests are served instantly (X-Cache: HIT). Use the url.card field returned by /api/level to get the pre-built URL directly.
idrequiredNumeric level ID.
sizeoptionalnormal (default) — outputs a 1600×520 px PNG · small — outputs a 1200×320 px PNG. Both are @2x renders of their logical canvas.
Errors400 if id is invalid · 404 if the level doesn't exist.
HTML
JavaScript
Python
PHP
Discord.js
Discord.py
Discord.NET
JDA
Eris
<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) }],
});
GET/thumbnail/{levelId}
Proxy that fetches the raw WebP thumbnail for a level from the upstream source (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.
Not all levels have a thumbnail — if none is available the upstream returns a 404 and so does this endpoint. In that case you can fall back to the noThumb placeholder used in the card generator.
levelIdrequiredLevel ID in the URL path — e.g. /thumbnail/128
Errors400 if ID is omitted · 404 if no thumbnail exists for that level.
HTML
JavaScript
React
Next.js
Vue
Svelte
Astro
Angular
PHP
Java
curl
<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
GET/api/random
Returns a random featured level. Picks a random page from the GDBrowser featured search and returns one result formatted identically to /api/level. Great for bots that show a "level of the day" or showcase random content.
No parameters required.
Errors502 if the upstream featured search fails.
curl
JavaScript
Python
Discord.js
Discord.py
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)
GET/api/user?name={username}
Returns profile data for a Geometry Dash player: stats (stars, diamonds, coins, demons, creator points), social links, global rank, and a ready-to-use avatar URL rendered by GDBrowser.
namerequiredExact GD username (case-insensitive).
Response fields
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.
Errors400 if name is missing · 404 if player not found.
curl
JavaScript
Python
Discord.js
Discord.py
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" }
}
Interactive

Try it live

Interact with the API directly — no code needed.

Card by ID/api/card
Generating card...
card
Thumbnail/thumbnail/:id
Loading thumbnail...
thumbnail
Multiple levels/api/levels
Loading levels...
Community

Comments & Bug Reports

Loading...