Skip to main content

Async & sync Python wrapper for the Clash of Clans API with auto key management

Project description

CI Status Pypi version PyPI Downloads

Python version License Ruff

cocapi

A Python wrapper for the official Clash of Clans API with full async support, automatic key management, caching, retries, and optional Pydantic models.

Install

pip install cocapi

# With Pydantic model support
pip install 'cocapi[pydantic]'

Requires Python 3.10+.

Quick Start

With an API Token

Get a token from developer.clashofclans.com:

from cocapi import CocApi

api = CocApi("your_api_token")

clan = api.clan_tag("#2PP")
print(clan["name"])

player = api.players("#900PUCPV")
print(player["trophies"])

With Email/Password (Automatic Key Management)

Skip manual key creation entirely. cocapi logs into the developer portal, detects your IP, and manages keys for you:

from cocapi import CocApi

api = CocApi.from_credentials("you@example.com", "your_password")
clan = api.clan_tag("#2PP")

Keys are created automatically, reused when valid, and rotated when your IP changes. See Authentication for details.

Async

import asyncio
from cocapi import CocApi

async def main():
    async with CocApi("your_token") as api:
        clan = await api.clan_tag("#2PP")
        player = await api.players("#900PUCPV")
        print(clan["name"], player["trophies"])

asyncio.run(main())

Authentication

cocapi supports two ways to authenticate:

1. API Token

Pass a token directly. You manage key creation and IP binding yourself at the developer portal.

api = CocApi("your_token")

2. Developer Portal Credentials

Provide your email and password. cocapi handles everything:

api = CocApi.from_credentials("email", "password")

What happens automatically:

  • Logs into the developer portal and detects your public IP
  • Creates API keys bound to that IP (or reuses existing ones)
  • If your IP changes mid-session and the API returns 403, revokes the old key and creates a new one
  • Respects the 10-key-per-account SuperCell limit

Configuration Options

from cocapi import CocApi, ApiConfig

config = ApiConfig(
    key_count=2,               # Number of keys to maintain (default: 1)
    key_name="my-bot",         # Key name on the portal (default: "cocapi_auto")
    auto_refresh_keys=True,    # Auto-rotate on IP change (default: True)
)
api = CocApi.from_credentials("email", "password", config=config)

Persisting Keys Locally

For scripts that run repeatedly, enable persist_keys to cache the token on disk. On the next run, if your IP hasn't changed, the cached token is reused without contacting the developer portal:

config = ApiConfig(persist_keys=True)
api = CocApi.from_credentials("email", "password", config=config)
# First run:  logs in, creates key, saves to ~/.cocapi/keys.json
# Next runs:  detects IP, matches cache, skips login entirely

Disabled by default. Storage path is customizable via key_storage_path.

Standalone Key Manager

Use SyncKeyManager or AsyncKeyManager directly if you need tokens outside of CocApi:

from cocapi import SyncKeyManager

with SyncKeyManager("email", "password", key_count=3) as km:
    tokens = km.manage_keys()
    print(f"Got {len(tokens)} token(s)")

# Async version
from cocapi import AsyncKeyManager

async with AsyncKeyManager("email", "password") as km:
    tokens = await km.manage_keys()

Configuration

All options are set through ApiConfig:

from cocapi import CocApi, ApiConfig

config = ApiConfig(
    # Request settings
    timeout=30,
    max_retries=3,
    retry_delay=1.0,           # Base delay for exponential backoff

    # Caching
    enable_caching=True,
    cache_ttl=600,             # Seconds (default: 300)

    # Rate limiting (async only)
    enable_rate_limiting=True,
    requests_per_second=10.0,
    burst_limit=20,

    # Metrics
    enable_metrics=True,
    metrics_window_size=1000,

    # Pydantic models (requires cocapi[pydantic])
    use_pydantic_models=True,
)

api = CocApi("your_token", config=config)

# Runtime management
api.get_cache_stats()
api.get_metrics()
api.clear_cache()
api.clear_metrics()

Middleware

Add custom processing to requests and responses:

from cocapi import CocApi
from cocapi.middleware import add_user_agent_middleware, add_request_id_middleware

