Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

OAuth for client developers

Use the authorization-code flow to connect a member’s account. The member signs in and approves access on Plamenu; your client receives a bearer token for API requests.

Use the server’s HTTPS hosting domain for every endpoint below. On a split-domain installation this may differ from the domain in account handles. Replace the example domains and uppercase placeholders with your own values.

Register your application

Registration does not require authentication. Register separately on each server:

curl --request POST https://social.example.com/api/v1/apps \
  --data-urlencode 'client_name=Example client' \
  --data-urlencode 'redirect_uris=https://client.example/callback' \
  --data-urlencode 'scopes=profile read:statuses write:statuses'

Save client_id and client_secret from the JSON response. The secret is returned only at creation. Plamenu requires both values in the POST body when exchanging a code or revoking a token, including when using PKCE. HTTP Basic client authentication and exchanges without a client secret are not supported.

redirect_uris accepts newline-separated URIs in a form request or an array in a JSON request. Use an HTTPS callback for a web client; native clients can register a custom URI scheme. Redirect URIs must match exactly at authorization and token exchange, including any path, port, and query string. Fragments are not allowed.

For a manual command-line flow, register urn:ietf:wg:oauth:2.0:oob as the redirect URI. Plamenu then displays the code in the browser for the member to copy.

Request authorization

Generate a fresh PKCE verifier and state for each attempt. Keep both in the client’s pending login session. PKCE is optional for compatibility with older clients; when used, its only supported method is S256.

This Python example generates the values and authorization URL:

import base64
import hashlib
import secrets
from urllib.parse import urlencode

verifier = secrets.token_urlsafe(32)
challenge = base64.urlsafe_b64encode(
    hashlib.sha256(verifier.encode("ascii")).digest()
).rstrip(b"=").decode("ascii")
state = secrets.token_urlsafe(32)

parameters = {
    "response_type": "code",
    "client_id": "CLIENT_ID",
    "redirect_uri": "https://client.example/callback",
    "scope": "profile read:statuses write:statuses",
    "state": state,
    "code_challenge": challenge,
    "code_challenge_method": "S256",
}
authorization_url = "https://social.example.com/oauth/authorize?" + urlencode(parameters)
print("PKCE verifier:", verifier)
print("State:", state)
print("Authorization URL:", authorization_url)

Open that URL in the member’s browser. After approval, Plamenu redirects to your callback with code and the original state. Reject a callback with missing or mismatched state. If the member declines, the callback contains error=access_denied and state.

Scopes are space-separated. Request only what the client needs, within the scopes registered for the application. This example uses profile to identify the signed-in account, read:statuses to read timelines, and write:statuses to publish posts. Add write:media to both requests if the client uploads files. A broad read or write grant covers its granular scopes; a granular grant covers only that resource. Omitting scope at authorization requests read, which must be covered by the registration; it does not select all registered scopes.

Exchange the code

Codes expire after ten minutes and can be used only once. Send the verifier from the same authorization attempt, and the same redirect URI:

curl --request POST https://social.example.com/oauth/token \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'client_id=CLIENT_ID' \
  --data-urlencode 'client_secret=CLIENT_SECRET' \
  --data-urlencode 'redirect_uri=https://client.example/callback' \
  --data-urlencode 'code=AUTHORIZATION_CODE' \
  --data-urlencode 'code_verifier=PKCE_VERIFIER'

The response contains access_token, token_type (Bearer), scope (the granted space-separated scopes), and created_at (Unix seconds). There is no refresh_token or expires_in: API tokens have no fixed expiry. They can still be revoked, and account or server restrictions can prevent their use.

Use and revoke the token

Set ACCESS_TOKEN in your environment and send it in the Authorization header:

curl https://social.example.com/api/v1/accounts/verify_credentials \
  --header "Authorization: Bearer $ACCESS_TOKEN"

This returns the member’s account, including its id and username. Store tokens in the client’s credential storage and keep them out of URLs and logs.

Revoke the token when disconnecting the account:

curl --request POST https://social.example.com/oauth/revoke \
  --data-urlencode 'client_id=CLIENT_ID' \
  --data-urlencode 'client_secret=CLIENT_SECRET' \
  --data-urlencode 'token=ACCESS_TOKEN'

A successful request returns HTTP 200 with {}, including when the token is already revoked or unknown. Members can also revoke access in their account settings.

Other grants and errors

/oauth/token also accepts grant_type=client_credentials with client_id, client_secret, and an optional scope. It creates an application token with no member attached, so it cannot read a member’s home timeline or post on their behalf. An omitted scope uses the application’s registered scopes. Password and refresh-token grants are not supported.

ErrorWhat to check
invalid_clientSend the client ID and secret from this server in the POST body.
invalid_scopeRequested scopes must be recognized and covered by the application’s registration.
invalid_grantCheck code age, exact redirect URI, application, and PKCE verifier. Start a new authorization attempt; a failed exchange may have consumed the code.
unsupported_grant_typeUse authorization_code or client_credentials.

For API requests, a 401 can mean the token is no longer valid; let the member authorize again. A 403 can mean insufficient scopes, account restrictions, or server policy. Check the response’s error field before retrying.