Skip to content

Client

The main client classes for interacting with the Geometry Dash API.

Both Client (synchronous) and AsyncClient (asynchronous) are available.

Client (Sync)

gdpy.client.Client

Client(base_url: str | None = None)

Synchronous client for interacting with the Geometry Dash API.

This client provides methods to interact with Geometry Dash's private API, including user lookups, level searches, and authentication.

Based on the Geometry Dash API documentation: https://github.com/Rifct/gd-docs

Parameters:

Name Type Description Default
base_url str | None

Optional custom base URL for the API. Defaults to boomlings.com.

None

Example:

client = Client()
user = client.get_user(account_id=71)
print(user.username)
client.close()

# Or use as context manager:
with Client() as client:
    user = client.get_user(account_id=71)
    print(user.username)

Initialize the client.

Parameters:

Name Type Description Default
base_url str | None

Optional custom base URL for the API.

None
Source code in gdpy/client.py
def __init__(self, base_url: str | None = None) -> None:
    """Initialize the client.

    Args:
        base_url: Optional custom base URL for the API.
    """
    self.base_url = base_url or URLs.DEFAULT
    self._client: httpx.Client | None = None
    self._account_id: int | None = None
    self._player_id: int | None = None
    self._username: str | None = None
    self._password: str | None = None

account_id property

account_id: int | None

Get the authenticated account ID.

is_authenticated property

is_authenticated: bool

Check if the client is authenticated.

username property

username: str | None

Get the authenticated username.

accept_friend_request

accept_friend_request(request_id: int) -> bool

Accept a friend request. Requires authentication.

Parameters:

Name Type Description Default
request_id int

The friend request ID.

required

Returns:

Type Description
bool

True if accepted successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def accept_friend_request(self, request_id: int) -> bool:
    """Accept a friend request. Requires authentication.

    Args:
        request_id: The friend request ID.

    Returns:
        True if accepted successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to accept friend request")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "requestID": str(request_id),
    }
    response = self._request("acceptGJFriendRequest20.php", data)
    return response == "1"

block_user

block_user(user_id: int) -> bool

Block a user. Requires authentication.

Parameters:

Name Type Description Default
user_id int

The user's account ID to block.

required

Returns:

Type Description
bool

True if blocked successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def block_user(self, user_id: int) -> bool:
    """Block a user. Requires authentication.

    Args:
        user_id: The user's account ID to block.

    Returns:
        True if blocked successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to block user")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "targetAccountID": str(user_id),
    }
    response = self._request("blockGJUser20.php", data)
    return response == "1"

close

close() -> None

Close the HTTP client.

Source code in gdpy/client.py
def close(self) -> None:
    """Close the HTTP client."""
    if self._client:
        self._client.close()
        self._client = None

delete_friend_request

delete_friend_request(request_id: int) -> bool

Delete a friend request. Requires authentication.

Parameters:

Name Type Description Default
request_id int

The friend request ID.

required

Returns:

Type Description
bool

True if deleted successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def delete_friend_request(self, request_id: int) -> bool:
    """Delete a friend request. Requires authentication.

    Args:
        request_id: The friend request ID.

    Returns:
        True if deleted successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete friend request")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "requestID": str(request_id),
    }
    response = self._request("deleteGJFriendRequests20.php", data)
    return response == "1"

delete_level

delete_level(level_id: int) -> bool

Delete a level. Requires authentication.

Parameters:

Name Type Description Default
level_id int

The level ID to delete.

required

Returns:

Type Description
bool

True if deleted successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def delete_level(self, level_id: int) -> bool:
    """Delete a level. Requires authentication.

    Args:
        level_id: The level ID to delete.

    Returns:
        True if deleted successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete level")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "levelID": str(level_id),
        "secret": "Wmfv2898gc9",
    }
    response = self._request("deleteGJLevelUser20.php", data)
    return response == "1"

delete_level_comment

delete_level_comment(
    comment_id: int, level_id: int
) -> bool

Delete a level comment. Requires authentication.

Parameters:

Name Type Description Default
comment_id int

The comment ID to delete.

required
level_id int

The level ID the comment is on.

required

Returns:

Type Description
bool

True if deleted successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def delete_level_comment(self, comment_id: int, level_id: int) -> bool:
    """Delete a level comment. Requires authentication.

    Args:
        comment_id: The comment ID to delete.
        level_id: The level ID the comment is on.

    Returns:
        True if deleted successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete comment")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "commentID": str(comment_id),
        "levelID": str(level_id),
    }
    response = self._request("deleteGJComment20.php", data)
    return response == "1"

delete_message

delete_message(message_id: int) -> bool

Delete a message. Requires authentication.

Parameters:

Name Type Description Default
message_id int

The message ID to delete.

required

Returns:

Type Description
bool

True if deleted successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def delete_message(self, message_id: int) -> bool:
    """Delete a message. Requires authentication.

    Args:
        message_id: The message ID to delete.

    Returns:
        True if deleted successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete message")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "messageID": str(message_id),
    }
    response = self._request("deleteGJMessages20.php", data)
    return response == "1"

delete_profile_comment

delete_profile_comment(comment_id: int) -> bool

Delete a profile comment. Requires authentication.

Parameters:

Name Type Description Default
comment_id int

The comment ID to delete.

required

Returns:

Type Description
bool

True if deleted successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def delete_profile_comment(self, comment_id: int) -> bool:
    """Delete a profile comment. Requires authentication.

    Args:
        comment_id: The comment ID to delete.

    Returns:
        True if deleted successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete profile comment")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "commentID": str(comment_id),
    }
    response = self._request("deleteGJAccComment20.php", data)
    return response == "1"

get_account_comments

get_account_comments(
    account_id: int, limit: int = 10, page: int = 0
) -> list[Comment]

Get comments on a user's profile.

Parameters:

Name Type Description Default
account_id int

The account ID of the user.

required
limit int

Maximum number of comments to return.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[Comment]

List of Comment objects.

Source code in gdpy/client.py
def get_account_comments(
    self, account_id: int, limit: int = 10, page: int = 0
) -> list[Comment]:
    """Get comments on a user's profile.

    Args:
        account_id: The account ID of the user.
        limit: Maximum number of comments to return.
        page: Page number for pagination.

    Returns:
        List of Comment objects.
    """
    data = {
        "accountID": str(account_id),
        "total": str(limit),
        "page": str(page),
    }
    response = self._request("getGJAccountComments20.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    comments_data = parse_list_response(parts[0])
    return [Comment.model_validate(c) for c in comments_data]

get_blocked_users

get_blocked_users() -> list[User]

Get blocked users list. Requires authentication.

Returns:

Type Description
list[User]

List of User objects.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def get_blocked_users(self) -> list[User]:
    """Get blocked users list. Requires authentication.

    Returns:
        List of User objects.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get blocked users")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "type": "1",
    }
    response = self._request("getGJUserList20.php", data)
    if response.startswith("-"):
        return []
    users_data = parse_list_response(response)
    return [User.model_validate(u) for u in users_data]

get_challenges

get_challenges() -> DailyChallenges

Get daily challenges/quests. Requires authentication.

Source code in gdpy/client.py
def get_challenges(self) -> DailyChallenges:
    """Get daily challenges/quests. Requires authentication."""

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get challenges")
    udid = self._generate_udid()
    data = {
        "udid": udid,
        "secret": Secrets.COMMON,
        "chk": generate_rewards_chk("19847"),
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
    }
    response = self._request("getGJChallenges.php", data)
    if response.startswith("-") or "|" not in response:
        return DailyChallenges()
    return self._parse_challenges(response)

get_chest_rewards

get_chest_rewards(reward_type: int = 0) -> ChestInfo

Get chest reward information. Requires authentication.

Parameters:

Name Type Description Default
reward_type int

0 for info, 1 for small chest, 2 for large chest.

0
Source code in gdpy/client.py
def get_chest_rewards(self, reward_type: int = 0) -> ChestInfo:
    """Get chest reward information. Requires authentication.

    Args:
        reward_type: 0 for info, 1 for small chest, 2 for large chest.
    """
    import random

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get chest rewards")
    udid = self._generate_udid()
    data = {
        "udid": udid,
        "secret": Secrets.COMMON,
        "chk": generate_rewards_chk("59182"),
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "rewardType": str(reward_type),
        "r1": str(random.randint(100, 99999)),
        "r2": str(random.randint(100, 99999)),
    }
    response = self._request("getGJRewards.php", data)
    if response.startswith("-") or "|" not in response:
        return ChestInfo()
    return self._parse_chest_info(response)

get_comment_history

get_comment_history(
    user_id: int,
    limit: int = 10,
    page: int = 0,
    mode: str = "recent",
) -> list[Comment]

Get a user's comment history (comments they've posted on levels).

Parameters:

Name Type Description Default
user_id int

The user's ID (not account ID).

required
limit int

Maximum number of comments to return.

10
page int

Page number for pagination.

0
mode str

"recent" or "liked".

'recent'

Returns:

Type Description
list[Comment]