api = CocApi("your_token")
api.add_request_middleware(add_user_agent_middleware("MyApp/1.0"))
api.add_request_middleware(add_request_id_middleware())

# Custom middleware
def add_custom_header(url, headers, params):
    headers["X-My-Header"] = "value"
    return url, headers, params

api.add_request_middleware(add_custom_header)

Custom Endpoints

Call new SuperCell endpoints without waiting for a library update:

result = api.custom_endpoint("/new-endpoint")
result = api.custom_endpoint("/clans/search", {"name": "clash", "limit": 10})

# With dynamic Pydantic model
result = api.custom_endpoint("/new-endpoint", use_dynamic_model=True)

Base URL Override

For proxies or testing environments:

config = ApiConfig(base_url="https://my-proxy.com/clash/v1")
api = CocApi("token", config=config)

# Or at runtime
api.set_base_url("https://staging.example.com/v1", force=True)
api.reset_base_url()

Pagination

Endpoints that return lists (clan members, rankings, leagues, etc.) use cursor-based pagination. The paginate() helper auto-follows cursors and yields items one by one:

# Iterate through all clan members
for member in api.paginate(api.clan_members, "#CLAN_TAG"):
    print(member["name"])

# Custom page size
for clan in api.paginate(api.location_id_clan_rank, "32000087", limit=50):
    print(clan["name"])

Pass the method reference and its arguments (excluding the params dict). The helper manages limit and after internally.

Works with async too:

async with CocApi("token") as api:
    async for member in api.paginate(api.clan_members, "#CLAN_TAG"):
        print(member["name"])

Works with any list endpoint: clan_members, clan_war_log, clan_capitalraidseasons, location, location_id_clan_rank, location_id_player_rank, league, league_season, league_season_id, labels_clans, labels_players, capitalleagues, builderbaseleagues, leaguetiers, and all other ranking/listing endpoints.

Note: clan() search has a special signature and should be paginated manually using the params dict if needed.

Batch Fetch

Fetch multiple resources in one call. Sync runs sequentially; async runs concurrently with asyncio.gather:

# Fetch multiple players at once
results = api.batch(api.players, ["#TAG1", "#TAG2", "#TAG3"])
for player in results:
    print(player["name"], player["trophies"])

# Methods with multiple args — pass tuples
results = api.batch(api.league_season_id, [("29000022", "2025-01"), ("29000022", "2025-02")])

Async with concurrency control:

async with CocApi("token") as api:
    results = await api.batch(api.clan_tag, clan_tags, max_concurrent=5)

If one call fails, its position in the result list gets the error dict. Other calls are unaffected.

Event Polling

Monitor clans, wars, and players in real time with poll-and-compare. Async only.

import asyncio
from cocapi import CocApi, ApiConfig
from cocapi.events import EventStream, EventType

async def main():
    async with CocApi("token", config=ApiConfig(enable_caching=False)) as api:
        stream = EventStream(api)
        stream.watch_clans(["#2PP"], interval=60)
        stream.watch_wars(["#2PP"], interval=30)
        stream.watch_players(["#900PUCPV"], interval=120)

        async with stream:
            async for event in stream:
                print(event.event_type, event.tag)
                for change in event.changes:
                    print(f"  {change.field}: {change.old_value} -> {change.new_value}")

asyncio.run(main())

Event Types

Event Trigger
CLAN_UPDATED Any clan field changed (level, points, description, etc.)
MEMBER_JOINED New member detected in clan
MEMBER_LEFT Member no longer in clan
MEMBER_UPDATED Existing member's data changed (trophies, donations, etc.)
MEMBER_ROLE_CHANGED Member promoted or demoted (member → elder → co-leader)
MEMBER_DONATIONS Member donation count changed
WAR_STATE_CHANGED War state transition (notInWar → preparation → inWar → warEnded)
WAR_ATTACK_NEW New attack detected in an active or ended war
PLAYER_UPDATED Any tracked player top-level field changed
TROOP_UPGRADED Individual troop level increased
SPELL_UPGRADED Individual spell level increased
HERO_UPGRADED Individual hero level increased
HERO_EQUIPMENT_UPGRADED Hero equipment level increased
TOWNHALL_UPGRADED Town Hall level increased
BUILDERHALL_UPGRADED Builder Hall level increased
PLAYER_NAME_CHANGED Player name changed
PLAYER_LEAGUE_CHANGED Player league changed
PLAYER_LABEL_CHANGED Player labels added or removed
MAINTENANCE_START API entered maintenance mode (HTTP 503)
MAINTENANCE_END API recovered from maintenance
POLL_ERROR API error during polling (watcher continues)

