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://thumball.liamt.xyz
thumball.liamt.xyz/api/card?id=128
Live card preview
card
Reference

API Endpoints

All endpoints are GET. CORS enabled for all origins. Returns JSON or PNG.

GET/api/level?id={levelId}
Returns a JSON with all resolved level data: name, author, stats, song, thumbnail and difficulty face URLs.
idrequiredNumeric level ID in Geometry Dash
curl
JavaScript
Python
Go
PHP
Ruby
C#
Java
Rust
curl "https://thumball.liamt.xyz/api/level?id=128"
const level = await fetch("https://thumball.liamt.xyz/api/level?id=128").then(r => r.json());
console.log(level.name, level.difficulty);
import requests
level = requests.get("https://thumball.liamt.xyz/api/level?id=128").json()
print(level["name"], level["difficulty"])
var level map[string]interface{}
resp, _ := http.Get("https://thumball.liamt.xyz/api/level?id=128")
json.NewDecoder(resp.Body).Decode(&level)
$level = json_decode(file_get_contents("https://thumball.liamt.xyz/api/level?id=128"), true);
echo $level["name"];
require "net/http"; require "json"
level = JSON.parse(Net::HTTP.get(URI("https://thumball.liamt.xyz/api/level?id=128")))
var level = await new HttpClient()
    .GetFromJsonAsync<JsonElement>("https://thumball.liamt.xyz/api/level?id=128");
var body = HttpClient.newHttpClient().send(
    HttpRequest.newBuilder().uri(URI.create("https://thumball.liamt.xyz/api/level?id=128")).build(),
    HttpResponse.BodyHandlers.ofString()).body();
let level: serde_json::Value = reqwest::get("https://thumball.liamt.xyz/api/level?id=128").await?.json().await?;

Response

{
  "id": 128, "name": "1st level", "author": "real storm",
  "downloads": 4011241, "likes": 297981, "difficulty": "Hard",
  "stars": 0, "coins": 0, "featured": false, "epic": false,
  "song": { "name": "Base After Base", "author": "DJVI" },
  "urls": { "thumbnail": "...", "diffFace": "...", "card": "..." }
}
GET/api/levels?ids={id1,id2,...}
Returns a JSON array with multiple levels in one request. Max 10 IDs.
idsrequiredComma-separated IDs — e.g. ?ids=128,1,2
curl
JavaScript
Python
PHP
Go
Java
curl "https://thumball.liamt.xyz/api/levels?ids=128,1,2"
const levels = await fetch("https://thumball.liamt.xyz/api/levels?ids=128,1,2").then(r => r.json());
import requests
levels = requests.get("https://thumball.liamt.xyz/api/levels?ids=128,1,2").json()
$levels = json_decode(file_get_contents("https://thumball.liamt.xyz/api/levels?ids=128,1,2"), true);
var levels []map[string]interface{}
resp, _ := http.Get("https://thumball.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://thumball.liamt.xyz/api/levels?ids=128,1,2")).build(),
    HttpResponse.BodyHandlers.ofString()).body();
GET/api/card?id={levelId}
Returns a generated PNG image with thumbnail, difficulty icon, stats and badges. First request ~1s, cached after.
idrequiredNumeric level ID
sizeoptionalnormal 1600×520px · small 1200×320px
HTML
JavaScript
Python
PHP
Discord.js
Discord.py
Discord.NET
JDA
Eris
<img src="https://thumball.liamt.xyz/api/card?id=128" style="width:100%;max-width:800px" />
const blob = await fetch("https://thumball.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://thumball.liamt.xyz/api/card?id=128").content)
echo '<img src="https://thumball.liamt.xyz/api/card?id=128">';
const data = await fetch(`https://thumball.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://thumball.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://thumball.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 for the level thumbnail served from thumball.liamt.xyz, cached for 24 hours.
levelIdrequiredID in the path — e.g. /thumbnail/128
HTML
JavaScript
React
Next.js
Vue
Svelte
Astro
Angular
PHP
Java
curl
<img src="https://thumball.liamt.xyz/thumbnail/128" style="width:320px;border-radius:8px"/>
// Fetch and display thumbnail as blob URL
const res  = await fetch("https://thumball.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://thumball.liamt.xyz/thumbnail/${id}`;
}
document.querySelector("img").src = getThumb(128);
export default function Thumb({ id }) {
  return <img src={`https://thumball.liamt.xyz/thumbnail/${id}`} style={{width:"320px"}}/>;
}
// next.config.js
const nextConfig = { images: { remotePatterns: [{ hostname: "thumball.liamt.xyz" }] } };
// Component
import Image from "next/image";
<Image src={`https://thumball.liamt.xyz/thumbnail/${id}`} width={320} height={180} alt=""/>
<template><img :src="`https://thumball.liamt.xyz/thumbnail/${id}`"/></template>
<script setup>defineProps({ id: Number })</script>
<script>export let id;</script>
<img src={`https://thumball.liamt.xyz/thumbnail/${id}`} style="width:320px"/>
---
const { id } = Astro.props;
---
<img src={`https://thumball.liamt.xyz/thumbnail/${id}`}/>
@Component({ template: `<img [src]="'https://thumball.liamt.xyz/thumbnail/'+id"/>` })
export class ThumbComponent { @Input() id!: number; }
echo "<img src='https://thumball.liamt.xyz/thumbnail/{$id}'>";
byte[] img = HttpClient.newHttpClient().send(
    HttpRequest.newBuilder().uri(URI.create("https://thumball.liamt.xyz/thumbnail/" + id)).build(),
    HttpResponse.BodyHandlers.ofByteArray()).body();
curl "https://thumball.liamt.xyz/thumbnail/128" --output thumb.webp
Interactive

Try it live

Interact with the API directly — no code needed.

Card by ID/api/card
Generating card...
card
Multiple levels/api/levels
Loading levels...