List of Comment objects.

Source code in gdpy/client.py
def get_comment_history(
    self, user_id: int, limit: int = 10, page: int = 0, mode: str = "recent"
) -> list[Comment]:
    """Get a user's comment history (comments they've posted on levels).

    Args:
        user_id: The user's ID (not account ID).
        limit: Maximum number of comments to return.
        page: Page number for pagination.
        mode: "recent" or "liked".

    Returns:
        List of Comment objects.
    """
    mode_value = "1" if mode == "liked" else "0"
    data = {
        "userID": str(user_id),
        "total": str(limit),
        "page": str(page),
        "mode": mode_value,
    }
    response = self._request("getGJCommentHistory.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    comments = []
    for comment_str in parts[0].split("|"):
        if not comment_str:
            continue
        comment_parts = comment_str.split(":")
        comment_data = parse_response(comment_parts[0])
        if len(comment_parts) > 1:
            user_data = parse_response(comment_parts[1])
            comment_data["username"] = user_data.get("1", "")
        comments.append(Comment.model_validate(comment_data))
    return comments

get_daily_level

get_daily_level(type: str = 'daily') -> DailyLevel

Get the current daily/weekly level info.

Parameters:

Name Type Description Default
type str

"daily", "weekly", or "event".

'daily'

Returns:

Type Description
DailyLevel

DailyLevel object with index and time_left.

Source code in gdpy/client.py
def get_daily_level(self, type: str = "daily") -> DailyLevel:
    """Get the current daily/weekly level info.

    Args:
        type: "daily", "weekly", or "event".

    Returns:
        DailyLevel object with index and time_left.
    """
    type_map = {"daily": "0", "weekly": "1", "event": "2"}
    data = {"type": type_map.get(type, "0")}
    response = self._request("getGJDailyLevel.php", data)
    if response.startswith("-"):
        return DailyLevel()
    parts = response.split("|")
    if len(parts) >= 2:
        return DailyLevel(index=int(parts[0]), time_left=int(parts[1]))
    return DailyLevel()

get_friend_requests

get_friend_requests(
    limit: int = 10, page: int = 0, sent: bool = False
) -> list[FriendRequest]

Get friend requests. Requires authentication.

Parameters:

Name Type Description Default
limit int

Maximum number of requests to return.

10
page int

Page number for pagination.

0
sent bool

If True, get sent requests instead of received.

False

Returns:

Type Description
list[FriendRequest]

List of FriendRequest objects.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def get_friend_requests(
    self, limit: int = 10, page: int = 0, sent: bool = False
) -> list[FriendRequest]:
    """Get friend requests. Requires authentication.

    Args:
        limit: Maximum number of requests to return.
        page: Page number for pagination.
        sent: If True, get sent requests instead of received.

    Returns:
        List of FriendRequest objects.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get friend requests")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "page": str(page),
        "total": str(limit),
        "getSent": "1" if sent else "0",
    }
    response = self._request("getGJFriendRequests20.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    requests_data = parse_list_response(parts[0])
    return [FriendRequest.model_validate(r) for r in requests_data]

get_friends

get_friends() -> list[User]

Get friends list. Requires authentication.

Returns:

Type Description
list[User]

List of User objects.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def get_friends(self) -> list[User]:
    """Get friends list. Requires authentication.

    Returns:
        List of User objects.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get friends")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "type": "0",
    }
    response = self._request("getGJUserList20.php", data)
    if response.startswith("-"):
        return []
    users_data = parse_list_response(response)
    return [User.model_validate(u) for u in users_data]

get_gauntlets

get_gauntlets() -> list[Gauntlet]

Get all gauntlets.

Returns:

Type Description
list[Gauntlet]

List of Gauntlet objects.

