ctfd.py
Async Python client for the CTFd API (v1).
Installation
Requirements: Python 3.13+ · httpx (installed automatically)
Quick start
import asyncio
from ctfd import CTFdClient
async def main():
async with CTFdClient('https://my-ctf.example.com', token='ctfd_...') as ctfd:
me = await ctfd.users.me()
print(me.id, me.name)
challenges = await ctfd.challenges.list()
for ch in challenges:
print(ch.name, ch.value, ch.category)
asyncio.run(main())
Authentication
Pass an API token obtained from Profile → API Access Tokens:
Without a token the client still works for public endpoints (e.g. scoreboard).
Resources
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 |
Pagination
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:
async for user in ctfd.users.iter():
print(user.id, user.name)
all_submissions = await ctfd.submissions.iter().all()
Common operations
Submit a flag
result = await ctfd.challenges.attempt(challenge_id=42, submission='flag{example}')
print(result['status']) # 'correct' or 'incorrect'
Create a challenge (admin)
ch = await ctfd.challenges.create({
'name': 'My Challenge',
'description': 'Find the flag.',
'value': 100,
'category': 'web',
'type': 'standard',
'state': 'visible',
})
print(ch.id)
Manage flags (admin)
flag = await ctfd.flags.create({
'challenge_id': ch.id,
'type': 'static',
'content': 'flag{secret}',
})
await ctfd.flags.delete(flag.id)
Upload a file (admin)
with open('attachment.zip', 'rb') as f:
files = await ctfd.files.create({
'files': [('file', ('attachment.zip', f, 'application/zip'))],
'type': 'challenge',
'challenge_id': 42,
})
Export (admin)
data = await ctfd.exports.raw()
with open('ctfd_backup.zip', 'wb') as f:
f.write(data)
async with CTFdClient(...) as ctfd:
with open('ctfd_backup.zip', 'wb') as f:
async for chunk in ctfd.exports.stream():
f.write(chunk)
Team management (admin)
await ctfd.teams.add_member(team_id=5, user_id=12)
await ctfd.teams.remove_member(team_id=5, user_id=12)
Config (admin)
await ctfd.configs.bulk_update({'ctf_name': 'My CTF', 'ctf_description': 'Have fun!'})
cfg = await ctfd.configs.get('ctf_name')
print(cfg.value)
Error handling
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')