{"openapi":"3.1.0","info":{"title":"Autodemia API","version":"1.0.0","summary":"Sign in with Autodemia, and read what a person's public profile shows.","description":"Other apps connect a person's Autodemia account with OAuth 2.0 (authorization code, PKCE, refresh tokens) and read what their public profile shows: activity grid, streaks, public pursuits, badges and recently mastered concepts. An app never sees more than a stranger would on https://autodemia.io/u/<handle>. Full guide: https://autodemia.io/developers","contact":{"name":"Autodemia","email":"hello@autodemia.io","url":"https://autodemia.io/developers"},"termsOfService":"https://autodemia.io/privacy"},"externalDocs":{"description":"Developer guide","url":"https://autodemia.io/developers"},"servers":[{"url":"https://autodemia.io"}],"tags":[{"name":"OAuth","description":"Connecting an account and getting tokens"},{"name":"Profile","description":"What a connected app reads"}],"paths":{"/oauth/authorize":{"get":{"tags":["OAuth"],"operationId":"authorize","summary":"Send the person here to connect","description":"A page, not an API call: open it in the person's browser. They sign in if needed, see what your app will see, and press Allow. Autodemia then redirects to redirect_uri with ?code=…&state=… (or ?error=access_denied). Someone who already allowed every scope you ask for goes straight back. A bad client_id or an unregistered redirect_uri is shown on the page and never redirected.","parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string","pattern":"^adc_"}},{"name":"redirect_uri","in":"query","required":false,"description":"Must match a registered redirect URL exactly. Optional only when the app has exactly one.","schema":{"type":"string","format":"uri"}},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","const":"code"}},{"name":"scope","in":"query","required":false,"description":"Space separated. Unknown scopes are ignored; profile is always included.","schema":{"type":"string","examples":["activity pursuits badges latest"]}},{"name":"state","in":"query","required":false,"description":"Returned unchanged. Use it against CSRF (up to 500 characters).","schema":{"type":"string","maxLength":500}},{"name":"code_challenge","in":"query","required":false,"description":"PKCE: base64url(SHA-256(code_verifier)), 43 characters. Recommended for every app.","schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{43}$"}},{"name":"code_challenge_method","in":"query","required":false,"schema":{"type":"string","const":"S256"}}],"responses":{"200":{"description":"The consent page"},"302":{"description":"Back to redirect_uri with code and state, or with error (access_denied, invalid_request, unsupported_response_type), error_description and state"}}}},"/oauth/token":{"post":{"tags":["OAuth"],"operationId":"token","summary":"Trade a code, or refresh tokens","description":"Send your client credentials as HTTP Basic or in the body. A code is good once, for ten minutes. An access token lasts 3600 seconds. Every refresh returns a new pair: store the new refresh token, because using an old one again ends every token for that connection. Up to 300 calls a minute per app.","security":[{"clientBasic":[]},{}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/TokenRequest"}},"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}},"responses":{"200":{"description":"Tokens","headers":{"Cache-Control":{"schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"invalid_request, invalid_grant or unsupported_grant_type","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client: unknown app or wrong secret","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Too many calls","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpError"}}}}}}},"/oauth/revoke":{"post":{"tags":["OAuth"],"operationId":"revoke","summary":"Revoke a token (RFC 7009)","description":"Call this when someone unlinks Autodemia on your side. Send an access or refresh token. Unknown or already revoked tokens still return 200.","security":[{"clientBasic":[]},{}],"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/RevokeRequest"}},"application/json":{"schema":{"$ref":"#/components/schemas/RevokeRequest"}}}},"responses":{"200":{"description":"Revoked (or nothing to revoke)","content":{"application/json":{"schema":{"type":"object"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/.well-known/oauth-authorization-server":{"get":{"tags":["OAuth"],"operationId":"metadata","summary":"OAuth server metadata (RFC 8414)","responses":{"200":{"description":"Endpoints, scopes and supported methods","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/api/v1/me":{"get":{"tags":["Profile"],"operationId":"me","summary":"The connected person's public profile","description":"What the person's public profile shows, cut to the scopes they allowed. With the profile off (or no username yet) only user.id, handle, name, public: false and url come back. 60 calls a minute per connection.","security":[{"oauth":["profile"]},{"bearer":[]}],"parameters":[{"name":"year","in":"query","required":false,"description":"A calendar year for the activity grid instead of the last twelve months. dayStreak is null when set.","schema":{"type":"integer","minimum":2000,"maximum":9999}}],"responses":{"200":{"description":"The profile","headers":{"Cache-Control":{"schema":{"type":"string","const":"private, no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"}}}},"401":{"description":"Missing, expired or revoked token, or the person disconnected your app","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpError"}}}},"429":{"description":"More than 60 calls a minute for this connection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HttpError"}}}}}}}},"components":{"securitySchemes":{"oauth":{"type":"oauth2","flows":{"authorizationCode":{"authorizationUrl":"https://autodemia.io/oauth/authorize","tokenUrl":"https://autodemia.io/oauth/token","refreshUrl":"https://autodemia.io/oauth/token","scopes":{"profile":"Your name, @username, photo, bio and website (always included)","activity":"Your activity grid, hours studied and streak, as numbers","pursuits":"Your public pursuits and your progress in them","badges":"The badges you've earned","latest":"Concepts you recently got solid or mastered, from public pursuits"}}}},"bearer":{"type":"http","scheme":"bearer","description":"An access token (ada_…)"},"clientBasic":{"type":"http","scheme":"basic","description":"client_id and client_secret"}},"schemas":{"TokenRequest":{"type":"object","required":["grant_type"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"code":{"type":"string","description":"authorization_code only"},"redirect_uri":{"type":"string","description":"authorization_code only; if sent, must be the one used at /oauth/authorize"},"code_verifier":{"type":"string","minLength":43,"maxLength":128,"description":"authorization_code only; required when a code_challenge was sent"},"refresh_token":{"type":"string","description":"refresh_token only (adr_…)"},"client_id":{"type":"string","description":"Unless sent as HTTP Basic (adc_…)"},"client_secret":{"type":"string","description":"Unless sent as HTTP Basic (ads_…)"}}},"TokenResponse":{"type":"object","required":["access_token","token_type","expires_in","refresh_token","scope"],"properties":{"access_token":{"type":"string","examples":["ada_…"]},"token_type":{"type":"string","const":"Bearer"},"expires_in":{"type":"integer","examples":[3600]},"refresh_token":{"type":"string","examples":["adr_…"]},"scope":{"type":"string","examples":["profile activity pursuits badges latest"]}}},"RevokeRequest":{"type":"object","required":["token"],"properties":{"token":{"type":"string"},"token_type_hint":{"type":"string","enum":["access_token","refresh_token"],"description":"Accepted and ignored"},"client_id":{"type":"string"},"client_secret":{"type":"string"}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","enum":["invalid_request","invalid_client","invalid_grant","unsupported_grant_type"],"description":"RFC 6749 §5.2 error code"},"error_description":{"type":"string","description":"A human-readable explanation"}}},"HttpError":{"type":"object","properties":{"error":{"type":"boolean","const":true},"url":{"type":"string"},"statusCode":{"type":"integer","examples":[401]},"statusMessage":{"type":"string"},"message":{"type":"string"}}},"User":{"type":"object","required":["id","handle","name","public","url"],"properties":{"id":{"type":"string","description":"Stable for this person and your app, and different for every other app. Link accounts with this, not the handle."},"handle":{"type":["string","null"],"description":"Their @username, if they picked one. It can change."},"name":{"type":"string"},"public":{"type":"boolean","description":"Whether their public profile is on. When false, nothing else comes back."},"url":{"type":["string","null"],"format":"uri"},"avatar":{"type":["string","null"],"format":"uri"},"bio":{"type":["string","null"]},"website":{"type":["string","null"],"format":"uri"},"joinedAt":{"type":"integer","format":"int64","description":"When they joined (epoch milliseconds)"}}},"Day":{"type":"object","properties":{"day":{"type":"string","format":"date"},"count":{"type":"integer","description":"Answers, reviews, lessons, practice and questions that day"},"level":{"type":"integer","minimum":0,"maximum":4,"description":"Shade, relative to their own busy days"}}},"Activity":{"type":"object","properties":{"from":{"type":"string","format":"date"},"to":{"type":"string","format":"date"},"year":{"type":["integer","null"]},"years":{"type":"array","items":{"type":"integer"},"description":"Years with any activity, for a year picker"},"total":{"type":"integer"},"last7Days":{"type":"integer"},"activeDays":{"type":"integer"},"hours":{"type":"number","description":"Study time"},"dayStreak":{"type":["integer","null"],"description":"Days in a row up to today; null when ?year is set"},"weekStreak":{"type":"integer","description":"Weeks in a row meeting their weekly goal"},"conceptsLearned":{"type":"integer"},"conceptsMastered":{"type":"integer"},"days":{"type":"array","items":{"$ref":"#/components/schemas/Day"}}}},"Pursuit":{"type":"object","properties":{"title":{"type":"string"},"goal":{"type":["string","null"]},"readiness":{"type":"number","minimum":0,"maximum":1},"concepts":{"type":"integer"},"solid":{"type":"integer"},"mastered":{"type":"integer"},"lastActive":{"type":["integer","null"],"format":"int64","description":"Last studied (epoch milliseconds)"},"startedAt":{"type":"integer","format":"int64","description":"Started (epoch milliseconds)"},"url":{"type":["string","null"],"format":"uri"}}},"Badge":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"blurb":{"type":"string"},"level":{"type":"integer"},"label":{"type":"string"},"earnedAt":{"type":"integer","format":"int64","description":"Earned (epoch milliseconds)"}}},"Latest":{"type":"object","properties":{"concept":{"type":"string"},"pursuit":{"type":"string"},"level":{"type":"string","enum":["Solid","Mastered"]},"at":{"type":"integer","format":"int64","description":"When (epoch milliseconds)"}}},"Me":{"type":"object","required":["user","scopes"],"properties":{"user":{"$ref":"#/components/schemas/User"},"scopes":{"type":"array","items":{"type":"string","enum":["profile","activity","pursuits","badges","latest"]}},"activity":{"$ref":"#/components/schemas/Activity","description":"With the activity scope"},"pursuits":{"type":"object","description":"With the pursuits scope","properties":{"list":{"type":"array","items":{"$ref":"#/components/schemas/Pursuit"}},"privateCount":{"type":"integer","description":"How many pursuits they keep private (never their names)"}}},"badges":{"type":"array","items":{"$ref":"#/components/schemas/Badge"},"description":"With the badges scope"},"latest":{"type":"array","maxItems":10,"items":{"$ref":"#/components/schemas/Latest"},"description":"With the latest scope: newest first, public pursuits only"}}}}}}