Source code in gdpy/client.py
def get_gauntlets(self) -> list[Gauntlet]:
    """Get all gauntlets.

    Returns:
        List of Gauntlet objects.
    """
    data = {"special": "1"}
    response = self._request("getGJGauntlets21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    gauntlets_data = parse_list_response(parts[0])
    return [Gauntlet.model_validate(g) for g in gauntlets_data]

get_leaderboard

get_leaderboard(
    limit: int = 100, type: str = "top"
) -> list[LeaderboardScore]

Get the leaderboard.

Parameters:

Name Type Description Default
limit int

Maximum number of results (max 100).

100
type str

Leaderboard type - "top" or "creators".

'top'

Returns:

Type Description
list[LeaderboardScore]

List of LeaderboardScore objects.

Source code in gdpy/client.py
def get_leaderboard(self, limit: int = 100, type: str = "top") -> list[LeaderboardScore]:
    """Get the leaderboard.

    Args:
        limit: Maximum number of results (max 100).
        type: Leaderboard type - "top" or "creators".

    Returns:
        List of LeaderboardScore objects.
    """
    type_value = "creators" if type == "creators" else "top"
    data = {"type": type_value, "count": str(min(limit, 100))}
    response = self._request("getGJScores20.php", data)
    if response.startswith("-"):
        return []
    entries_data = parse_list_response(response)
    return [LeaderboardScore.model_validate(e) for e in entries_data]

get_level

get_level(level_id: int) -> Level

Get a level by its ID.

Parameters:

Name Type Description Default
level_id int

The ID of the level.

required

Returns:

Type Description
Level

Level object with level information.

Raises:

Type Description
NotFoundError

If level is not found.

InvalidRequestError

If rate limited.

Source code in gdpy/client.py
def get_level(self, level_id: int) -> Level:
    """Get a level by its ID.

    Args:
        level_id: The ID of the level.

    Returns:
        Level object with level information.

    Raises:
        NotFoundError: If level is not found.
        InvalidRequestError: If rate limited.
    """
    data = {"levelID": str(level_id)}
    response = self._request("downloadGJLevel22.php", data)
    if response.startswith("-"):
        self._handle_error(response)
    parsed = parse_response(response.split("#")[0])
    return Level.model_validate(parsed)

get_level_comments

get_level_comments(
    level_id: int, limit: int = 10, page: int = 0
) -> list[Comment]

Get comments for a level.

Parameters:

Name Type Description Default
level_id int

The ID of the level.

required
limit int

Maximum number of comments to return.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[Comment]

List of Comment objects.

Source code in gdpy/client.py
def get_level_comments(self, level_id: int, limit: int = 10, page: int = 0) -> list[Comment]:
    """Get comments for a level.

    Args:
        level_id: The ID of the level.
        limit: Maximum number of comments to return.
        page: Page number for pagination.

    Returns:
        List of Comment objects.
    """
    data = {
        "levelID": str(level_id),
        "total": str(limit),
        "page": str(page),
        "mode": "0",
    }
    response = self._request("getGJComments21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    comments = []
    for comment_str in parts[0].split("|"):
        if not comment_str:
            continue
        comment_parts = comment_str.split(":")
        comment_data = parse_response(comment_parts[0])
        if len(comment_parts) > 1:
            user_data = parse_response(comment_parts[1])
            comment_data["username"] = user_data.get("1", "")
        comments.append(Comment.model_validate(comment_data))
    return comments

get_level_lists

get_level_lists(
    limit: int = 10, page: int = 0
) -> list[dict[str, str]]

Get level lists (user-created lists of levels).

Parameters:

Name Type Description Default
limit int

Maximum number of lists to return.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[dict[str, str]]

List of level list dictionaries.

Source code in gdpy/client.py
def get_level_lists(self, limit: int = 10, page: int = 0) -> list[dict[str, str]]:
    """Get level lists (user-created lists of levels).

    Args:
        limit: Maximum number of lists to return.
        page: Page number for pagination.

    Returns:
        List of level list dictionaries.
    """
    data = {"type": "0", "str": "", "total": str(limit), "page": str(page)}
    response = self._request("getGJLevelLists.php", data)
    if response.startswith("-"):
        return []
    return parse_list_response(response)

get_map_packs

get_map_packs(page: int = 0) -> list[MapPack]

Get map packs.

Parameters:

Name Type Description Default
page int

Page number for pagination.

0

Returns:

Type Description
list[MapPack]

List of MapPack objects.

Source code in gdpy/client.py
def get_map_packs(self, page: int = 0) -> list[MapPack]:
    """Get map packs.

    Args:
        page: Page number for pagination.

    Returns:
        List of MapPack objects.
    """
    data = {"page": str(page)}
    response = self._request("getGJMapPacks21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    packs_data = parse_list_response(parts[0])
    return [MapPack.model_validate(p) for p in packs_data]

get_message

get_message(message_id: int) -> Message

Download full message content. Requires authentication.

Parameters:

Name Type Description Default
message_id int

The message ID to download.

required

Returns:

Type Description
Message

Message object with full content.

Raises:

Type Description
RuntimeError

If not authenticated.

NotFoundError

If message not found.

Source code in gdpy/client.py
def get_message(self, message_id: int) -> Message:
    """Download full message content. Requires authentication.

    Args:
        message_id: The message ID to download.

    Returns:
        Message object with full content.

    Raises:
        RuntimeError: If not authenticated.
        NotFoundError: If message not found.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get message")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "messageID": str(message_id),
    }
    response = self._request("downloadGJMessage20.php", data)
    if response.startswith("-"):
        self._handle_error(response)
    parsed = parse_response(response)
    return Message.model_validate(parsed)

get_messages

get_messages(
    limit: int = 10, page: int = 0, sent: bool = False
) -> list[Message]

Get inbox or sent messages. Requires authentication.

Parameters:

Name Type Description Default
limit int

Maximum number of messages to return.

10
page int

Page number for pagination.

0
sent bool

If True, get sent messages instead of inbox.

False

Returns:

Type Description
list[Message]

List of Message objects.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def get_messages(self, limit: int = 10, page: int = 0, sent: bool = False) -> list[Message]:
    """Get inbox or sent messages. Requires authentication.

    Args:
        limit: Maximum number of messages to return.
        page: Page number for pagination.
        sent: If True, get sent messages instead of inbox.

    Returns:
        List of Message objects.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get messages")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "page": str(page),
        "total": str(limit),
        "getSent": "1" if sent else "0",
    }
    response = self._request("getGJMessages20.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    messages_data = parse_list_response(parts[0])
    return [Message.model_validate(m) for m in messages_data]

get_song

get_song(song_id: int) -> Song

Get a custom song by its ID.

Parameters:

Name Type Description Default
song_id int

The ID of the song.

required

Returns:

Type Description
Song

Song object with song information.

Raises:

Type Description
NotFoundError

If song is not found.

InvalidRequestError

If rate limited.

Source code in gdpy/client.py
def get_song(self, song_id: int) -> Song:
    """Get a custom song by its ID.

    Args:
        song_id: The ID of the song.

    Returns:
        Song object with song information.

    Raises:
        NotFoundError: If song is not found.
        InvalidRequestError: If rate limited.
    """
    data = {"songID": str(song_id)}
    response = self._request("getGJSongInfo.php", data)
    if response.startswith("-"):
        self._handle_error(response)
    parsed = parse_response(response)
    return Song.model_validate(parsed)

get_top_1000

get_top_1000(limit: int = 100, page: int = 0) -> list[User]

Get top 1000 users by stars.

Parameters:

Name Type Description Default
limit int

Maximum number of users to return.

100
page int

Page number for pagination.

0

Returns:

Type Description
list[User]

List of User objects.

Source code in gdpy/client.py
def get_top_1000(self, limit: int = 100, page: int = 0) -> list[User]:
    """Get top 1000 users by stars.

    Args:
        limit: Maximum number of users to return.
        page: Page number for pagination.

    Returns:
        List of User objects.
    """
    data = {"type": "top", "count": str(limit), "page": str(page)}
    response = self._request("getTop1000.php", data)
    if response.startswith("-"):
        return []
    users_data = parse_list_response(response)
    return [User.model_validate(u) for u in users_data]

get_top_artists

get_top_artists(page: int = 0) -> list[TopArtist]

Get RobTop's handpicked top artists.

Parameters:

Name Type Description Default
page int

Page number for pagination.

0

Returns:

Type Description
list[TopArtist]

List of TopArtist objects.

Source code in gdpy/client.py
def get_top_artists(self, page: int = 0) -> list[TopArtist]:
    """Get RobTop's handpicked top artists.

    Args:
        page: Page number for pagination.

    Returns:
        List of TopArtist objects.
    """
    data = {"page": str(page)}
    response = self._request("getGJTopArtists.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    artists_data = parse_list_response(parts[0])
    return [TopArtist.model_validate(a) for a in artists_data]

get_user

get_user(account_id: int) -> User

Get a user by their account ID.

Parameters:

Name Type Description Default
account_id int

The account ID of the user.

required

Returns:

Type Description
User

User object with profile information.

Raises:

Type Description
NotFoundError

If user is not found.

InvalidRequestError

If rate limited.

Source code in gdpy/client.py
def get_user(self, account_id: int) -> User:
    """Get a user by their account ID.

    Args:
        account_id: The account ID of the user.

    Returns:
        User object with profile information.

    Raises:
        NotFoundError: If user is not found.
        InvalidRequestError: If rate limited.
    """
    data = {"targetAccountID": str(account_id)}
    response = self._request("getGJUserInfo20.php", data)
    if response.startswith("-"):
        self._handle_error(response)
    parsed = parse_response(response)
    return User.model_validate(parsed)

get_user_levels

get_user_levels(
    user_id: int, limit: int = 10, page: int = 0
) -> list[Level]

Get levels created by a user.

Parameters:

Name Type Description Default
user_id int

The user's ID (not account ID).

required
limit int

Maximum number of levels to return.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[Level]

List of Level objects created by the user.

Source code in gdpy/client.py
def get_user_levels(self, user_id: int, limit: int = 10, page: int = 0) -> list[Level]:
    """Get levels created by a user.

    Args:
        user_id: The user's ID (not account ID).
        limit: Maximum number of levels to return.
        page: Page number for pagination.

    Returns:
        List of Level objects created by the user.
    """
    data = {
        "str": str(user_id),
        "total": str(limit),
        "page": str(page),
        "type": "5",
    }
    response = self._request("getGJLevels21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    levels_data = parse_list_response(parts[0])
    return [Level.model_validate(level) for level in levels_data]

like_comment

like_comment(comment_id: int, like: bool = True) -> bool

Like or dislike a level comment.

Parameters:

Name Type Description Default
comment_id int

The comment ID.

required
like bool

True to like, False to dislike.

True

Returns:

Type Description
bool

True if successful.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def like_comment(self, comment_id: int, like: bool = True) -> bool:
    """Like or dislike a level comment.

    Args:
        comment_id: The comment ID.
        like: True to like, False to dislike.

    Returns:
        True if successful.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to like comments")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "itemID": str(comment_id),
        "type": "2",
        "like": "1" if like else "0",
    }
    response = self._request("likeGJItem211.php", data)
    return response == "1"

like_level

like_level(level_id: int, like: bool = True) -> bool

Like or dislike a level.

Parameters:

Name Type Description Default
level_id int

The level ID.

required
like bool

True to like, False to dislike.

True

Returns:

Type Description
bool

True if successful.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def like_level(self, level_id: int, like: bool = True) -> bool:
    """Like or dislike a level.

    Args:
        level_id: The level ID.
        like: True to like, False to dislike.

    Returns:
        True if successful.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to like levels")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "itemID": str(level_id),
        "type": "1",
        "like": "1" if like else "0",
    }
    response = self._request("likeGJItem211.php", data)
    return response == "1"

login

login(username: str, password: str) -> bool

Login to a Geometry Dash account.

Parameters:

Name Type Description Default
username str

Account username.

required
password str

Account password.

required

Returns:

Type Description
bool

True if login successful, False otherwise.

Raises:

Type Description
InvalidCredentialsError

If credentials are incorrect.

AccountDisabledError

If account is disabled.

InvalidRequestError

If rate limited.

Source code in gdpy/client.py
def login(self, username: str, password: str) -> bool:
    """Login to a Geometry Dash account.

    Args:
        username: Account username.
        password: Account password.

    Returns:
        True if login successful, False otherwise.

    Raises:
        InvalidCredentialsError: If credentials are incorrect.
        AccountDisabledError: If account is disabled.
        InvalidRequestError: If rate limited.
    """
    import random

    udid = "S" + str(random.randint(100000, 100000000)) + str(random.randint(100000, 100000000))
    gjp2 = generate_gjp2(password)
    data = {
        "udid": udid,
        "userName": username,
        "gjp2": gjp2,
        "secret": Secrets.ACCOUNT,
    }
    response = self._request("accounts/loginGJAccount.php", data)
    if response.startswith("-"):
        self._handle_error(response)
        return False
    parts = response.split(",")
    if len(parts) >= 2:
        self._account_id = int(parts[0])
        self._player_id = int(parts[1])
        self._username = username
        self._password = password
        return True
    return False

post_level_comment

post_level_comment(
    level_id: int, content: str, percent: int = 0
) -> int | None

Post a comment on a level. Requires authentication.

Parameters:

Name Type Description Default
level_id int

The level ID to comment on.

required
content str

The comment content.

required
percent int

Optional percentage to display.

0

Returns:

Type Description
int | None

Comment ID if successful, None otherwise.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def post_level_comment(self, level_id: int, content: str, percent: int = 0) -> int | None:
    """Post a comment on a level. Requires authentication.

    Args:
        level_id: The level ID to comment on.
        content: The comment content.
        percent: Optional percentage to display.

    Returns:
        Comment ID if successful, None otherwise.

    Raises:
        RuntimeError: If not authenticated.
    """
    import base64

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to post comment")
    encoded_content = base64.b64encode(content.encode()).decode()
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "userName": self._username or "",
        "comment": encoded_content,
        "levelID": str(level_id),
        "percent": str(percent),
    }
    response = self._request("uploadGJComment21.php", data)
    if response.startswith("-"):
        return None
    return int(response) if response.isdigit() else None

post_profile_comment

post_profile_comment(content: str) -> int | None

Post a comment on your profile. Requires authentication.

Parameters:

Name Type Description Default
content str

The comment content.

required

Returns:

Type Description
int | None

Comment ID if successful, None otherwise.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def post_profile_comment(self, content: str) -> int | None:
    """Post a comment on your profile. Requires authentication.

    Args:
        content: The comment content.

    Returns:
        Comment ID if successful, None otherwise.

    Raises:
        RuntimeError: If not authenticated.
    """
    import base64

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to post profile comment")
    encoded_content = base64.b64encode(content.encode()).decode()
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "userName": self._username or "",
        "comment": encoded_content,
    }
    response = self._request("uploadGJAccComment20.php", data)
    if response.startswith("-"):
        return None
    return int(response) if response.isdigit() else None

register

register(username: str, password: str, email: str) -> bool

Register a new Geometry Dash account.

Uses the pre-2.2 API endpoint. GD 2.2 added captcha protection via register.php web page, but registerGJAccount.php API still works.

Note: GD rejects temporary email addresses.

Parameters:

Name Type Description Default
username str

Desired username (min 3 characters).

required
password str

Desired password (min 6 characters).

required
email str

Email address (must be real, not temp email).

required

Returns:

Type Description
bool

True if registration successful, False otherwise.

Raises:

Type Description
UsernameTakenError

If username is already taken.

EmailTakenError

If email is already registered.

PasswordTooShortError

If password is too short.

UsernameTooShortError

If username is too short.

InvalidEmailError

If email is invalid or blacklisted.

Source code in gdpy/client.py
def register(self, username: str, password: str, email: str) -> bool:
    """Register a new Geometry Dash account.

    Uses the pre-2.2 API endpoint. GD 2.2 added captcha protection via
    register.php web page, but registerGJAccount.php API still works.

    Note: GD rejects temporary email addresses.

    Args:
        username: Desired username (min 3 characters).
        password: Desired password (min 6 characters).
        email: Email address (must be real, not temp email).

    Returns:
        True if registration successful, False otherwise.

    Raises:
        UsernameTakenError: If username is already taken.
        EmailTakenError: If email is already registered.
        PasswordTooShortError: If password is too short.
        UsernameTooShortError: If username is too short.
        InvalidEmailError: If email is invalid or blacklisted.
    """
    data = {
        "userName": username,
        "password": password,
        "email": email,
    }
    response = self._request("accounts/registerGJAccount.php", data, Secrets.ACCOUNT)
    if response.startswith("-"):
        self._handle_error(response)
        return False
    return response == "1"

remove_friend

remove_friend(friend_id: int) -> bool

Remove a friend. Requires authentication.

Parameters:

Name Type Description Default
friend_id int

The friend's account ID.

required

Returns:

Type Description
bool

True if removed successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def remove_friend(self, friend_id: int) -> bool:
    """Remove a friend. Requires authentication.

    Args:
        friend_id: The friend's account ID.

    Returns:
        True if removed successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to remove friend")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "targetAccountID": str(friend_id),
    }
    response = self._request("removeGJFriend20.php", data)
    return response == "1"

report_level

report_level(level_id: int) -> bool

Report a level.

Parameters:

Name Type Description Default
level_id int

The level ID to report.

required

Returns:

Type Description
bool

True if reported successfully.

Source code in gdpy/client.py
def report_level(self, level_id: int) -> bool:
    """Report a level.

    Args:
        level_id: The level ID to report.

    Returns:
        True if reported successfully.
    """
    data = {
        "levelID": str(level_id),
        "secret": Secrets.COMMON,
    }
    response = self._request("reportGJLevel.php", data)
    return response == "1"

search_levels

search_levels(
    query: str = "", limit: int = 10, page: int = 0
) -> list[Level]

Search for levels by name.

Parameters:

Name Type Description Default
query str

Search query (level name or partial match).

''
limit int

Maximum number of results per page.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[Level]

List of Level objects matching the query.

Source code in gdpy/client.py
def search_levels(self, query: str = "", limit: int = 10, page: int = 0) -> list[Level]:
    """Search for levels by name.

    Args:
        query: Search query (level name or partial match).
        limit: Maximum number of results per page.
        page: Page number for pagination.

    Returns:
        List of Level objects matching the query.
    """
    data = {"str": query, "total": str(limit), "page": str(page), "type": "0"}
    response = self._request("getGJLevels21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    levels_data = parse_list_response(parts[0])
    return [Level.model_validate(level) for level in levels_data]

search_users

search_users(query: str, limit: int = 10) -> list[User]

Search for users by name.

Parameters:

Name Type Description Default
query str

Search query (username or partial match).

required
limit int

Maximum number of results to return.

10

Returns:

Type Description
list[User]

List of User objects matching the query.

Source code in gdpy/client.py
def search_users(self, query: str, limit: int = 10) -> list[User]:
    """Search for users by name.

    Args:
        query: Search query (username or partial match).
        limit: Maximum number of results to return.

    Returns:
        List of User objects matching the query.
    """
    data = {"str": query, "total": str(limit), "page": "0"}
    response = self._request("getGJUsers20.php", data)
    if response.startswith("-"):
        return []
    user_data = parse_response(response.split("#")[0])
    return [User.model_validate(user_data)]

send_friend_request

send_friend_request(
    recipient_id: int, message: str = ""
) -> bool

Send a friend request. Requires authentication.

Parameters:

Name Type Description Default
recipient_id int

The recipient's account ID.

required
message str

Optional message with the request.

''

Returns:

Type Description
bool

True if request sent successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def send_friend_request(self, recipient_id: int, message: str = "") -> bool:
    """Send a friend request. Requires authentication.

    Args:
        recipient_id: The recipient's account ID.
        message: Optional message with the request.

    Returns:
        True if request sent successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    import base64

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to send friend request")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "toAccountID": str(recipient_id),
        "comment": base64.b64encode(message.encode()).decode() if message else "",
    }
    response = self._request("uploadFriendRequest20.php", data)
    return response != "-1"

send_message

send_message(
    recipient_id: int, subject: str, body: str
) -> bool

Send a private message. Requires authentication.

Parameters:

Name Type Description Default
recipient_id int

The recipient's account ID.

required
subject str

Message subject.

required
body str

Message body.

required

Returns:

Type Description
bool

True if message sent successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def send_message(self, recipient_id: int, subject: str, body: str) -> bool:
    """Send a private message. Requires authentication.

    Args:
        recipient_id: The recipient's account ID.
        subject: Message subject.
        body: Message body.

    Returns:
        True if message sent successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    import base64

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to send message")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "toAccountID": str(recipient_id),
        "subject": base64.b64encode(subject.encode()).decode(),
        "body": base64.b64encode(body.encode()).decode(),
    }
    response = self._request("uploadGJMessage20.php", data)
    return response != "-1"

test_song

test_song(song_id: int) -> bool

Test if a custom song is available.

Parameters:

Name Type Description Default
song_id int

The song ID to test.

required

Returns:

Type Description
bool

True if song is available, False otherwise.

Source code in gdpy/client.py
def test_song(self, song_id: int) -> bool:
    """Test if a custom song is available.

    Args:
        song_id: The song ID to test.

    Returns:
        True if song is available, False otherwise.
    """
    data = {"songID": str(song_id)}
    response = self._request("testSong.php", data)
    return not response.startswith("-")

unblock_user

unblock_user(user_id: int) -> bool

Unblock a user. Requires authentication.

Parameters:

Name Type Description Default
user_id int

The user's account ID to unblock.

required

Returns:

Type Description
bool

True if unblocked successfully.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def unblock_user(self, user_id: int) -> bool:
    """Unblock a user. Requires authentication.

    Args:
        user_id: The user's account ID to unblock.

    Returns:
        True if unblocked successfully.

    Raises:
        RuntimeError: If not authenticated.
    """
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to unblock user")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "targetAccountID": str(user_id),
    }
    response = self._request("unblockGJUser20.php", data)
    return response == "1"

upload_level

upload_level(
    name: str,
    level_string: str,
    description: str = "",
    level_id: int = 0,
    version: int = 1,
    length: int = 0,
    audio_track: int = 0,
    song_id: int = 0,
    password: int = 0,
    original: int = 0,
    two_player: bool = False,
    objects: int = 1,
    coins: int = 0,
    requested_stars: int = 0,
    unlisted: int = 0,
    ldm: bool = False,
) -> int | None

Upload a level. Requires authentication.

Parameters:

Name Type Description Default
name str

Level name.

required
level_string str

Level data (will be compressed and encoded).

required
description str

Level description.

''
level_id int

0 for new level, or existing level ID to update.

0
version int

Level version number.

1
length int

Level length (0=tiny, 4=XL, 5=platformer).

0
audio_track int

Official song number (0 if using custom song).

0
song_id int

Custom song ID (0 if using official song).

0
password int

Copy password (0=none, 1=free copy).

0
original int

Original level ID if copied.

0
two_player bool

Whether level uses two player mode.

False
objects int

Number of objects in level.

1
coins int

Number of user coins.

0
requested_stars int

Requested star rating.

0
unlisted int

0=public, 1=friends only, 2=unlisted.

0
ldm bool

Whether level has low detail mode.

False

Returns:

Type Description
int | None

The uploaded level ID, or None if failed.

Raises:

Type Description
RuntimeError

If not authenticated.

Source code in gdpy/client.py
def upload_level(
    self,
    name: str,
    level_string: str,
    description: str = "",
    level_id: int = 0,
    version: int = 1,
    length: int = 0,
    audio_track: int = 0,
    song_id: int = 0,
    password: int = 0,
    original: int = 0,
    two_player: bool = False,
    objects: int = 1,
    coins: int = 0,
    requested_stars: int = 0,
    unlisted: int = 0,
    ldm: bool = False,
) -> int | None:
    """Upload a level. Requires authentication.

    Args:
        name: Level name.
        level_string: Level data (will be compressed and encoded).
        description: Level description.
        level_id: 0 for new level, or existing level ID to update.
        version: Level version number.
        length: Level length (0=tiny, 4=XL, 5=platformer).
        audio_track: Official song number (0 if using custom song).
        song_id: Custom song ID (0 if using official song).
        password: Copy password (0=none, 1=free copy).
        original: Original level ID if copied.
        two_player: Whether level uses two player mode.
        objects: Number of objects in level.
        coins: Number of user coins.
        requested_stars: Requested star rating.
        unlisted: 0=public, 1=friends only, 2=unlisted.
        ldm: Whether level has low detail mode.

    Returns:
        The uploaded level ID, or None if failed.

    Raises:
        RuntimeError: If not authenticated.
    """
    import base64
    import gzip
    import random

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to upload level")

    # Compress and encode level string
    compressed = gzip.compress(level_string.encode())
    encoded_string = base64.urlsafe_b64encode(compressed).decode()

    # Encode description
    encoded_desc = base64.urlsafe_b64encode(description.encode()).decode()

    # Generate seed2 (chk from first 50 chars of compressed data)
    import hashlib

    seed_data = compressed[:50] if len(compressed) >= 50 else compressed
    seed2 = hashlib.sha1(seed_data + b"xI25fpAapCQg").hexdigest()

    data = {
        "gameVersion": "22",
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "userName": self._username or "",
        "levelID": str(level_id),
        "levelName": name,
        "levelDesc": encoded_desc,
        "levelVersion": str(version),
        "levelLength": str(length),
        "audioTrack": str(audio_track),
        "auto": "0",
        "password": str(password),
        "original": str(original),
        "twoPlayer": "1" if two_player else "0",
        "songID": str(song_id),
        "objects": str(objects),
        "coins": str(coins),
        "requestedStars": str(requested_stars),
        "unlisted": str(unlisted),
        "ldm": "1" if ldm else "0",
        "levelString": encoded_string,
        "seed2": seed2,
        "secret": Secrets.COMMON,
        "wt": str(random.randint(100, 99999)),
        "wt2": str(random.randint(100, 99999)),
    }
    response = self._request("uploadGJLevel21.php", data)
    if response.startswith("-"):
        return None
    return int(response) if response.isdigit() else None

AsyncClient

gdpy.client.AsyncClient

AsyncClient(base_url: str | None = None)

Asynchronous client for interacting with the Geometry Dash API.

This client provides async methods to interact with Geometry Dash's private API, including user lookups, level searches, and authentication.

Based on the Geometry Dash API documentation: https://github.com/Rifct/gd-docs

Parameters:

Name Type Description Default
base_url str | None

Optional custom base URL for the API. Defaults to boomlings.com.

None

Example:

client = AsyncClient()
user = await client.get_user(account_id=71)
print(user.username)
await client.close()

# Or use as context manager:
async with AsyncClient() as client:
    user = await client.get_user(account_id=71)
    print(user.username)

Initialize the async client.

Parameters:

Name Type Description Default
base_url str | None

Optional custom base URL for the API.

None
Source code in gdpy/client.py
def __init__(self, base_url: str | None = None) -> None:
    """Initialize the async client.

    Args:
        base_url: Optional custom base URL for the API.
    """
    self.base_url = base_url or URLs.DEFAULT
    self._client: httpx.AsyncClient | None = None
    self._account_id: int | None = None
    self._player_id: int | None = None
    self._username: str | None = None
    self._password: str | None = None

account_id property

account_id: int | None

Get the authenticated account ID.

is_authenticated property

is_authenticated: bool

Check if the client is authenticated.

username property

username: str | None

Get the authenticated username.

accept_friend_request async

accept_friend_request(request_id: int) -> bool

Accept a friend request. Requires authentication.

Source code in gdpy/client.py
async def accept_friend_request(self, request_id: int) -> bool:
    """Accept a friend request. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to accept friend request")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "requestID": str(request_id),
    }
    response = await self._request("acceptGJFriendRequest20.php", data)
    return response == "1"