Callbacks

Use decorators instead of (or alongside) the async generator:

stream = EventStream(api)
stream.watch_clans(["#2PP"])

@stream.on(EventType.MEMBER_JOINED)
async def on_join(event):
    print(f"{event.metadata['member_name']} joined {event.tag}!")

@stream.on(EventType.WAR_STATE_CHANGED)
async def on_war(event):
    print(f"War: {event.metadata['war_state_from']} -> {event.metadata['war_state_to']}")

await stream.run()  # Blocks, dispatches to callbacks

Options

  • Field filtering for player watchers: watch_players(tags, include_fields=frozenset({"trophies"})) or exclude_fields
  • Member tracking: watch_clans(tags, track_members=False) to skip join/leave/update detection
  • Backpressure: EventStream(api, queue_size=1000) — bounded queue prevents unbounded memory growth
  • Maintenance detection: stream.watch_maintenance(interval=30) — detects API 503 maintenance windows
  • State persistence: EventStream(api, persist_path="state.json") — saves snapshots on stop, restores on start

Examples

Runnable scripts in the examples/ folder:

Script What it covers
basic_usage.py Sync: clan info, player info, search, error handling
async_usage.py Async with context manager, concurrent requests
credential_auth.py Email/password login, key persistence, auto-refresh
pagination_batch.py paginate() and batch() in sync and async
event_polling.py EventStream with async for — real-time monitoring
event_callbacks.py @stream.on() decorators and stream.run()
configuration.py ApiConfig, caching, metrics, middleware
# Run any example (replace the token inside first)
python examples/basic_usage.py

CLI

Install with the cli extra:

pip install 'cocapi[cli]'

Login with credentials (persisted)

Log in once with your developer portal email/password. The API key is saved to ~/.cocapi/keys.json and reused automatically:

cocapi login --email you@example.com --password yourpass

# Now all commands work without --token
cocapi clan "#2PP"
cocapi goldpass

Other authentication methods

# Explicit token
cocapi clan "#2PP" --token YOUR_TOKEN

# Environment variable
export COCAPI_TOKEN="YOUR_TOKEN"
cocapi clan "#2PP"

# Inline credentials (also persisted)
cocapi clan "#2PP" --email you@example.com --password yourpass

Commands

Add --json to any command for raw JSON output. Add --limit N where supported.

Clans

cocapi clan "#2PP"                           # Clan info
cocapi members "#2PP" --limit 10             # Clan members
cocapi war "#2PP"                            # Current war
cocapi warlog "#2PP"                         # War log
cocapi cwl "#2PP"                            # CWL league group
cocapi raids "#2PP"                          # Capital raid seasons
cocapi search "clash" --limit 5              # Search clans by name
cocapi search "war" --min-level 10 --war-frequency always  # Advanced search

Players

cocapi player "#900PUCPV"                    # Player info
cocapi verify-token "#900PUCPV" "abc123"     # Verify in-game token

Locations & Rankings

cocapi locations                             # List all locations
cocapi location 32000006                     # Specific location info
cocapi rankings 32000087 clans               # Clan rankings for location
cocapi rankings 32000087 players             # Player rankings
cocapi rankings 32000087 clans-builder-base  # Builder base clan rankings
cocapi rankings 32000087 players-builder-base
cocapi rankings 32000087 capitals            # Capital rankings

Leagues

cocapi leagues                               # List leagues
cocapi league 29000022                       # Specific league info
cocapi league-seasons 29000022               # List seasons (Legend League)
cocapi league-seasons 29000022 2025-01       # Season rankings
cocapi war-leagues                           # List war leagues
cocapi war-leagues 48000000                  # Specific war league
cocapi capital-leagues                       # List capital leagues
cocapi capital-leagues 85000000              # Specific capital league
cocapi builder-base-leagues                  # List builder base leagues
cocapi builder-base-leagues 44000000         # Specific builder base league
cocapi league-tiers                          # List league tiers
cocapi league-tiers 105000001               # Specific league tier

