Developers · v1

Build with the GDM catalog

Find levels, compare recordings and link people to the right macro. The public API is free to access, read-only and requires no account or API key.

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
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 pathReturnsQuery parameters
/api/v1API version and endpoint pathsNone
/api/v1/levelsMatching levels, including every recording on each levelsearch, recorder, fps, page, limit
/api/v1/levels/{levelId}One level and all its recordingsNone; levelId is 1–12 digits
/api/v1/macrosOnly matching recordings, each with its level metadatasearch, recorder, fps, page, limit
/api/v1/statsLevel count, recording count and counts by recorderNone

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

ParameterDefault / allowed valuesBehavior
searchOptional; at most 100 characters after trimmingCase-insensitive literal substring. Not fuzzy search, a regex, or the website’s multi-word search.
recorderMega Hack, xdBot, zBotExact spelling and case. Encode spaces with URLSearchParams or %20.
fpsPositive finite number, including 59.94 or 360Exact recording-rate match. Scientific notation is accepted; zero, negatives and Infinity are rejected.
page1; integer from 1 to 1,000,000One-based page number; plain digits with no leading zero.
limit20; integer from 1 to 100Maximum 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.

Level list response
{
  "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.

Statistics response
{
  "data": {
    "levels": 1,
    "macros": 2,
    "recorders": [
      {
        "recorder": "Mega Hack",
        "macros": 0
      },
      {
        "recorder": "xdBot",
        "macros": 2
      },
      {
        "recorder": "zBot",
        "macros": 0
      }
    ]
  }
}

Field reference

Level fields

FieldTypeMeaning
levelIdstringGeometry Dash level ID, represented as a string.
namestringLevel name.
creatorstringLevel creator.
slugstringCurrent website slug; it may change when a level is renamed.
urlstringGDMacros level page.
descriptionstring | nullOptional level description.
videoUrlstring | nullOptional video URL.
thumbnailUrlstring | nullAbsolute thumbnail URL, or null.
addedAtstring | nullDate 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

FieldTypeMeaning
authorstringCatalog recording credit, not a verified account identity.
recorderstringExact recorder name, including case and spaces.
fpsnumberPositive finite recording FPS, including decimal rates; use this rate for playback.
testedAtstring | nullRecorded last-tested date, or null. Historical values may be upload-date baselines, not new playback verification.
downloadTypestringHosting provider label.
downloadUrlstringPublic 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

FieldMeaning
pageRequested one-based page
limitRequested page size
totalNumber of matching levels or recordings across all pages
totalPagesCeiling 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.

JavaScript
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);
Python (standard library)
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)
PowerShell
$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)"
}
JavaScript pagination
// 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

Application error response
{
  "error": {
    "code": "INVALID_QUERY",
    "message": "limit must be an integer between 1 and 100."
  }
}
HTTP statusCodeWhat to do
400INVALID_QUERYCorrect the parameters or malformed level ID; do not retry unchanged.
404NOT_FOUNDCheck the level ID/path. Removed levels may no longer exist.
405METHOD_NOT_ALLOWEDUse GET, HEAD or OPTIONS.
429Platform-controlledPause requests. Honor Retry-After if present; otherwise wait at least 60 seconds, then retry with backoff and jitter.
500INTERNAL_ERRORTemporary 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.

You’re invited

We’ve made a Discord server!

Come hang out with the GDM Community!

Join the Discord (opens in a new tab)