block_user async

block_user(user_id: int) -> bool

Block a user. Requires authentication.

Source code in gdpy/client.py
async def block_user(self, user_id: int) -> bool:
    """Block a user. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to block user")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "targetAccountID": str(user_id),
    }
    response = await self._request("blockGJUser20.php", data)
    return response == "1"

close async

close() -> None

Close the HTTP client.

Source code in gdpy/client.py
async def close(self) -> None:
    """Close the HTTP client."""
    if self._client:
        await self._client.aclose()
        self._client = None

delete_friend_request async

delete_friend_request(request_id: int) -> bool

Delete a friend request. Requires authentication.

Source code in gdpy/client.py
async def delete_friend_request(self, request_id: int) -> bool:
    """Delete a friend request. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete friend request")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "requestID": str(request_id),
    }
    response = await self._request("deleteGJFriendRequests20.php", data)
    return response == "1"

delete_level async

delete_level(level_id: int) -> bool

Delete a level. Requires authentication.

Source code in gdpy/client.py
async def delete_level(self, level_id: int) -> bool:
    """Delete a level. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete level")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "levelID": str(level_id),
        "secret": "Wmfv2898gc9",
    }
    response = await self._request("deleteGJLevelUser20.php", data)
    return response == "1"

delete_level_comment async

delete_level_comment(
    comment_id: int, level_id: int
) -> bool

Delete a level comment. Requires authentication.

Source code in gdpy/client.py
async def delete_level_comment(self, comment_id: int, level_id: int) -> bool:
    """Delete a level comment. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete comment")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "commentID": str(comment_id),
        "levelID": str(level_id),
    }
    response = await self._request("deleteGJComment20.php", data)
    return response == "1"