Other

cocapi goldpass                              # Current gold pass season
cocapi labels clans                          # Clan labels
cocapi labels players                        # Player labels
cocapi cwl-war "#WARTAG"                     # Specific CWL war

API Reference

All methods work in both sync and async mode. In async, use await. Pagination parameters (limit, after, before) can be passed as a dict.


Clans

Clan Info

api.clan_tag(tag)  # e.g. "#2PP"
Response
{
  "tag": "string",
  "name": "string",
  "type": "string",
  "description": "string",
  "location": {"id": 0, "name": "string", "isCountry": true, "countryCode": "string"},
  "isFamilyFriendly": true,
  "badgeUrls": {"small": "string", "large": "string", "medium": "string"},
  "clanLevel": 0,
  "clanPoints": 0,
  "clanBuilderBasePoints": 0,
  "clanCapitalPoints": 0,
  "capitalLeague": {"id": 0, "name": "string"},
  "requiredTrophies": 0,
  "requiredBuilderBaseTrophies": 0,
  "requiredTownhallLevel": 0,
  "warFrequency": "string",
  "warWinStreak": 0,
  "warWins": 0,
  "isWarLogPublic": true,
  "warLeague": {"id": 0, "name": "string"},
  "members": 0,
  "memberList": [{"tag": "string", "name": "string", "role": "string", "townHallLevel": 0, "expLevel": 0, "trophies": 0, "clanRank": 0, "donations": 0, "donationsReceived": 0}],
  "labels": [{"id": 0, "name": "string", "iconUrls": {}}],
  "clanCapital": {"capitalHallLevel": 0, "districts": []},
  "chatLanguage": {"id": 0, "name": "string", "languageCode": "string"}
}

Clan Members

api.clan_members(tag)
api.clan_members(tag, {"limit": 20})

Search Clans

api.clan("clash", 10)

# With filters
api.clan(
    "war", 20,
    war_frequency="always",
    location_id=32000006,
    min_members=30,
    max_members=50,
    min_clan_points=20000,
    min_clan_level=10,
    label_ids="56000000,56000001",
)

War Log

api.clan_war_log(tag)
Response
{"items": [{"result": "string", "endTime": "string", "teamSize": 0, "attacksPerMember": 0, "clan": {"tag": "string", "name": "string", "stars": 0, "destructionPercentage": 0.0}, "opponent": {"tag": "string", "name": "string", "stars": 0, "destructionPercentage": 0.0}}], "paging": {"cursors": {}}}

Current War

api.clan_current_war(tag)
Response
{"state": "string", "teamSize": 0, "preparationStartTime": "string", "startTime": "string", "endTime": "string", "clan": {"tag": "string", "name": "string", "clanLevel": 0, "attacks": 0, "stars": 0, "destructionPercentage": 0.0, "members": [{"tag": "string", "name": "string", "townhallLevel": 0, "mapPosition": 0, "attacks": [{"order": 0, "attackerTag": "string", "defenderTag": "string", "stars": 0, "destructionPercentage": 0}]}]}, "opponent": {"tag": "string", "name": "string", "clanLevel": 0, "members": []}}

Clan War League Group

api.clan_leaguegroup(tag)
Response
{"tag": "string", "state": "string", "season": "string", "clans": [{"tag": "string", "clanLevel": 0, "name": "string", "members": [{"tag": "string", "townHallLevel": 0, "name": "string"}], "badgeUrls": {}}], "rounds": [{"warTags": ["string"]}]}

Capital Raid Seasons

api.clan_capitalraidseasons(tag)
api.clan_capitalraidseasons(tag, {"limit": 5})

CWL War by Tag

api.warleague(war_tag)

Players

Player Info

