Quick start
Base URL: https://www.gdmacros.com/api/v1. Use HTTPS and GET requests. Responses are JSON; you can use the API from a browser, server, bot or command-line tool.
curl "https://www.gdmacros.com/api/v1/levels?search=acheron&limit=5"For a known Geometry Dash level ID, request /api/v1/levels/73667628. The ID identifies a level, which can contain several recordings.
Endpoints
| GET path | Returns | Query parameters |
|---|---|---|
/api/v1 | API version and endpoint paths | None |
/api/v1/levels | Matching levels, including every recording on each level | search, recorder, fps, page, limit |
/api/v1/levels/{levelId} | One level and all its recordings | None; levelId is 1–12 digits |
/api/v1/macros | Only matching recordings, each with its level metadata | search, recorder, fps, page, limit |
/api/v1/stats | Level count, recording count and counts by recorder | None |
HEAD returns the same status and headers without a response body. OPTIONS supports browser preflight requests. POST, PUT, PATCH and DELETE return 405. This API does not provide submissions, account data, moderation or file conversion.
Filters & pagination
| Parameter | Default / allowed values | Behavior |
|---|---|---|
| search | Optional; at most 100 characters after trimming | Case-insensitive literal substring. Not fuzzy search, a regex, or the website’s multi-word search. |
| recorder | Mega Hack, xdBot, zBot | Exact spelling and case. Encode spaces with URLSearchParams or %20. |
| fps | Positive finite number, including 59.94 or 360 | Exact recording-rate match. Scientific notation is accepted; zero, negatives and Infinity are rejected. |
| page | 1; integer from 1 to 1,000,000 | One-based page number; plain digits with no leading zero. |
| limit | 20; integer from 1 to 100 | Maximum number of levels or recordings on a page. |
Filters combine with AND. A level matches recorder and FPS only when the same recording satisfies both. Level results include all recordings for that level; use /macros if you want only the matching recordings.
Level search checks name, creator, level ID and recording author, recorder and hosting label. Recording search checks level name, creator and ID plus that recording’s author and recorder. Empty search matches everything. Unknown query parameters and repeated parameters return 400, including parameters added to endpoints that accept none.
Levels are ordered alphabetically by name. Recordings follow that level order and then their order in the catalog. There is no sort parameter. No matches, or a page beyond the end, returns 200 with an empty data array. With no matches, total and totalPages are both zero.
Catalog changes can move entries between pages. Pagination does not provide a frozen snapshot across deployments; keep multi-page fetches short and refresh your cached copy periodically.
Response shapes
The following examples illustrate the structure; names and counts are examples, not current catalog totals. Missing optional values are returned as null.
{
"data": [
{
"levelId": "12345678",
"name": "Example Level",
"creator": "LevelCreator",
"slug": "example-level",
"url": "https://www.gdmacros.com/macro/example-level",
"description": null,
"videoUrl": null,
"thumbnailUrl": null,
"addedAt": "2026-10-27",
"macros": [
{
"author": "MacroAuthor",
"recorder": "xdBot",
"fps": 240,
"testedAt": null,
"downloadType": "GitHub",
"downloadUrl": "https://github.com/OWNER/REPO/releases/download/TAG/FILE.gdr2"
}
]
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 1,
"totalPages": 1
}
}A single level response is { "data": { ...level fields, "macros": [...] } } with no pagination. Recording lists use { "data": [{ ...recording fields, "level": { ...level fields } }], "pagination": { ... } }; their nested level has no macros array.
{
"data": {
"levels": 1,
"macros": 2,
"recorders": [
{
"recorder": "Mega Hack",
"macros": 0
},
{
"recorder": "xdBot",
"macros": 2
},
{
"recorder": "zBot",
"macros": 0
}
]
}
}Field reference
Level fields
| Field | Type | Meaning |
|---|---|---|
levelId | string | Geometry Dash level ID, represented as a string. |
name | string | Level name. |
creator | string | Level creator. |
slug | string | Current website slug; it may change when a level is renamed. |
url | string | GDMacros level page. |
description | string | null | Optional level description. |
videoUrl | string | null | Optional video URL. |
thumbnailUrl | string | null | Absolute thumbnail URL, or null. |
addedAt | string | null | Date the level was added, or null when unknown. |
Level list and detail responses also include macros, an array containing at least one recording. Every field listed above is present, including nullable fields.
Recording fields
| Field | Type | Meaning |
|---|---|---|
author | string | Catalog recording credit, not a verified account identity. |
recorder | string | Exact recorder name, including case and spaces. |
fps | number | Positive finite recording FPS, including decimal rates; use this rate for playback. |
testedAt | string | null | Recorded last-tested date, or null. Historical values may be upload-date baselines, not new playback verification. |
downloadType | string | Hosting provider label. |
downloadUrl | string | Public recording file URL. Download directly from this host, not through the API. |
Individual recordings do not have permanent API IDs. Do not use array position as a permanent identifier. Slugs, author credits and download URLs may change after catalog edits. Level IDs are strings; preserve their representation when constructing requests.
Mega Hack and xdBot submissions use .gdr2; zBot uses .gdr. The API returns existing URLs without converting files. Download directly from downloadUrl and retain author credits. A tested date or FPS label is metadata, not a guarantee of compatibility with every game or tool version.
Pagination fields
| Field | Meaning |
|---|---|
| page | Requested one-based page |
| limit | Requested page size |
| total | Number of matching levels or recordings across all pages |
| totalPages | Ceiling of total ÷ limit; zero when there are no matches |
Code examples
These examples make requests only when you run them. The documentation does not automatically query the API. Download links may redirect to the hosting provider.
const params = new URLSearchParams({ recorder: "xdBot", fps: "240", limit: "20" });
const response = await fetch("https://www.gdmacros.com/api/v1/macros?" + params);
if (!response.ok) {
// Firewall errors may be plain text or HTML: do not assume JSON.
throw new Error("GDM API returned HTTP " + response.status);
}
const { data, pagination } = await response.json();
for (const macro of data) {
console.log(macro.level.name, macro.fps, macro.downloadUrl);
}
console.log(pagination);import json
from urllib.parse import urlencode
from urllib.request import urlopen
from urllib.error import HTTPError, URLError
query = urlencode({"search": "acheron", "limit": 5})
try:
with urlopen("https://www.gdmacros.com/api/v1/levels?" + query, timeout=15) as response:
result = json.load(response)
for level in result["data"]:
print(level["name"], level["url"])
except HTTPError as error:
print("GDM API returned HTTP", error.code)
except URLError as error:
print("Connection failed:", error.reason)$uri = "https://www.gdmacros.com/api/v1/levels?search=acheron&limit=5"
try {
$result = Invoke-RestMethod -Uri $uri -TimeoutSec 15
$result.data | Select-Object levelId, name, url
$result.pagination
} catch {
Write-Error "The API request failed: $($_.Exception.Message)"
}// Fetch pages sequentially, with a pause to stay below the shared IP limit.
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const levels = [];
for (let page = 1; ; page++) {
const response = await fetch("https://www.gdmacros.com/api/v1/levels?limit=100&page=" + page);
if (!response.ok) throw new Error("HTTP " + response.status);
const result = await response.json();
levels.push(...result.data);
if (page >= result.pagination.totalPages) break;
await sleep(1500);
}
console.log(levels);Browser fetches need no Authorization header or credentials. Public application responses allow all origins. If an edge/firewall error lacks CORS headers, the browser may expose a generic network failure rather than its HTTP response.
Errors
{
"error": {
"code": "INVALID_QUERY",
"message": "limit must be an integer between 1 and 100."
}
}| HTTP status | Code | What to do |
|---|---|---|
| 400 | INVALID_QUERY | Correct the parameters or malformed level ID; do not retry unchanged. |
| 404 | NOT_FOUND | Check the level ID/path. Removed levels may no longer exist. |
| 405 | METHOD_NOT_ALLOWED | Use GET, HEAD or OPTIONS. |
| 429 | Platform-controlled | Pause requests. Honor Retry-After if present; otherwise wait at least 60 seconds, then retry with backoff and jitter. |
| 500 | INTERNAL_ERROR | Temporary catalog failure. Retry later with a bounded backoff. |
Application errors are JSON and marked no-store. Hosting/firewall errors can be text or HTML and may not include Retry-After or the application error shape. Check status before parsing JSON; handle timeouts and network errors too. Avoid infinite retries.
Limits & caching
The production firewall is configured for 60 requests per IP per 60-second fixed window, shared across the API prefix. OPTIONS is excluded. Shared networks share the allowance; the fixed window can also produce bursts near a boundary. This is a firewall setting, not an API key quota or a guaranteed global counter.
Cache results in your integration and avoid polling on every keystroke. Fetch pages sequentially and leave headroom for other users on the same IP. Use a timeout and bounded retries. API access is free, but availability remains subject to hosting allowances; there is no uptime SLA.
Successful responses permit a 300-second CDN cache and 60-second stale revalidation. Browser max-age is zero. Updates appear after website deployment and cache refresh; do not assume instant visibility. Responses intentionally omit ETags and do not offer a 304 conditional-request contract.
The v1 URL versions the interface. Clients should tolerate additional response fields and handle null values. Breaking interface changes should use a new version. The catalog itself can change or remove entries without changing the API version.
Read the Terms and Privacy Policy. Questions or broken metadata can be reported through support or the GitHub issue tracker.
For maintainers
The API reads the same repository catalog as the website. No additional database, credentials or DNS configuration is required. Keep public/openapi.json, this reference and API regression tests aligned when changing the contract.
Before releasing, run npm run test:api, catalog validation, TypeScript checks and the production build. Verify the production commit through /api/version, then smoke-test the live endpoint and CORS response headers. Firewall-generated responses must be tested on Vercel; local development does not enforce that rule.
Manage the API-only rate-limit rule in Vercel Firewall and review usage before increasing it. Roll back a bad release using the hosting deployment rollback, or revert the code change and redeploy. Keep catalog publications coordinated with rollbacks so accepted recordings remain available. API code changes do not require replaying database migrations.