delete_message async

delete_message(message_id: int) -> bool

Delete a message. Requires authentication.

Source code in gdpy/client.py
async def delete_message(self, message_id: int) -> bool:
    """Delete a message. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete message")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "messageID": str(message_id),
    }
    response = await self._request("deleteGJMessages20.php", data)
    return response == "1"

delete_profile_comment async

delete_profile_comment(comment_id: int) -> bool

Delete a profile comment. Requires authentication.

Source code in gdpy/client.py
async def delete_profile_comment(self, comment_id: int) -> bool:
    """Delete a profile comment. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to delete profile comment")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "commentID": str(comment_id),
    }
    response = await self._request("deleteGJAccComment20.php", data)
    return response == "1"

get_account_comments async

get_account_comments(
    account_id: int, limit: int = 10, page: int = 0
) -> list[Comment]

Get comments on a user's profile.

Parameters:

Name Type Description Default
account_id int

The account ID of the user.

required
limit int

Maximum number of comments to return.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[Comment]

List of Comment objects.

Source code in gdpy/client.py
async def get_account_comments(
    self, account_id: int, limit: int = 10, page: int = 0
) -> list[Comment]:
    """Get comments on a user's profile.

    Args:
        account_id: The account ID of the user.
        limit: Maximum number of comments to return.
        page: Page number for pagination.

    Returns:
        List of Comment objects.
    """
    data = {
        "accountID": str(account_id),
        "total": str(limit),
        "page": str(page),
    }
    response = await self._request("getGJAccountComments20.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    comments_data = parse_list_response(parts[0])
    return [Comment.model_validate(c) for c in comments_data]

get_blocked_users async

get_blocked_users() -> list[User]

Get blocked users list. Requires authentication.

Source code in gdpy/client.py
async def get_blocked_users(self) -> list[User]:
    """Get blocked users list. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get blocked users")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "type": "1",
    }
    response = await self._request("getGJUserList20.php", data)
    if response.startswith("-"):
        return []
    users_data = parse_list_response(response)
    return [User.model_validate(u) for u in users_data]

get_challenges async

get_challenges() -> DailyChallenges

Get daily challenges/quests. Requires authentication.

Source code in gdpy/client.py
async def get_challenges(self) -> DailyChallenges:
    """Get daily challenges/quests. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get challenges")
    udid = self._generate_udid()
    data = {
        "udid": udid,
        "secret": Secrets.COMMON,
        "chk": generate_rewards_chk("19847"),
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
    }
    response = await self._request("getGJChallenges.php", data)
    if response.startswith("-") or "|" not in response:
        return DailyChallenges()
    return self._parse_challenges(response)

get_chest_rewards async

get_chest_rewards(reward_type: int = 0) -> ChestInfo

Get chest reward information. Requires authentication.

Parameters:

Name Type Description Default
reward_type int

0 for info, 1 for small chest, 2 for large chest.

0
Source code in gdpy/client.py
async def get_chest_rewards(self, reward_type: int = 0) -> ChestInfo:
    """Get chest reward information. Requires authentication.

    Args:
        reward_type: 0 for info, 1 for small chest, 2 for large chest.
    """
    import random

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get chest rewards")
    udid = self._generate_udid()
    data = {
        "udid": udid,
        "secret": Secrets.COMMON,
        "chk": generate_rewards_chk("59182"),
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "rewardType": str(reward_type),
        "r1": str(random.randint(100, 99999)),
        "r2": str(random.randint(100, 99999)),
    }
    response = await self._request("getGJRewards.php", data)
    if response.startswith("-") or "|" not in response:
        return ChestInfo()
    return self._parse_chest_info(response)

get_comment_history async

get_comment_history(
    user_id: int,
    limit: int = 10,
    page: int = 0,
    mode: str = "recent",
) -> list[Comment]

Get a user's comment history (comments they've posted on levels).

