Connect CodeBlanks to your product
Let your users sign in with their CodeBlanks account, read their learning progress (XP, badges, certificates) via the API, and receive events instantly via webhooks.
Sign in to get startedOverview
There are three ways in; use one or all of them:
Authentication
Send a credential in the Authorization header with every request. Two kinds are accepted:
- Personal API key (starts with cbk_): created under Dashboard > Developer, reads your own account's data.
- OAuth access token: obtained via "Sign in with CodeBlanks", reads the consenting user's data within the granted scopes.
curl -H "Authorization: Bearer cbk_…" https://codeblanks.com/api/v1/me Keep keys and secrets on your server only; never ship them in a browser or a mobile app bundle.
Scopes
A key or token can only call endpoints for the scopes it holds.
- profile: name, username, avatar, profile link, XP
- email: email address
- progress: XP, rank, streak, badges, certificates
Endpoints
All endpoints are read-only (GET) and return JSON. Success responses use the "data" field, errors the "error" field. Use ?lang=tr|en to choose the language of badge and rank names.
https://codeblanks.com/api/v1
| Request | Scope | Description |
|---|---|---|
GET /me |
profile |
Profile of the authenticated user. Includes the email when the email scope is granted. |
GET /me/progress |
progress |
XP, rank, streak, completed course count, badges and certificates. |
GET /users/{username} |
profile |
Public profile of any user. |
GET /users/{username}/badges |
profile |
Badges the user has earned. |
GET /certificates/{code} |
any | Certificate verification: returns the holder and the course when the code is valid. |
Example request
curl -H "Authorization: Bearer cbk_…" "https://codeblanks.com/api/v1/me/progress?lang=tr" Example response
{
"data": {
"xp": 1840,
"rank": {
"current": { "id": 3, "name": "Kod Çırağı", "min_xp": 1000 },
"next": { "id": 4, "name": "Geliştirici", "min_xp": 2500 }
},
"streak": { "current": 6, "longest": 21, "last_activity": "2026-10-13" },
"courses_completed": 2,
"badges": [
{ "id": 12, "key": "lessons_completed_10", "name": "10 ders", "icon": "fas fa-book", "color": "#0D9488", "xp_reward": 50, "earned_at": "2026-10-02T14:11:09+03:00" }
],
"certificates": [
{ "code": "CB-1A2B-3C4D-5E6F", "course_id": 4, "course_title": "PHP Temelleri", "issued_at": "2026-09-28T10:00:00+03:00", "verify_url": "https://codeblanks.com/api/v1/certificates/CB-1A2B-3C4D-5E6F" }
]
}
} Rate limits and errors
60 requests per minute per key / token. Every response includes X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset headers; over the limit you get 429 with Retry-After.
401 |
Credential missing, invalid, expired or revoked. |
403 |
Insufficient scope (insufficient_scope) or suspended account. |
404 |
User, certificate or endpoint not found. |
429 |
Rate limit exceeded. |
503 |
The API is temporarily disabled. |
{ "error": { "code": "insufficient_scope", "message": "This credential does not have the \"progress\" scope." } } Add "Sign in with CodeBlanks"
Standard OAuth 2.0 authorization code flow + PKCE (S256, required) + OpenID Connect. Any OAuth / OIDC library works by pointing it at the discovery URL.
- Create an app under Dashboard > Developer > My apps; copy the client_id and client secret and register your redirect URI.
- Send the user to the authorization URL (response_type=code, client_id, redirect_uri, scope, state, code_challenge, code_challenge_method=S256).
- After consent, the user returns to your redirect URI with ?code=…&state=…; verify the state.
- Exchange the code at the token endpoint from your server (grant_type=authorization_code, code, redirect_uri, code_verifier, client credentials). You get an access_token, refresh_token and id_token.
- Verify the id_token with the JWKS, or call userinfo / the API with the access_token. Match the user by "sub" (the CodeBlanks user ID).
Endpoints
| Discovery document | https://codeblanks.com/.well-known/openid-configuration |
| Authorization | https://codeblanks.com/oauth/authorize |
| Token | POST https://codeblanks.com/oauth/token |
| User info | https://codeblanks.com/oauth/userinfo |
| Signing keys (JWKS) | https://codeblanks.com/oauth/jwks.json |
| Token revocation | POST https://codeblanks.com/oauth/revoke |
Access tokens last 1 hour, refresh tokens 30 days. Refresh tokens rotate on every use; if an old one is reused, all tokens of that session are revoked.
PHP example (league/oauth2-client)
$provider = new \League\OAuth2\Client\Provider\GenericProvider([
'clientId' => 'cb_…',
'clientSecret' => 'cbs_…',
'redirectUri' => 'https://liyakat.net/auth/codeblanks/callback',
'urlAuthorize' => 'https://codeblanks.com/oauth/authorize',
'urlAccessToken' => 'https://codeblanks.com/oauth/token',
'urlResourceOwnerDetails' => 'https://codeblanks.com/oauth/userinfo',
'scopes' => 'openid profile email progress',
'scopeSeparator' => ' ',
'pkceMethod' => \League\OAuth2\Client\Provider\GenericProvider::PKCE_METHOD_S256,
]);
// 1) Giriş düğmesi
if (!isset($_GET['code'])) {
$url = $provider->getAuthorizationUrl();
$_SESSION['oauth2state'] = $provider->getState();
$_SESSION['oauth2pkce'] = $provider->getPkceCode();
header('Location: ' . $url);
exit;
}
// 2) Dönüş
if (!hash_equals($_SESSION['oauth2state'] ?? '', $_GET['state'] ?? '')) {
exit('Invalid state');
}
$provider->setPkceCode($_SESSION['oauth2pkce']);
$token = $provider->getAccessToken('authorization_code', ['code' => $_GET['code']]);
$user = $provider->getResourceOwner($token)->toArray(); // sub, preferred_username, name, email …
// 3) API: XP ve rozetler
$req = $provider->getAuthenticatedRequest('GET', 'https://codeblanks.com/api/v1/me/progress', $token);
$progress = json_decode((string) $provider->getResponse($req)->getBody(), true)['data']; Webhooks
Choose a URL and events under Dashboard > Developer > Webhooks. A personal webhook receives only your events; a webhook attached to your app receives events for every user who granted your app the "progress" scope.
Events
badge.earned |
Badge earned | progress |
course.completed |
Course completed | progress |
certificate.issued |
Certificate issued | progress |
rank.changed |
Rank up | progress |
Headers
POST /webhooks/codeblanks HTTP/1.1
Content-Type: application/json
User-Agent: CodeBlanks-Webhooks/1.0
X-CodeBlanks-Event: badge.earned
X-CodeBlanks-Delivery: 9f0c…
X-CodeBlanks-Signature: t=1791880000,v1=5d41402abc4b2a76b9719d911017c592… Payload
{
"id": "evt_9f0c2b7e4d1a4c0b8e6f3a2d1c0b9a87",
"type": "badge.earned",
"created": 1791880000,
"data": {
"user": { "id": 42, "username": "murat", "profile_url": "https://codeblanks.com/@murat" },
"badge": { "id": 12, "key": "lessons_completed_10", "name": "10 ders", "icon": "fas fa-book", "xp_reward": 50 }
}
} Verify the signature
The X-CodeBlanks-Signature header looks like t=<timestamp>,v1=<signature>. signature = HMAC-SHA256(signing secret, "<t>.<raw body>"). Reject requests older than 5 minutes and don't process the same X-CodeBlanks-Delivery twice.
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_CODEBLANKS_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $sig); // ['t' => …, 'v1' => …]
$expected = hash_hmac('sha256', ($sig['t'] ?? '') . '.' . $body, getenv('CODEBLANKS_WEBHOOK_SECRET'));
if (!hash_equals($expected, $sig['v1'] ?? '') || abs(time() - (int) ($sig['t'] ?? 0)) > 300) {
http_response_code(400);
exit;
}
http_response_code(204); // hemen dön, işi kuyruğa at
$event = json_decode($body, true); // Node.js (Express): gövdeyi ham haliyle al
app.post('/webhooks/codeblanks', express.raw({ type: 'application/json' }), (req, res) => {
const sig = Object.fromEntries(req.get('X-CodeBlanks-Signature').split(',').map(p => p.split('=')));
const expected = crypto.createHmac('sha256', process.env.CODEBLANKS_WEBHOOK_SECRET)
.update(sig.t + '.' + req.body).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(sig.v1 || '');
const ok = a.length === b.length && crypto.timingSafeEqual(a, b);
if (!ok || Math.abs(Date.now() / 1000 - Number(sig.t)) > 300) return res.sendStatus(400);
res.sendStatus(204);
const event = JSON.parse(req.body);
}); A non-2xx response or a timeout (5 s) counts as a failure and is retried: 1 min, 5 min, 30 min, 2 h, 6 h, 24 h. After many consecutive failures the webhook is disabled. Return 2xx right away and do the work in the background.
Support
For questions or bug reports, use the contact page. Contact