Thumbnails, generated PNG cards, name search and full level data for any GD level. No API key, no rate limits, no sign-up.
All endpoints are GET. CORS enabled for all origins. Returns JSON or PNG.
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": "..." }
}
?ids=128,1,2curl "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();
curl "https://thumball.liamt.xyz/api/search?q=Bloodbath&count=5"const res = await fetch(`https://thumball.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://thumball.liamt.xyz/api/search", params={"q": "Bloodbath"}).json()
$r = json_decode(file_get_contents("https://thumball.liamt.xyz/api/search?q=Bloodbath"), true);
const q = interaction.options.getString("query"); const res = await fetch(`https://thumball.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://thumball.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))
normal 1600×520px · small 1200×320px<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) }], });
/thumbnail/128<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.webpInteract with the API directly — no code needed.