Parameters:

Name Type Description Default
user_id int

The user's ID (not account ID).

required
limit int

Maximum number of comments to return.

10
page int

Page number for pagination.

0
mode str

"recent" or "liked".

'recent'

Returns:

Type Description
list[Comment]

List of Comment objects.

Source code in gdpy/client.py
async def get_comment_history(
    self, user_id: int, limit: int = 10, page: int = 0, mode: str = "recent"
) -> list[Comment]:
    """Get a user's comment history (comments they've posted on levels).

    Args:
        user_id: The user's ID (not account ID).
        limit: Maximum number of comments to return.
        page: Page number for pagination.
        mode: "recent" or "liked".

    Returns:
        List of Comment objects.
    """
    mode_value = "1" if mode == "liked" else "0"
    data = {
        "userID": str(user_id),
        "total": str(limit),
        "page": str(page),
        "mode": mode_value,
    }
    response = await self._request("getGJCommentHistory.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    comments = []
    for comment_str in parts[0].split("|"):
        if not comment_str:
            continue
        comment_parts = comment_str.split(":")
        comment_data = parse_response(comment_parts[0])
        if len(comment_parts) > 1:
            user_data = parse_response(comment_parts[1])
            comment_data["username"] = user_data.get("1", "")
            comments.append(Comment.model_validate(comment_data))
    return comments

get_daily_level async

get_daily_level(type: str = 'daily') -> DailyLevel

Get the current daily/weekly level info.

Parameters:

Name Type Description Default
type str

"daily", "weekly", or "event".

'daily'

Returns:

Type Description
DailyLevel

DailyLevel object with index and time_left.

Source code in gdpy/client.py
async def get_daily_level(self, type: str = "daily") -> DailyLevel:
    """Get the current daily/weekly level info.

    Args:
        type: "daily", "weekly", or "event".

    Returns:
        DailyLevel object with index and time_left.
    """
    type_map = {"daily": "0", "weekly": "1", "event": "2"}
    data = {"type": type_map.get(type, "0")}
    response = await self._request("getGJDailyLevel.php", data)
    if response.startswith("-"):
        return DailyLevel()
    parts = response.split("|")
    if len(parts) >= 2:
        return DailyLevel(index=int(parts[0]), time_left=int(parts[1]))
    return DailyLevel()

get_friend_requests async

get_friend_requests(
    limit: int = 10, page: int = 0, sent: bool = False
) -> list[FriendRequest]

Get friend requests. Requires authentication.

Source code in gdpy/client.py
async def get_friend_requests(
    self, limit: int = 10, page: int = 0, sent: bool = False
) -> list[FriendRequest]:
    """Get friend requests. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get friend requests")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "page": str(page),
        "total": str(limit),
        "getSent": "1" if sent else "0",
    }
    response = await self._request("getGJFriendRequests20.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    requests_data = parse_list_response(parts[0])
    return [FriendRequest.model_validate(r) for r in requests_data]

get_friends async

get_friends() -> list[User]

Get friends list. Requires authentication.

Source code in gdpy/client.py
async def get_friends(self) -> list[User]:
    """Get friends list. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get friends")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "type": "0",
    }
    response = await self._request("getGJUserList20.php", data)
    if response.startswith("-"):
        return []
    users_data = parse_list_response(response)
    return [User.model_validate(u) for u in users_data]

get_gauntlets async

get_gauntlets() -> list[Gauntlet]

Get all gauntlets.

Returns:

Type Description
list[Gauntlet]

List of Gauntlet objects.