api.players(player_tag)  # e.g. "#900PUCPV"
Response
{
  "tag": "string",
  "name": "string",
  "townHallLevel": 0,
  "expLevel": 0,
  "trophies": 0,
  "bestTrophies": 0,
  "warStars": 0,
  "attackWins": 0,
  "defenseWins": 0,
  "builderHallLevel": 0,
  "builderBaseTrophies": 0,
  "role": "string",
  "warPreference": "string",
  "donations": 0,
  "donationsReceived": 0,
  "clan": {"tag": "string", "name": "string", "clanLevel": 0, "badgeUrls": {}},
  "achievements": [{"name": "string", "stars": 0, "value": 0, "target": 0, "info": "string"}],
  "troops": [{"name": "string", "level": 0, "maxLevel": 0, "village": "string"}],
  "heroes": [{"name": "string", "level": 0, "maxLevel": 0, "village": "string"}],
  "heroEquipment": [],
  "spells": [{"name": "string", "level": 0, "maxLevel": 0, "village": "string"}]
}

Verify Player Token

api.verify_player_token(player_tag, "player_api_token")

Returns {"tag": "string", "token": "string", "status": "string"}.


Locations

All Locations

api.location()
api.location({"limit": 10})
Response
{"items": [{"id": 0, "name": "string", "isCountry": true, "countryCode": "string"}], "paging": {"cursors": {}}}

Single Location

api.location_id("32000006")
Response
{"id": 0, "name": "string", "isCountry": true, "countryCode": "string"}

Top Clans in a Location

api.location_id_clan_rank(location_id)
api.location_id_clan_rank(location_id, {"limit": 10})
Response
{"items": [{"tag": "string", "name": "string", "location": {"id": 0, "name": "string", "isCountry": true, "countryCode": "string"}, "badgeUrls": {}, "clanLevel": 0, "members": 0, "clanPoints": 0, "rank": 0, "previousRank": 0}], "paging": {"cursors": {}}}

Top Players in a Location

api.location_id_player_rank(location_id)
Response
{"items": [{"tag": "string", "name": "string", "expLevel": 0, "trophies": 0, "attackWins": 0, "defenseWins": 0, "rank": 0, "previousRank": 0, "clan": {"tag": "string", "name": "string", "badgeUrls": {}}, "league": {"id": 0, "name": "string", "iconUrls": {}}, "leagueTier": {"id": 0, "name": "string", "iconUrls": {}}}], "paging": {"cursors": {}}}

Top Builder Base Clans in a Location

api.location_clans_builder_base(location_id)
Response
{"items": [{"tag": "string", "name": "string", "location": {"id": 0, "name": "string", "isCountry": true, "countryCode": "string"}, "badgeUrls": {}, "clanLevel": 0, "members": 0, "rank": 0, "previousRank": 0, "clanBuilderBasePoints": 0}], "paging": {"cursors": {}}}

Top Builder Base Players in a Location

api.location_players_builder_base(location_id)
Response
{"items": [{"tag": "string", "name": "string", "expLevel": 0, "rank": 0, "previousRank": 0, "builderBaseTrophies": 0, "clan": {"tag": "string", "name": "string", "badgeUrls": {}}, "builderBaseLeague": {"id": 0, "name": "string"}}], "paging": {"cursors": {}}}

Capital Rankings in a Location

api.location_capital_rankings(location_id)
Response
{"items": [{"tag": "string", "name": "string", "location": {"id": 0, "name": "string", "isCountry": true, "countryCode": "string"}, "badgeUrls": {}, "clanLevel": 0, "members": 0, "rank": 0, "previousRank": 0, "clanCapitalPoints": 0}], "paging": {"cursors": {}}}

Top Versus Clans in a Location (Deprecated)

api.location_clan_versus(location_id)

Deprecated: May return an error with error_type: "deprecated".

Top Versus Players in a Location (Deprecated)

api.location_player_versus(location_id)

Deprecated: May return an error with error_type: "deprecated".


Leagues

List Leagues

api.league()
api.league({"limit": 5})
Response
{"items": [{"id": 0, "name": "string", "iconUrls": {}}], "paging": {"cursors": {}}}

League Info

api.league_id("29000022")
Response
{"id": 0, "name": "string", "iconUrls": {"small": "string", "tiny": "string", "medium": "string"}}

