Authentication
Personal API keys for scripts and automation tools; OAuth 2.1 with PKCE and dynamic client registration for apps like Claude and ChatGPT.
Every request carries a bearer credential:
Authorization: Bearer <credential>
X-API-Key: <api key> is also accepted for tools that can't set Authorization. Cookies are never used, so the API works from servers, scripts and browsers (CORS is open) alike.
| Credential | Looks like | Lifetime | Best for |
|---|---|---|---|
| API key | ace_sk_… |
Until it expires (30 days–never) or is revoked | Your own scripts, n8n, Zapier, Make, Postman |
| OAuth access token | ace_at_… |
1 hour | Apps other people install: Claude, ChatGPT, published integrations |
| OAuth refresh token | ace_rt_… |
60 days, rotated on every use | Getting new access tokens |
The prefixes let secret scanners (GitHub, GitGuardian) recognize leaked Aceversity credentials.
Scopes
| Scope | Allows |
|---|---|
read |
Profile, courses (incl. syllabus text), deadlines, milestones, grades, practice stats, vocabulary |
write |
Creating, changing and deleting those |
read is always granted. A call without the needed scope fails with 403 insufficient_scope. Each endpoint's scope is listed in the reference and in openapi.json (security: [{ OAuth2: ["write"] }]).
API keys
Create, list and revoke keys at https://api.aceversity.com/keys. A member can have 10 active keys. The page shows when each key was last used (updated at most every 5 minutes).
Keys are stored as SHA-256 hashes only; we can't show a key again or recover it.
OAuth 2.1
Aceversity runs its own authorization server; the API origin is the issuer.
| Issuer | https://api.aceversity.com |
| Authorization server metadata (RFC 8414) | https://api.aceversity.com/.well-known/oauth-authorization-server |
| Protected resource metadata (RFC 9728) | https://api.aceversity.com/.well-known/oauth-protected-resource and …/oauth-protected-resource/mcp |
| Authorization endpoint | https://api.aceversity.com/oauth/authorize |
| Token endpoint | https://api.aceversity.com/oauth/token |
| Dynamic client registration (RFC 7591) | https://api.aceversity.com/oauth/register |
| Revocation (RFC 7009) | https://api.aceversity.com/oauth/revoke |
| Grant types | authorization_code, refresh_token |
| PKCE | Required, S256 only |
| Client authentication | none (public, PKCE), client_secret_basic, client_secret_post |
| Resource indicators (RFC 8707) | resource=https://api.aceversity.com/mcp or https://api.aceversity.com (optional) |
Register a client
MCP clients such as Claude register themselves automatically. To register by hand (e.g. for ChatGPT Actions or your own web app):
curl -X POST https://api.aceversity.com/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Study App",
"redirect_uris": ["https://myapp.example.com/oauth/callback"],
"token_endpoint_auth_method": "client_secret_post"
}'
{
"client_id": "aceclient_5b0e…",
"client_secret": "ace_cs_…",
"client_secret_expires_at": 0,
"client_id_issued_at": 1792000000,
"client_name": "My Study App",
"redirect_uris": ["https://myapp.example.com/oauth/callback"],
"token_endpoint_auth_method": "client_secret_post",
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"scope": "read write"
}
Redirect URI rules: https:// anywhere; http:// only on localhost, 127.0.0.1 or [::1] (any port — RFC 8252); private-use schemes like cursor://… for desktop apps. No fragments, no credentials in the URL. Up to 10 URIs, matched exactly. Registration is limited to 20 per hour per IP.
Authorization code flow
1. Send the member to the consent page
https://api.aceversity.com/oauth/authorize
?response_type=code
&client_id=aceclient_5b0e…
&redirect_uri=https%3A%2F%2Fmyapp.example.com%2Foauth%2Fcallback
&scope=read%20write
&state=<random>
&code_challenge=<BASE64URL(SHA256(code_verifier))>
&code_challenge_method=S256
&resource=https://api.aceversity.com
The member signs in to Aceversity if needed, sees your app's name, where it will send them back, and the scopes — they can untick Edit to grant read-only. Apps whose callback isn't a recognized platform are labelled Unverified app.
2. Receive the code at your redirect URI: ?code=ace_ac_…&state=<same>&iss=https://api.aceversity.com. Check state and iss. Codes expire after 10 minutes and work once.
If the member cancels you get ?error=access_denied&state=….
3. Exchange it
curl -X POST https://api.aceversity.com/oauth/token \
-d grant_type=authorization_code \
-d code=ace_ac_… \
-d redirect_uri=https://myapp.example.com/oauth/callback \
-d client_id=aceclient_5b0e… \
-d client_secret=ace_cs_… \
-d code_verifier=<the original verifier>
{ "access_token": "ace_at_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "ace_rt_…", "scope": "read write" }
4. Refresh before or after the access token expires. The response contains a new refresh token; the old one stops working immediately, so store the new one atomically.
curl -X POST https://api.aceversity.com/oauth/token -d grant_type=refresh_token -d refresh_token=ace_rt_… -d client_id=aceclient_5b0e… -d client_secret=ace_cs_…
5. Revoke on sign-out: POST /oauth/revoke with token=… (access or refresh) and your client credentials. Always returns 200.
Token errors follow RFC 6749: {"error": "invalid_grant", "error_description": "…"} with status 400 (401 for invalid_client).
Members stay in control
Members see every connected app at https://api.aceversity.com/keys under Connected apps and can disconnect any of them; that revokes all of the app's tokens immediately.
Membership
Credentials belong to a member. If their subscription lapses, GET /v1/me still works (it reports membership.active: false) and every other call returns:
{ "error": { "type": "membership_required", "message": "This Aceversity account has no active membership. Subscribe or renew at https://aceversity.com/subscribe.", "request_id": "req_…" } }