Authorization Flow

Register an app, drive a user through authorize and consent, and exchange the code for tokens.

Overview

The OAuth Apps flow has four steps:

  1. Register your app (once) → get client_id + client_secret.
  2. Authorize — show the connecting user what your app is requesting.
  3. Consent — the org admin approves; you receive a single-use code.
  4. Token exchange — swap the code (with your client secret) for an access token + refresh token.

All endpoints below are on https://api.featureos.app.

1. Register an app

Owner-org admin only. Authenticated with the user's session JWT + the X-Organization header.

POST https://api.featureos.app/oauth/apps
curl -sX POST https://api.featureos.app/oauth/apps \
  -H "Authorization: Bearer <USER_JWT>" \
  -H "X-Organization: your-org" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Integration",
    "redirect_uris": ["https://myapp.com/callback"],
    "allowed_actor_modes": ["app", "self"],
    "declared_scopes": ["posts:read", "posts:write"]
  }'

Each entry in redirect_uris must be an absolute https URI. http is accepted only for loopback hosts (localhost, 127.0.0.1, [::1]) to support local development.

The response includes the client_id and the client_secret (shown once).

2. Authorize

The connecting user (logged in to FeatureOS) is sent through the authorize endpoint, which returns the consent metadata (app details, requested scopes, and which orgs the user may install into).

GET https://api.featureos.app/oauth/authorize
curl -sG https://api.featureos.app/oauth/authorize \
  -H "Authorization: Bearer <USER_JWT>" \
  -H "X-Organization: target-org" \
  --data-urlencode "client_id=foapp_…" \
  --data-urlencode "redirect_uri=https://myapp.com/callback" \
  --data-urlencode "scope=posts:read posts:write" \
  --data-urlencode "actor=self"
Parameter Required Description
client_id true Your app's foapp_… id.
redirect_uri true Must exactly match one of the app's registered redirect_uris.
scope false Space-separated scopes; must be a subset of the app's declared_scopes. Omit to request all declared.
actor false app or self; must be one the app allows.

The org admin approves the install into a specific target organization. This mints a single-use authorization code.

POST https://api.featureos.app/oauth/authorize/consent
curl -sX POST https://api.featureos.app/oauth/authorize/consent \
  -H "Authorization: Bearer <USER_JWT>" \
  -H "X-Organization: target-org" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "foapp_…",
    "redirect_uri": "https://myapp.com/callback",
    "scope": "posts:read posts:write",
    "actor": "self",
    "target_organization_id": 7,
    "state": "xyz123"
  }'
# → { "redirect_to": "https://myapp.com/callback?code=fooc_…&state=xyz123" }
Parameter Required Description
target_organization_id true The org to install into. The caller must be an admin of it.
state false Opaque value echoed back on the redirect (CSRF protection).
granted_webhook_events false Restrict which webhook events this install grants (subset of the app's subscribed events). See Webhooks.

The browser is redirected to your redirect_uri with code (and state). The code is single-use and expires in 5 minutes.

4. Token exchange

Server-to-server, authenticated with your client credentials (no user session).

POST https://api.featureos.app/oauth/token
curl -sX POST https://api.featureos.app/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "client_id": "foapp_…",
    "client_secret": "fosec_…",
    "code": "fooc_…",
    "redirect_uri": "https://myapp.com/callback"
  }'
# → {
#   "access_token": "foot_…",
#   "refresh_token": "foor_…",
#   "token_type": "Bearer",
#   "expires_in": 86400,
#   "scope": "posts:read posts:write"
# }

The exchange verifies your client secret, that the code is unused/unexpired, and that the redirect_uri matches exactly. You now have a 24-hour access token — see Using Access Tokens and Refresh.