Developer docs
Disfora uses standard OAuth 2.0 (authorization code + PKCE). Users sign in on Disfora, approve the scopes you ask for, and your app gets tokens to call the API as them.
Quickstart
- Sign in here and create an app: name, redirect URIs, and the scopes it may ask for.
- Send users to
/oauth/authorize. They come back to your redirect URI with acode. - Exchange the code at
/oauth/tokenfor an access token (15 min) and a refresh token (30 days). - Call the API with
Authorization: Bearer …. Refresh before the access token expires.
Scopes
Your app may only ask for scopes ticked in its settings. profile is always included.
| Scope | What the user sees |
|---|---|
profile | See your username, name and profile picture |
email | See your email address |
comments:write | Post, edit and delete your comments |
ratings:write | Rate pages as you |
notifications:read | Read your notifications |
2. Exchange the code (within 5 minutes, once)
POST https://disfora.com/oauth/token Content-Type: application/x-www-form-urlencoded (JSON works too) grant_type=authorization_code &code=CODE &redirect_uri=https://example.com/auth/disfora/callback &client_id=YOUR_CLIENT_ID &client_secret=YOUR_CLIENT_SECRET (or HTTP Basic; public apps omit it) &code_verifier=VERIFIER
{
"access_token": "dfa_…", "token_type": "bearer", "expires_in": 900,
"refresh_token": "dfr_…", "scope": "profile comments:write",
"user": {
"sub": "stable id for this user in your app",
"username": "…", "first_name": "…", "last_name": "…",
"avatar_url": "…", "profile_url": "https://disfora.com/u/…",
"email": "…", "email_verified": true (only with the email scope)
}
}
Key your users on sub: it never changes for your app and is different in every app. Keep tokens and the client secret on your server.
3. Call the API as the user
| Endpoint | Scope |
|---|---|
GET /oauth/userinfo | any |
POST /api/comments/ | comments:write |
PUT /api/comments/{comment_id} | comments:write |
DELETE /api/comments/{comment_id} | comments:write |
POST /api/ratings/ | ratings:write |
GET /api/ratings/my-rating | ratings:write |
DELETE /api/ratings/my-rating | ratings:write |
GET /api/notifications | notifications:read |
GET /api/notifications/unread-count | notifications:read |
POST https://disfora.com/api/comments/
Authorization: Bearer ACCESS_TOKEN
X-Project-Id: proj_… (the site's project key: comments and ratings)
X-Page-Origin: https://the-site.com (the site's origin: comments and ratings)
Content-Type: application/json
{"page_url": "https://the-site.com/post", "comment": "Hello", "parent_comment_id": null}
Reading a page's comments is public: GET /api/comments/?page_url=… with the same two headers, no token needed.
4. Refresh, and revoke on sign-out
POST https://disfora.com/oauth/token grant_type=refresh_token&refresh_token=dfr_…&client_id=…&client_secret=… POST https://disfora.com/oauth/revoke token=dfr_…&client_id=…&client_secret=… (always 200)
Every refresh returns a new refresh token: store it and drop the old one. Reusing a spent refresh token revokes all of that user's tokens for your app.
Rules
- Access tokens last 15 minutes, refresh tokens 30 days (renewed on each refresh).
- Codes are single use, valid 5 minutes, and bound to your client, redirect URI and PKCE verifier.
- Up to 600 requests a minute per app. Per-user limits, bans and moderation apply as normal.
- Tokens never carry moderator powers, even for a moderator's account.
- Users can disconnect your app from their profile; deleting your app revokes everything. Both take effect immediately.
- Removing a scope from your app removes it from every existing token too.
Errors
Bodies are {"detail": "…"}.
- 400 on authorize: unknown client, unregistered redirect URI, bad state, or a scope your app isn't registered for.
- 401 on token/revoke: wrong or missing client secret.
- 400
Invalid or expired refresh token: spent, expired or revoked. - 401 on the API: expired or revoked token, or an endpoint apps can't use.
- 403 +
WWW-Authenticate: Bearer error="insufficient_scope": the token lacks that endpoint's scope. - 429: your app's rate limit. Wait for
Retry-After.