Async Python client for the CTFd API (v1).
uv add ctfd-api
# or
pip install ctfd-api- Python 3.13+
- httpx (installed automatically)
import asyncio
from ctfd import CTFdClient
async def main():
async with CTFdClient('https://my-ctf.example.com', token='ctfd_...') as ctfd:
# Get the current user
me = await ctfd.users.me()
print(me.id, me.name)
# List all challenges
challenges = await ctfd.challenges.list()
for ch in challenges:
print(ch.name, ch.value, ch.category)
asyncio.run(main())Pass an API token obtained from Profile → API Access Tokens:
ctfd = CTFdClient('https://my-ctf.example.com', token='ctfd_abc123')Without a token the client still works for public endpoints (e.g. scoreboard).
Every swagger tag maps to an attribute on CTFdClient:
| Attribute | Resource |
|---|---|
ctfd.challenges |
Challenges, attempts, sub-resources |
ctfd.users |
Users, /me, solves, fails, awards |
ctfd.teams |
Teams, /me, members, solves, fails, awards |
ctfd.scoreboard |
Full list, top-N |
ctfd.flags |
Flags, types |
ctfd.hints |
Hints |
ctfd.tags |
Tags |
ctfd.topics |
Topics |
ctfd.awards |
Awards |
ctfd.submissions |
Submissions |
ctfd.files |
Files, upload, download |
ctfd.notifications |
Notifications |
ctfd.configs |
Config keys, fields |
ctfd.pages |
Pages |
ctfd.tokens |
API tokens |
ctfd.unlocks |
Unlocks |
ctfd.comments |
Comments |
ctfd.shares |
Shares |
ctfd.brackets |
Brackets |
ctfd.solutions |
Solutions |
ctfd.statistics |
Statistics aggregates |
ctfd.exports |
Export archive |
List endpoints return the first page. Use .iter() to walk all pages automatically:
async with CTFdClient('https://my-ctf.example.com', token='ctfd_...') as ctfd:
# All users, page by page
async for user in ctfd.users.iter():
print(user.id, user.name)
# Or collect everything at once
all_submissions = await ctfd.submissions.iter().all()result = await ctfd.challenges.attempt(challenge_id=42, submission='flag{example}')
print(result['status']) # 'correct' or 'incorrect'from ctfd.models import Challenge
ch = await ctfd.challenges.create({
'name': 'My Challenge',
'description': 'Find the flag.',
'value': 100,
'category': 'web',
'type': 'standard',
'state': 'visible',
})
print(ch.id)flag = await ctfd.flags.create({
'challenge_id': ch.id,
'type': 'static',
'content': 'flag{secret}',
})
await ctfd.flags.delete(flag.id)with open('attachment.zip', 'rb') as f:
files = await ctfd.files.create({
'files': [('file', ('attachment.zip', f, 'application/zip'))],
'type': 'challenge',
'challenge_id': 42,
})# Download entirely in memory
data = await ctfd.exports.raw()
with open('ctfd_backup.zip', 'wb') as f:
f.write(data)
# Or stream to disk
async with CTFdClient(...) as ctfd:
with open('ctfd_backup.zip', 'wb') as f:
async for chunk in ctfd.exports.stream():
f.write(chunk)# Add a user to a team
await ctfd.teams.add_member(team_id=5, user_id=12)
# Remove a member
await ctfd.teams.remove_member(team_id=5, user_id=12)# Bulk update
await ctfd.configs.bulk_update({'ctf_name': 'My CTF', 'ctf_description': 'Have fun!'})
# Single key
cfg = await ctfd.configs.get('ctf_name')
print(cfg.value)from ctfd import (
CTFdAuthenticationError,
CTFdNotFoundError,
CTFdPermissionError,
CTFdRateLimitError,
CTFdValidationError,
)
try:
ch = await ctfd.challenges.get(9999)
except CTFdNotFoundError:
print('challenge not found')
except CTFdAuthenticationError:
print('invalid or missing token')
except CTFdPermissionError:
print('admin rights required')
except CTFdValidationError as e:
print('bad request:', e.errors)
except CTFdRateLimitError:
print('rate limited, slow down')uv sync # install deps + dev tools
uv run pre-commit install # install git hooks
uv run pytest # run tests
uv run pytest --cov # tests with coverage
uv run ruff check ctfd # lint
uv run mypy # type-check