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_…" } }