Legend League Seasons

api.league_season("29000022")
Response
{"items": [{"id": "string"}], "paging": {"cursors": {}}}

Legend League Season Rankings

api.league_season_id("29000022", "2025-01", {"limit": 100})

Note: limit must be between 100 and 25,000.

Response
{"items": [{"tag": "string", "name": "string", "expLevel": 0, "trophies": 0, "attackWins": 0, "defenseWins": 0, "rank": 0, "clan": {"tag": "string", "name": "string", "badgeUrls": {}}, "leagueTier": {"id": 0, "name": "string", "iconUrls": {}}}], "paging": {"cursors": {}}}

Capital Leagues

api.capitalleagues()
api.capitalleagues_id("85000000")
Response
{"items": [{"id": 0, "name": "string"}], "paging": {"cursors": {}}}

Builder Base Leagues

api.builderbaseleagues()
api.builderbaseleagues_id("44000000")
Response
{"items": [{"id": 0, "name": "string"}], "paging": {"cursors": {}}}

League Tiers

api.leaguetiers()
api.leaguetiers_id("105000001")
Response
{"items": [{"id": 0, "name": "string", "iconUrls": {"small": "string", "large": "string"}}], "paging": {"cursors": {}}}

War Leagues

api.warleagues()
api.warleagues_id("48000000")
Response
{"items": [{"id": 0, "name": "string"}], "paging": {"cursors": {}}}

Gold Pass

api.goldpass()
Response
{"startTime": "string", "endTime": "string"}

Labels

Clan Labels

api.labels_clans()
Response
{"items": [{"id": 0, "name": "string", "iconUrls": {}}], "paging": {"cursors": {}}}

Player Labels

api.labels_players()
Response
{"items": [{"id": 0, "name": "string", "iconUrls": {}}], "paging": {"cursors": {}}}

Error Handling

All endpoints return a dict. Errors have this structure:

result = api.clan_tag("#INVALID")
if result.get("result") == "error":
    print(result["message"])      # Human-readable message
    print(result["error_type"])   # "timeout", "connection", "http", "json", "retry_exhausted", "unknown"
    print(result.get("http_status"))  # HTTP status code (present for "http" error_type)

Pydantic Models

When use_pydantic_models=True (requires pip install 'cocapi[pydantic]'), endpoints return typed model objects instead of dicts:

from cocapi import CocApi, ApiConfig

config = ApiConfig(use_pydantic_models=True)
api = CocApi("token", config=config)

clan = api.clan_tag("#2PP")
print(clan.name)        # Attribute access with IDE autocompletion
print(clan.clanLevel)

player = api.players("#TAG")
print(player.trophies)

Available models: Clan, Player, ClanMember, League, Location, Achievement, ClanWar, GoldPassSeason, and more. Import directly from cocapi.

Credits

cocapi is not affiliated with SuperCell.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

cocapi-4.0.0.tar.gz (105.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

cocapi-4.0.0-py3-none-any.whl (73.4 kB view details)

Uploaded Python 3

File details

Details for the file cocapi-4.0.0.tar.gz.

File metadata

  • Download URL: cocapi-4.0.0.tar.gz
  • Upload date:
  • Size: 105.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for cocapi-4.0.0.tar.gz
Algorithm Hash digest
SHA256 5660b654a9997f75aa381c14b3eec78a3f9beb349579171e4e1ed80f4b3e341c
MD5 f01e3652a7a7a56aa5cad39b9ee04231
BLAKE2b-256 ae7052a75be4d31a864ab4b112b353307c70d6b9f2e97781df3a7fd3663669e9

See more details on using hashes here.

File details

Details for the file cocapi-4.0.0-py3-none-any.whl.

File metadata

  • Download URL: cocapi-4.0.0-py3-none-any.whl
  • Upload date:
  • Size: 73.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for cocapi-4.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 81a4f4a5b9b1cd8163656064d611f0f0123f73bb8114663d3b6ee5e288e17bc5
MD5 2429d84dfe00e10ec2464705fb6ba245
BLAKE2b-256 c8a105e02236eced23ecc9da5032078247593ba09b41e14d27322f5b658876c0

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page