API & Developers
A small, honest JSON API over the same data the website shows you — CTF challenges and submissions, the leaderboard, public profiles, and the forum. Built this whole platform and want to pull live data into your own site, script, or bot? This is how.
Authentication
Every request is authenticated with an API key, generated from your account's
Settings page. Keys look like
sg_<48 hex characters> and are shown to you exactly once, at creation —
SecurityGab only ever stores a one-way hash of it, the same way your password is stored, so
nobody (including us) can look up or recover the raw key later. If you lose it, revoke it and
generate a new one.
Pass it in either header — both are accepted identically:
http
Authorization: Bearer sg_4285a7b26bc38b2f583d3003ab3f24a21186ab898d65bc6c # — or — X-Api-Key: sg_4285a7b26bc38b2f583d3003ab3f24a21186ab898d65bc6c
Endpoints marked auth below require a key. Endpoints marked public work with no key at all, but including one still counts toward your own rate limit bucket rather than sharing the anonymous one.
Quickstart
Fetch the leaderboard — no key needed for a read-only public endpoint:
curl
curl https://website.dengisan.nl/api/v1/leaderboard.php?limit=10
Submit a CTF flag — requires a key, since it acts on your account:
curl
curl -X POST https://website.dengisan.nl/api/v1/ctf/submit.php \
-H "Authorization: Bearer sg_YOUR_KEY_HERE" \
-H "Content-Type: application/json" \
-d '{"challenge_id": 3, "flag": "flag{...}"}'
Same thing from JavaScript:
javascript
const res = await fetch("https://website.dengisan.nl/api/v1/ctf/submit.php", {
method: "POST",
headers: {
"Authorization": "Bearer sg_YOUR_KEY_HERE",
"Content-Type": "application/json",
},
body: JSON.stringify({ challenge_id: 3, flag: "flag{...}" }),
});
const { success, data, error } = await res.json();
Or Python:
python
import requests
r = requests.post(
"https://website.dengisan.nl/api/v1/ctf/submit.php",
headers={"Authorization": "Bearer sg_YOUR_KEY_HERE"},
json={"challenge_id": 3, "flag": "flag{...}"},
)
print(r.json())
Endpoints
Every response is JSON: {"success": true, "data": ...} or {"success": false, "error": "..."}.
| method | path | description | auth |
|---|---|---|---|
| GET | /api/v1/me.php | Info about the authenticated caller. | auth |
| GET | /api/v1/ctf/challenges.php | List active CTF challenges. | public |
| POST | /api/v1/ctf/submit.php | Submit a flag — single-flag or a stage of a mixed challenge. | auth |
| GET | /api/v1/leaderboard.php?limit=50 | CTF leaderboard. | public |
| GET | /api/v1/profile.php?u=username | Public profile: bio, avatar, stats, reputation, badges. | public |
| GET | /api/v1/forum/boards.php | Categories and boards. | public |
| GET | /api/v1/forum/threads.php?board_id=1 | Threads in a board. | public |
| GET | /api/v1/forum/thread.php?id=1 | A thread and its posts. | public |
| GET | /api/v1/forum/search.php?q=sqlmap | Search thread titles and post bodies. | public |
| POST | /api/v1/forum/reply.php | Post a reply to a thread. Requires a verified, unmuted account. | auth |
| POST | /api/v1/ctf/reveal-hint.php | Reveal a CTF hint — costs points off that challenge's eventual solve award. | auth |
Rate limits
- 120 requests/minute per key (or per IP address if you're calling public endpoints without one) across the whole API.
- 10 requests/minute specifically on flag submission, on top of the limit above.
- Going over either returns HTTP
429with{"success": false, "error": "..."}. Back off and retry after a minute.
Errors
Standard HTTP status codes, always with a JSON body — never an HTML error page or a stack trace:
| status | meaning |
|---|---|
400 | Request understood, but rejected (e.g. wrong flag, already solved). |
401 | Missing or invalid API key on an endpoint that requires one. |
404 | The thing you asked for doesn't exist (or isn't visible to you). |
422 | Required parameter missing or malformed. |
429 | Rate limit exceeded. |
500 | Something broke on our end. These are logged; if it persists, let us know. |
Internal errors are deliberately generic on purpose — the real exception detail (query text, file, line) is logged server-side only and never included in an API response, even in a 500. That's not an oversight to work around; it's how the endpoint is supposed to behave.
Data & privacy
The API doesn't expose anything the website itself doesn't already show a logged-out visitor, plus whatever's tied to your own key when you're authenticated. Concretely:
- Public endpoints (challenges, leaderboard, profiles, forum) only ever return the same public-facing fields the website renders: username, bio, avatar, role, public stats, badges, board/thread/post content. Never email addresses, password hashes, session tokens, IP addresses, or any other account-internal field, regardless of who's asking.
- Authenticated endpoints (
/me.php, flag submission) only ever act on or return data belonging to the account the key was issued to. There's no way for one key to read or affect another member's private state. - What we log about API usage itself: each key's
last_used_attimestamp (so you can see in Settings whether a key is actually being used), and short-lived rate-limit counters (a rolling list of request timestamps per key/IP, used only to enforce the limits above and automatically pruned as they age out). We don't log full request bodies, flag guesses, or response payloads anywhere. - API data isn't sold, shared with third parties, or used for anything beyond running the platform — same policy as the rest of the site. See the full Privacy Policy for how account data is handled generally.
If you build something public with this (a bot, a leaderboard widget, a companion app) — please don't cache or republish more than you need, and don't scrape endpoints faster than the rate limits already allow just because you technically can from multiple keys. It's a small community; play fair with it.