Using Access Tokens

Call the v3 API with a bearer token, refresh it when it expires, and handle revocation.

Calling the API

Send the access token as a bearer token on any supported v3 endpoint. No API-KEY header is needed — the organization is resolved from the token:

curl -s https://api.featureos.app/api/v3/feature_requests \
  -H "Authorization: Bearer foot_xxxxxxxxxxxxxxxx"

The request runs inside the token's target organization, as the token's actor, limited to its granted scopes.

Resolving the acting identity

To find out who your token acts as, call GET /api/v3/session_info (requires posts:read):

curl -s https://api.featureos.app/api/v3/session_info \
  -H "Authorization: Bearer foot_xxxxxxxxxxxxxxxx"
# → {
#     "success": true,
#     "user": { "id": 4521, "name": "Acme Integration", "email": "[email protected]", "profile_picture": null },
#     "actor": "app",
#     "organization": { "id": 88, "subdomain": "feedback", "name": "FeatureOS" }
#   }

For an app-actor token the user is your app's dedicated bot identity; for self it's the authorizing team member. Some endpoints attribute actions to this identity — e.g. creating a vote on behalf (POST /api/v3/votes_on_behalf). With an OAuth token you can omit by_email there and it defaults to this actor; if you do send it, use the user.email returned here.

Scope enforcement

Each endpoint requires a scope (e.g. listing posts needs posts:read, creating one needs posts:write). If the token lacks the required scope you get:

{ "message": "Missing required OAuth scope: posts:write", "status": 403, "success": false }

Endpoints that haven't been opened to OAuth at all also return 403 for OAuth tokens (default-deny).

Token expiry

Access tokens expire 24 hours after they're issued (expires_in: 86400). A request with an expired token returns 401. When that happens, get a new one with your refresh token rather than sending the user through authorization again.

Refreshing an access token

Use the refresh_token grant (server-to-server, with your client credentials):

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

Refresh is a rotation: the install keeps the same connection, but the access token and refresh token are both replaced. The old access token and old refresh token stop working immediately — always store the newest pair from each refresh response.

A refresh token is valid until the install is revoked; it does not expire on its own. If a refresh returns invalid_grant, the install was revoked — restart the authorization flow.

Revocation

An admin of the target organization can disconnect your app at any time, and the connection ends when:

  • the org admin revokes the app's token, or
  • the owner suspends the app, or
  • a self-actor user's membership is removed.

After revocation, both the access and refresh tokens fail (401 / invalid_grant). Your app should treat this as a disconnect and re-request authorization if the user wants to reconnect.

Error reference

Status When
401 Missing, invalid, expired, or revoked access token.
403 Token is valid but lacks the required scope for the endpoint.
400 invalid_client Wrong client_id/client_secret, or the app is suspended.
400 invalid_grant Bad/expired/used code, or an invalid/revoked refresh token.