OAuth2 app authorization
How a third-party app asks a FeatherPanel user for an API key through the consent screen, the device flow for CLIs, callback modes and the error codes.
On this page
FeatherPanel has a consent flow so an app can ask a user for API access without the user copying keys by hand. It is OAuth2 in shape (a consent screen, an authorization code, a device flow), but what the app receives is an ordinary FeatherPanel API key. There are no scopes and no refresh tokens: the key can do what the user can do, and it lives until the user deletes it.
Browser flow
Send the user to the consent page on the panel with the request in the query string:
https://panel.example.com/dashboard/account/oauth2/api/new
?name=My%20Bot
&callbackurl=https://app.example.com/featherpanel/callback
&appName=My%20Bot
&appLogo=https://app.example.com/logo.png
&description=Reads%20server%20status
&allowedips=203.0.113.10
&mode=user
| Parameter | Required | Meaning |
|---|---|---|
name | yes | Name of the API key that will be created, up to 191 characters. |
callbackurl | yes | Where the result goes. See the rules below. |
appName | no | App name shown on the consent screen, up to 191 characters. |
appLogo | no | Logo URL shown on the consent screen. Must be a valid URL. |
description | no | Stored as the key description. |
allowedips | no | IP allow list for the new key, same format as the API keys page. |
alertCors | no | true turns on the foreign IP e-mail alert for the key. |
mode | no | user (default) or server. |
The user signs in if needed, sees the app name and logo, and approves or denies. A consent request stays valid for 24 hours.
Callback URL rules
httpsURLs are always allowed.httpis only allowed forlocalhostand127.0.0.1, except inservermode.- Custom schemes such as
myapp://are allowed, for desktop and mobile apps. javascript:,data:,file:andvbscript:are rejected withINVALID_CALLBACK_URL.
What the app receives
In user mode the browser is redirected to the callback URL with the result in the fragment, so it never reaches
web server logs:
https://app.example.com/featherpanel/callback#public_key=...&private_key=...&token_type=featherpanel_api_key&issued_at=2026-10-23T08%3A00%3A00%2B00%3A00&authorization_code=fpoauthcode_...
If the user denies, the fragment carries error=access_denied instead.
In server mode the panel does not redirect. It sends a JSON POST to the callback URL with success,
token_type, public_key, private_key, authorization_code, issued_at, app_name and request_name, with a
15 second timeout. If that delivery fails, the user sees SERVER_CALLBACK_DELIVERY_FAILED (502).
Exchange the authorization code
The callback also carries an authorization_code. A backend that only received the code can trade it for the key
once:
curl -X POST https://panel.example.com/api/user/api-clients/oauth2/token \
-H "Content-Type: application/json" \
-d '{"code":"fpoauthcode_..."}'
The response data holds token_type, public_key and private_key. A second exchange of the same code returns
AUTHORIZATION_CODE_USED; an unknown or expired code returns INVALID_AUTHORIZATION_CODE.
Device flow
For CLIs and devices without a browser, start a device authorization:
curl -X POST https://panel.example.com/api/user/api-clients/oauth2/device \
-H "Content-Type: application/json" \
-d '{"name":"My CLI","appName":"My CLI"}'
The body accepts name (required), appName, appLogo, description, allowedips and alertCors. The
response contains:
| Field | Meaning |
|---|---|
device_code | Secret the app polls with. |
user_code | Short code the user types in. |
verification_uri | /dashboard/account/oauth2/api/device on your panel. |
verification_uri_complete | The same page with the code filled in. |
expires_in | 900 seconds. |
interval | 5 seconds between polls. |
Show the user the user_code and the link, then poll:
curl -X POST https://panel.example.com/api/user/api-clients/oauth2/device/token \
-H "Content-Type: application/json" \
-d '{"device_code":"fpdev_..."}'
| Result | Meaning |
|---|---|
authorization_pending | The user has not answered yet. Keep polling. |
slow_down | You polled faster than every 5 seconds. |
access_denied | The user denied the request. |
expired_token | The code expired. Start again. |
| success | data has public_key, private_key and issued_at. |
Revoking access
Every approved request creates an API key named after the request on the user’s account, and the approval is recorded in the account activity. The user removes access by deleting that key on the API keys page.
Related
Frequently asked questions
No. When the user approves, the panel creates a normal API key (public and private key) on their account and hands it to the app. The app then sends the private key as a Bearer token.
900 seconds. Poll the token endpoint no faster than every 5 seconds or the panel answers slow_down.
Yes. The approved app shows up as an API key on the user's account. Deleting that key revokes the app.