Every URL here is on the tenant’s own host —
acme.orgo.space in these examples, or the organization’s custom domain. The bare app.orgo.space domain resolves no tenant and returns 404. See Tenancy.When to use OAuth vs Api-Token
- Api-Token — your server acts as itself; the data is yours. Use for backend integrations, dashboards, batch jobs.
- OAuth — your server acts on behalf of an Orgo user; you only see what that user can see. Use when each end-user logs into your app as themselves.
Prerequisites
- A callback URL on your app served over HTTPS (localhost is allowed for development).
- For a confidential client: an admin in the Orgo tenant who can create the app.
Step 0 — Read the discovery document
Don’t hardcode endpoints. Every tenant publishes its own metadata, andissuer reflects the host you fetch it from:
authorization_endpoint, token_endpoint and userinfo_endpoint from the response. Fetch this once per tenant you integrate with — never reuse one tenant’s document for another.
Step 1 — Register your client
- Public client (self-service)
- Confidential client (admin)
Best for SPAs, mobile apps, and anything that cannot keep a secret. No admin involvement and no authentication needed, but limited to 10 registrations per hour per IP:Returns
201 with a client_id. No secret is issued — PKCE is your only client authentication. Optional metadata: client_uri, logo_uri, policy_uri, tos_uri.Step 2 — Redirect the user to authorize
When a user clicks “Log in with Orgo” in your app, redirect them to:
If the member isn’t signed in, Orgo handles login and returns them to the consent screen automatically — including via Google, Apple or Microsoft. You don’t handle that.
Step 3 — Receive the callback
Orgo sends the user back to yourredirect_uri with two parameters:
state matches the one you stored, then move on.
Step 4 — Exchange the code for an access token
Form-encoded body, not JSON. A confidential client does this server-side — the Client Secret must never reach the browser.- Public client
- Confidential client
PKCE only. Sending a
client_secret here is an error (invalid_client):Step 5 — Use the access token
The access token works as a normal Bearer JWT.sub is the user’s stable Orgo ID — use this as the primary key in your app’s users table. Email can change; sub cannot.
You can also use the same access token to call other API endpoints scoped to the user’s permissions:
Step 6 — Refresh expired tokens
Access tokens last 1 hour (expires_in in the original response). Refresh tokens last 30 days. Use the refresh token to get a new pair without sending the user back through the consent screen:
401.
Step 7 — Know what your token cannot do
Two limits catch most integrations, and neither is adjustable per request. An app acts as a member, never as an administrator. The token is capped to member level (ROLE_USER) no matter who authorized it. If a tenant administrator connects your app, it still cannot perform administrator actions.
Read-only apps are gated by HTTP method. If the app is registered read-only it may use GET, HEAD and OPTIONS only. Anything else returns 403:
POST is still blocked. If you need those, register the app with acting enabled.
Available scopes
Scopes describe what an application requests and what the member approves when they authorize it. What the resulting token can actually reach is bounded by the member-level cap and the application’s read-only / act-on-behalf setting in step 7.
Common gotchas
Token exchange returns `invalid_client`
Token exchange returns `invalid_client`
Wrong
client_id, wrong client_secret, or a confidential client omitting its secret entirely (that one returns 401 with WWW-Authenticate: Basic realm="orgo-oauth"). A public client sending a client_secret also fails — it has none, and PKCE is its only authentication.Note that sending client_secret and code_verifier together is correct for a confidential client: PKCE applies to every client. What is not allowed is sending credentials by more than one method at once — Basic header and form field — which returns invalid_request.Token exchange returns `429 rate_limit_exceeded`
Token exchange returns `429 rate_limit_exceeded`
Repeated failed client authentications for the same
client_id are throttled: after 10 failures in 5 minutes you get 429 with a Retry-After header instead of 401. This means your credentials are wrong, not that you are calling too often — a request with the correct secret is never throttled, however many failures came before it. Fix the secret rather than backing off.The user's groups don't match what I see in the Orgo UI
The user's groups don't match what I see in the Orgo UI
The
groups scope returns slug-style group names, not display names. To resolve to a full Group resource, call GET /api/v1/units?slug=boston-local separately.My app is a SPA — can I skip the backend entirely?
My app is a SPA — can I skip the backend entirely?
Use PKCE without a Client Secret. Generate a verifier (random 43-128 chars), hash it with SHA-256, base64url-encode the hash, send the hash as
code_challenge on authorize, then send the verifier as code_verifier on token exchange. The Client Secret stays unused.A token that looks valid is being rejected
A token that looks valid is being rejected
Access tokens are RS256 JWTs, but they are revocable server-side and revocation takes effect on the next request. A token whose signature verifies and whose
exp is still in the future can still be rejected.Do not treat a local expiry check as proof a token works — handle 401 at any point in a token’s lifetime and fall back to refresh, then to re-authorization. Verify signatures against jwks_uri from the discovery document.What to do next
- Authentication — comparison of OAuth vs Api-Token vs JWT vs OTP, and the full discovery reference
- Migrating from legacy SSO — if you already use the
*-ssohandshake - OAuth Server — admin-side documentation on managing OAuth applications
- Tenancy — how OAuth tokens carry tenant scope