Source code in gdpy/client.py
async def get_gauntlets(self) -> list[Gauntlet]:
    """Get all gauntlets.

    Returns:
        List of Gauntlet objects.
    """
    data = {"special": "1"}
    response = await self._request("getGJGauntlets21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    gauntlets_data = parse_list_response(parts[0])
    return [Gauntlet.model_validate(g) for g in gauntlets_data]

get_leaderboard async

get_leaderboard(
    limit: int = 100, type: str = "top"
) -> list[LeaderboardScore]

Get the leaderboard.

Parameters:

Name Type Description Default
limit int

Maximum number of results (max 100).

100
type str

Leaderboard type - "top" or "creators".

'top'

Returns:

Type Description
list[LeaderboardScore]

List of LeaderboardScore objects.

Source code in gdpy/client.py
async def get_leaderboard(self, limit: int = 100, type: str = "top") -> list[LeaderboardScore]:
    """Get the leaderboard.

    Args:
        limit: Maximum number of results (max 100).
        type: Leaderboard type - "top" or "creators".

    Returns:
        List of LeaderboardScore objects.
    """
    type_value = "creators" if type == "creators" else "top"
    data = {"type": type_value, "count": str(min(limit, 100))}
    response = await self._request("getGJScores20.php", data)
    if response.startswith("-"):
        return []
    entries_data = parse_list_response(response)
    return [LeaderboardScore.model_validate(e) for e in entries_data]

get_level async

get_level(level_id: int) -> Level

Get a level by its ID.

Parameters:

Name Type Description Default
level_id int

The ID of the level.

required

Returns:

Type Description
Level

Level object with level information.

Raises:

Type Description
NotFoundError

If level is not found.

InvalidRequestError

If rate limited.

Source code in gdpy/client.py
async def get_level(self, level_id: int) -> Level:
    """Get a level by its ID.

    Args:
        level_id: The ID of the level.

    Returns:
        Level object with level information.

    Raises:
        NotFoundError: If level is not found.
        InvalidRequestError: If rate limited.
    """
    data = {"levelID": str(level_id)}
    response = await self._request("downloadGJLevel22.php", data)
    if response.startswith("-"):
        self._handle_error(response)
    parsed = parse_response(response.split("#")[0])
    return Level.model_validate(parsed)

get_level_comments async

get_level_comments(
    level_id: int, limit: int = 10, page: int = 0
) -> list[Comment]

Get comments for a level.

Parameters:

Name Type Description Default
level_id int

The ID of the level.

required
limit int

Maximum number of comments to return.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[Comment]

List of Comment objects.

Source code in gdpy/client.py
async def get_level_comments(
    self, level_id: int, limit: int = 10, page: int = 0
) -> list[Comment]:
    """Get comments for a level.

    Args:
        level_id: The ID of the level.
        limit: Maximum number of comments to return.
        page: Page number for pagination.

    Returns:
        List of Comment objects.
    """
    data = {
        "levelID": str(level_id),
        "total": str(limit),
        "page": str(page),
        "mode": "0",
    }
    response = await self._request("getGJComments21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    comments = []
    for comment_str in parts[0].split("|"):
        if not comment_str:
            continue
        comment_parts = comment_str.split(":")
        comment_data = parse_response(comment_parts[0])
        if len(comment_parts) > 1:
            user_data = parse_response(comment_parts[1])
            comment_data["username"] = user_data.get("1", "")
        comments.append(Comment.model_validate(comment_data))
    return comments

get_level_lists async

get_level_lists(
    limit: int = 10, page: int = 0
) -> list[dict[str, str]]

Get level lists (user-created lists of levels).

Parameters:

Name Type Description Default
limit int

Maximum number of lists to return.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[dict[str, str]]

List of level list dictionaries.

Source code in gdpy/client.py
async def get_level_lists(self, limit: int = 10, page: int = 0) -> list[dict[str, str]]:
    """Get level lists (user-created lists of levels).

    Args:
        limit: Maximum number of lists to return.
        page: Page number for pagination.

    Returns:
        List of level list dictionaries.
    """
    data = {"type": "0", "str": "", "total": str(limit), "page": str(page)}
    response = await self._request("getGJLevelLists.php", data)
    if response.startswith("-"):
        return []
    return parse_list_response(response)

get_map_packs async

get_map_packs(page: int = 0) -> list[MapPack]

Get map packs.

Parameters:

Name Type Description Default
page int

Page number for pagination.

0

Returns:

Type Description
list[MapPack]

List of MapPack objects.

Source code in gdpy/client.py
async def get_map_packs(self, page: int = 0) -> list[MapPack]:
    """Get map packs.

    Args:
        page: Page number for pagination.

    Returns:
        List of MapPack objects.
    """
    data = {"page": str(page)}
    response = await self._request("getGJMapPacks21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    packs_data = parse_list_response(parts[0])
    return [MapPack.model_validate(p) for p in packs_data]

get_message async

get_message(message_id: int) -> Message

Download full message content. Requires authentication.

Source code in gdpy/client.py
async def get_message(self, message_id: int) -> Message:
    """Download full message content. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get message")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "messageID": str(message_id),
    }
    response = await self._request("downloadGJMessage20.php", data)
    if response.startswith("-"):
        self._handle_error(response)
    parsed = parse_response(response)
    return Message.model_validate(parsed)

get_messages async

get_messages(
    limit: int = 10, page: int = 0, sent: bool = False
) -> list[Message]

Get inbox or sent messages. Requires authentication.

Source code in gdpy/client.py
async def get_messages(
    self, limit: int = 10, page: int = 0, sent: bool = False
) -> list[Message]:
    """Get inbox or sent messages. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to get messages")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "page": str(page),
        "total": str(limit),
        "getSent": "1" if sent else "0",
    }
    response = await self._request("getGJMessages20.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    messages_data = parse_list_response(parts[0])
    return [Message.model_validate(m) for m in messages_data]

get_song async

get_song(song_id: int) -> Song

Get a custom song by its ID.

Parameters:

Name Type Description Default
song_id int

The ID of the song.

required

Returns:

Type Description
Song

Song object with song information.

Raises:

Type Description
NotFoundError

If song is not found.

InvalidRequestError

If rate limited.

Source code in gdpy/client.py
async def get_song(self, song_id: int) -> Song:
    """Get a custom song by its ID.

    Args:
        song_id: The ID of the song.

    Returns:
        Song object with song information.

    Raises:
        NotFoundError: If song is not found.
        InvalidRequestError: If rate limited.
    """
    data = {"songID": str(song_id)}
    response = await self._request("getGJSongInfo.php", data)
    if response.startswith("-"):
        self._handle_error(response)
    parsed = parse_response(response)
    return Song.model_validate(parsed)

get_top_1000 async

get_top_1000(limit: int = 100, page: int = 0) -> list[User]

Get top 1000 users by stars.

Parameters:

Name Type Description Default
limit int

Maximum number of users to return.

100
page int

Page number for pagination.

0

Returns:

Type Description
list[User]

List of User objects.

Source code in gdpy/client.py
async def get_top_1000(self, limit: int = 100, page: int = 0) -> list[User]:
    """Get top 1000 users by stars.

    Args:
        limit: Maximum number of users to return.
        page: Page number for pagination.

    Returns:
        List of User objects.
    """
    data = {"type": "top", "count": str(limit), "page": str(page)}
    response = await self._request("getTop1000.php", data)
    if response.startswith("-"):
        return []
    users_data = parse_list_response(response)
    return [User.model_validate(u) for u in users_data]

get_top_artists async

get_top_artists(page: int = 0) -> list[TopArtist]

Get RobTop's handpicked top artists.

Parameters:

Name Type Description Default
page int

Page number for pagination.

0

Returns:

Type Description
list[TopArtist]

List of TopArtist objects.

Source code in gdpy/client.py
async def get_top_artists(self, page: int = 0) -> list[TopArtist]:
    """Get RobTop's handpicked top artists.

    Args:
        page: Page number for pagination.

    Returns:
        List of TopArtist objects.
    """
    data = {"page": str(page)}
    response = await self._request("getGJTopArtists.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    artists_data = parse_list_response(parts[0])
    return [TopArtist.model_validate(a) for a in artists_data]

get_user async

get_user(account_id: int) -> User

Get a user by their account ID.

Parameters:

Name Type Description Default
account_id int

The account ID of the user.

required

Returns:

Type Description
User

User object with profile information.

Raises:

Type Description
NotFoundError

If user is not found.

InvalidRequestError

If rate limited.

Source code in gdpy/client.py
async def get_user(self, account_id: int) -> User:
    """Get a user by their account ID.

    Args:
        account_id: The account ID of the user.

    Returns:
        User object with profile information.

    Raises:
        NotFoundError: If user is not found.
        InvalidRequestError: If rate limited.
    """
    data = {"targetAccountID": str(account_id)}
    response = await self._request("getGJUserInfo20.php", data)
    if response.startswith("-"):
        self._handle_error(response)
    parsed = parse_response(response)
    return User.model_validate(parsed)

get_user_levels async

get_user_levels(
    user_id: int, limit: int = 10, page: int = 0
) -> list[Level]

Get levels created by a user.

Parameters:

Name Type Description Default
user_id int

The user's ID (not account ID).

required
limit int

Maximum number of levels to return.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[Level]

List of Level objects created by the user.

Source code in gdpy/client.py
async def get_user_levels(self, user_id: int, limit: int = 10, page: int = 0) -> list[Level]:
    """Get levels created by a user.

    Args:
        user_id: The user's ID (not account ID).
        limit: Maximum number of levels to return.
        page: Page number for pagination.

    Returns:
        List of Level objects created by the user.
    """
    data = {
        "str": str(user_id),
        "total": str(limit),
        "page": str(page),
        "type": "5",
    }
    response = await self._request("getGJLevels21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    levels_data = parse_list_response(parts[0])
    return [Level.model_validate(level) for level in levels_data]

like_comment async

like_comment(comment_id: int, like: bool = True) -> bool

Like or dislike a level comment.

Source code in gdpy/client.py
async def like_comment(self, comment_id: int, like: bool = True) -> bool:
    """Like or dislike a level comment."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to like comments")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "itemID": str(comment_id),
        "type": "2",
        "like": "1" if like else "0",
    }
    response = await self._request("likeGJItem211.php", data)
    return response == "1"

like_level async

like_level(level_id: int, like: bool = True) -> bool

Like or dislike a level.

Source code in gdpy/client.py
async def like_level(self, level_id: int, like: bool = True) -> bool:
    """Like or dislike a level."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to like levels")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "itemID": str(level_id),
        "type": "1",
        "like": "1" if like else "0",
    }
    response = await self._request("likeGJItem211.php", data)
    return response == "1"

login async

login(username: str, password: str) -> bool

Login to a Geometry Dash account.

Parameters:

Name Type Description Default
username str

Account username.

required
password str

Account password.

required

Returns:

Type Description
bool

True if login successful, False otherwise.

Raises:

Type Description
InvalidCredentialsError

If credentials are incorrect.

AccountDisabledError

If account is disabled.

InvalidRequestError

If rate limited.

Source code in gdpy/client.py
async def login(self, username: str, password: str) -> bool:
    """Login to a Geometry Dash account.

    Args:
        username: Account username.
        password: Account password.

    Returns:
        True if login successful, False otherwise.

    Raises:
        InvalidCredentialsError: If credentials are incorrect.
        AccountDisabledError: If account is disabled.
        InvalidRequestError: If rate limited.
    """
    import random

    udid = "S" + str(random.randint(100000, 100000000)) + str(random.randint(100000, 100000000))
    gjp2 = generate_gjp2(password)
    data = {
        "udid": udid,
        "userName": username,
        "gjp2": gjp2,
        "secret": Secrets.ACCOUNT,
    }
    response = await self._request("accounts/loginGJAccount.php", data)
    if response.startswith("-"):
        self._handle_error(response)
        return False
    parts = response.split(",")
    if len(parts) >= 2:
        self._account_id = int(parts[0])
        self._player_id = int(parts[1])
        self._username = username
        self._password = password
        return True
    return False

post_level_comment async

post_level_comment(
    level_id: int, content: str, percent: int = 0
) -> int | None

Post a comment on a level. Requires authentication.

Source code in gdpy/client.py
async def post_level_comment(self, level_id: int, content: str, percent: int = 0) -> int | None:
    """Post a comment on a level. Requires authentication."""
    import base64

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to post comment")
    encoded_content = base64.b64encode(content.encode()).decode()
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "userName": self._username or "",
        "comment": encoded_content,
        "levelID": str(level_id),
        "percent": str(percent),
    }
    response = await self._request("uploadGJComment21.php", data)
    if response.startswith("-"):
        return None
    return int(response) if response.isdigit() else None

post_profile_comment async

post_profile_comment(content: str) -> int | None

Post a comment on your profile. Requires authentication.

Source code in gdpy/client.py
async def post_profile_comment(self, content: str) -> int | None:
    """Post a comment on your profile. Requires authentication."""
    import base64

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to post profile comment")
    encoded_content = base64.b64encode(content.encode()).decode()
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "userName": self._username or "",
        "comment": encoded_content,
    }
    response = await self._request("uploadGJAccComment20.php", data)
    if response.startswith("-"):
        return None
    return int(response) if response.isdigit() else None

register async

register(username: str, password: str, email: str) -> bool

Register a new Geometry Dash account.

Uses the pre-2.2 API endpoint. GD 2.2 added captcha protection via register.php web page, but registerGJAccount.php API still works.

Note: GD rejects temporary email addresses.

Parameters:

Name Type Description Default
username str

Desired username (min 3 characters).

required
password str

Desired password (min 6 characters).

required
email str

Email address (must be real, not temp email).

required

Returns:

Type Description
bool

True if registration successful, False otherwise.

Raises:

Type Description
UsernameTakenError

If username is already taken.

EmailTakenError

If email is already registered.

PasswordTooShortError

If password is too short.

UsernameTooShortError

If username is too short.

InvalidEmailError

If email is invalid or blacklisted.

Source code in gdpy/client.py
async def register(self, username: str, password: str, email: str) -> bool:
    """Register a new Geometry Dash account.

    Uses the pre-2.2 API endpoint. GD 2.2 added captcha protection via
    register.php web page, but registerGJAccount.php API still works.

    Note: GD rejects temporary email addresses.

    Args:
        username: Desired username (min 3 characters).
        password: Desired password (min 6 characters).
        email: Email address (must be real, not temp email).

    Returns:
        True if registration successful, False otherwise.

    Raises:
        UsernameTakenError: If username is already taken.
        EmailTakenError: If email is already registered.
        PasswordTooShortError: If password is too short.
        UsernameTooShortError: If username is too short.
        InvalidEmailError: If email is invalid or blacklisted.
    """
    data = {
        "userName": username,
        "password": password,
        "email": email,
    }
    response = await self._request("accounts/registerGJAccount.php", data, Secrets.ACCOUNT)
    if response.startswith("-"):
        self._handle_error(response)
        return False
    return response == "1"

remove_friend async

remove_friend(friend_id: int) -> bool

Remove a friend. Requires authentication.

Source code in gdpy/client.py
async def remove_friend(self, friend_id: int) -> bool:
    """Remove a friend. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to remove friend")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "targetAccountID": str(friend_id),
    }
    response = await self._request("removeGJFriend20.php", data)
    return response == "1"

report_level async

report_level(level_id: int) -> bool

Report a level.

Source code in gdpy/client.py
async def report_level(self, level_id: int) -> bool:
    """Report a level."""
    data = {
        "levelID": str(level_id),
        "secret": Secrets.COMMON,
    }
    response = await self._request("reportGJLevel.php", data)
    return response == "1"

search_levels async

search_levels(
    query: str = "", limit: int = 10, page: int = 0
) -> list[Level]

Search for levels by name.

Parameters:

Name Type Description Default
query str

Search query (level name or partial match).

''
limit int

Maximum number of results per page.

10
page int

Page number for pagination.

0

Returns:

Type Description
list[Level]

List of Level objects matching the query.

Source code in gdpy/client.py
async def search_levels(self, query: str = "", limit: int = 10, page: int = 0) -> list[Level]:
    """Search for levels by name.

    Args:
        query: Search query (level name or partial match).
        limit: Maximum number of results per page.
        page: Page number for pagination.

    Returns:
        List of Level objects matching the query.
    """
    data = {"str": query, "total": str(limit), "page": str(page), "type": "0"}
    response = await self._request("getGJLevels21.php", data)
    if response.startswith("-"):
        return []
    parts = response.split("#")
    if not parts:
        return []
    levels_data = parse_list_response(parts[0])
    return [Level.model_validate(level) for level in levels_data]

search_users async

search_users(query: str, limit: int = 10) -> list[User]

Search for users by name.

Parameters:

Name Type Description Default
query str

Search query (username or partial match).

required
limit int

Maximum number of results to return.

10

Returns:

Type Description
list[User]

List of User objects matching the query.

Source code in gdpy/client.py
async def search_users(self, query: str, limit: int = 10) -> list[User]:
    """Search for users by name.

    Args:
        query: Search query (username or partial match).
        limit: Maximum number of results to return.

    Returns:
        List of User objects matching the query.
    """
    data = {"str": query, "total": str(limit), "page": "0"}
    response = await self._request("getGJUsers20.php", data)
    if response.startswith("-"):
        return []
    user_data = parse_response(response.split("#")[0])
    return [User.model_validate(user_data)]

send_friend_request async

send_friend_request(
    recipient_id: int, message: str = ""
) -> bool

Send a friend request. Requires authentication.

Source code in gdpy/client.py
async def send_friend_request(self, recipient_id: int, message: str = "") -> bool:
    """Send a friend request. Requires authentication."""
    import base64

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to send friend request")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "toAccountID": str(recipient_id),
        "comment": base64.b64encode(message.encode()).decode() if message else "",
    }
    response = await self._request("uploadFriendRequest20.php", data)
    return response != "-1"

send_message async

send_message(
    recipient_id: int, subject: str, body: str
) -> bool

Send a private message. Requires authentication.

Source code in gdpy/client.py
async def send_message(self, recipient_id: int, subject: str, body: str) -> bool:
    """Send a private message. Requires authentication."""
    import base64

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to send message")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "toAccountID": str(recipient_id),
        "subject": base64.b64encode(subject.encode()).decode(),
        "body": base64.b64encode(body.encode()).decode(),
    }
    response = await self._request("uploadGJMessage20.php", data)
    return response != "-1"

test_song async

test_song(song_id: int) -> bool

Test if a custom song is available.

Parameters:

Name Type Description Default
song_id int

The song ID to test.

required

Returns:

Type Description
bool

True if song is available, False otherwise.

Source code in gdpy/client.py
async def test_song(self, song_id: int) -> bool:
    """Test if a custom song is available.

    Args:
        song_id: The song ID to test.

    Returns:
        True if song is available, False otherwise.
    """
    data = {"songID": str(song_id)}
    response = await self._request("testSong.php", data)
    return not response.startswith("-")

unblock_user async

unblock_user(user_id: int) -> bool

Unblock a user. Requires authentication.

Source code in gdpy/client.py
async def unblock_user(self, user_id: int) -> bool:
    """Unblock a user. Requires authentication."""
    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to unblock user")
    data = {
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "targetAccountID": str(user_id),
    }
    response = await self._request("unblockGJUser20.php", data)
    return response == "1"

upload_level async

upload_level(
    name: str,
    level_string: str,
    description: str = "",
    level_id: int = 0,
    version: int = 1,
    length: int = 0,
    audio_track: int = 0,
    song_id: int = 0,
    password: int = 0,
    original: int = 0,
    two_player: bool = False,
    objects: int = 1,
    coins: int = 0,
    requested_stars: int = 0,
    unlisted: int = 0,
    ldm: bool = False,
) -> int | None

Upload a level. Requires authentication.

Source code in gdpy/client.py
async def upload_level(
    self,
    name: str,
    level_string: str,
    description: str = "",
    level_id: int = 0,
    version: int = 1,
    length: int = 0,
    audio_track: int = 0,
    song_id: int = 0,
    password: int = 0,
    original: int = 0,
    two_player: bool = False,
    objects: int = 1,
    coins: int = 0,
    requested_stars: int = 0,
    unlisted: int = 0,
    ldm: bool = False,
) -> int | None:
    """Upload a level. Requires authentication."""
    import base64
    import gzip
    import hashlib
    import random

    if not self.is_authenticated:
        raise RuntimeError("Must be authenticated to upload level")

    compressed = gzip.compress(level_string.encode())
    encoded_string = base64.urlsafe_b64encode(compressed).decode()
    encoded_desc = base64.urlsafe_b64encode(description.encode()).decode()

    seed_data = compressed[:50] if len(compressed) >= 50 else compressed
    seed2 = hashlib.sha1(seed_data + b"xI25fpAapCQg").hexdigest()

    data = {
        "gameVersion": "22",
        "accountID": str(self._account_id),
        "gjp2": self._get_gjp2(),
        "userName": self._username or "",
        "levelID": str(level_id),
        "levelName": name,
        "levelDesc": encoded_desc,
        "levelVersion": str(version),
        "levelLength": str(length),
        "audioTrack": str(audio_track),
        "auto": "0",
        "password": str(password),
        "original": str(original),
        "twoPlayer": "1" if two_player else "0",
        "songID": str(song_id),
        "objects": str(objects),
        "coins": str(coins),
        "requestedStars": str(requested_stars),
        "unlisted": str(unlisted),
        "ldm": "1" if ldm else "0",
        "levelString": encoded_string,
        "seed2": seed2,
        "secret": Secrets.COMMON,
        "wt": str(random.randint(100, 99999)),
        "wt2": str(random.randint(100, 99999)),
    }
    response = await self._request("uploadGJLevel21.php", data)
    if response.startswith("-"):
        return None
    return int(response) if response.isdigit() else None