# 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.

Source: https://featherpanel.com/docs/api/oauth2/

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](https://featherpanel.com/docs/api/authentication/). 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:

```text
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

- `https` URLs are always allowed.
- `http` is only allowed for `localhost` and `127.0.0.1`, except in `server` mode.
- Custom schemes such as `myapp://` are allowed, for desktop and mobile apps.
- `javascript:`, `data:`, `file:` and `vbscript:` are rejected with `INVALID_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:

```text
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:

```bash
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:

```bash
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:

```bash
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

- [API authentication](https://featherpanel.com/docs/api/authentication/)
- [Rate limits and errors](https://featherpanel.com/docs/api/rate-limits-and-errors/)
- [Account API endpoints](https://featherpanel.com/docs/api/reference/user/api-clients/)

## Frequently asked questions

### Does FeatherPanel issue OAuth access tokens?

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.

### How long does a device code stay valid?

900 seconds. Poll the token endpoint no faster than every 5 seconds or the panel answers slow_down.

### Can the user revoke an app later?

Yes. The approved app shows up as an API key on the user's account. Deleting that key revokes the app.
