Authentication
Get a bearer token, send it, know when it expires — and the cookie-session alternative for browser clients.
Nearly every endpoint in the OSS Panel API requires an authenticated caller. There are two ways to be one, and which you pick depends on what is calling:
| Use when | How | |
|---|---|---|
| Bearer token | Scripts, CI, another server, anything that is not a browser. | Authorization: Bearer <token> |
| Cookie session | A browser app served from a host the panel trusts. | Session cookie + CSRF header |
Both resolve to the same user and the same permissions. The panel's own frontend uses cookies; everything else should use a token.
Base URL
All paths below are relative to https://<your-panel-host>/api. The
installer prints the host; it is the same address you sign in at.
Bearer tokens
Getting one
POST /auth/login with a username and password returns a token:
curl -X POST https://panel.example.com/api/auth/login \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"username": "admin", "password": "your-password"}'{
"user": {
"id": 1,
"username": "admin",
"is_admin": true,
"roles": [{ "id": 1, "name": "Administrator", "slug": "administrator" }],
"created_at": "23-07-2026 10:00:00",
"created_at_human": "3 weeks ago"
},
"token": "1|xcMq8K3vZ2pL…"
}The token is the token field verbatim, including the <id>| prefix. It is
shown once and not stored in retrievable form — the panel keeps only a hash,
so there is no endpoint that will tell you a token you have lost. Get a new
one by logging in again.
On a brand-new panel there is no account to log in with yet. The first
administrator is created through POST /auth/register, which also returns a
token; see First login for the
normal, browser-based version of that step.
Registration closes permanently
POST /auth/register only works while the panel holds no real user account.
After the first administrator exists it answers 403 with
"Registration is closed. An administrator already exists; ask an admin to
create your account." Later accounts are created under
Administration → Users.
Sending one
curl https://panel.example.com/api/auth/me \
-H 'Authorization: Bearer 1|xcMq8K3vZ2pL…' \
-H 'Accept: application/json'Every /api/* route answers JSON whatever you ask for, so Accept: application/json is good manners rather than a requirement — but send it
anyway, so a future proxy or error page in front of the panel cannot hand your
client HTML.
Add Accept-Language: de (or any of the eight
supported locales) to get error
messages and permission titles in that language.
Lifetime
A token issued by login, registration or a password change expires 10 days
later. That window is set per panel by TOKEN_EXPIRATION_DAYS in the
backend's .env, so a long-lived integration on a panel you control can be
given a longer one.
An expired token is indistinguishable from a wrong one — both answer
401 Unauthenticated. A client that runs unattended should treat any 401 as
"log in again", not as a fatal error.
Tokens carry full * abilities: a token can do anything its user can do.
Scoping is done through the user's role, not the token.
Losing one
Three things end a token's usefulness:
| Event | Effect |
|---|---|
POST /auth/logout | Revokes the token used to make that call only. Other tokens for the same user keep working. |
PUT /auth/password | Revokes every other token for that user and returns a fresh one in {"token": "…"}. A bearer client must switch to it. |
| Expiry | After TOKEN_EXPIRATION_DAYS. |
The password-change behaviour is the one that surprises people: changing your own password in the panel's UI silently invalidates the token your deploy script has been using. Rotate it at the same time.
Cookie sessions
A browser app can authenticate without handling a token at all, which keeps
credentials out of JavaScript-reachable storage. It only works from a host
listed in SANCTUM_STATEFUL_DOMAINS, which defaults to the panel's own URL
plus localhost.
Get a CSRF cookie
curl -i https://panel.example.com/sanctum/csrf-cookieResponds 204 and sets XSRF-TOKEN and laravel-session cookies. Both are
Secure and scoped to the panel's domain.
Log in with the cookies attached
POST /auth/login with the same body as above, plus the XSRF-TOKEN cookie
value — URL-decoded — in an X-XSRF-TOKEN header. On success the session
cookie becomes an authenticated one.
Send credentials with the request (credentials: 'include' in fetch, or
withCredentials: true in axios).
Call the API
Later requests need the session cookie and, for anything that is not a GET,
the X-XSRF-TOKEN header. No Authorization header is involved.
Login also returns a token in the cookie flow. A browser client can ignore
it. POST /auth/logout invalidates the session and regenerates the CSRF
token.
Knowing who you are
GET /auth/me is the cheapest way to confirm a credential works and to read
the caller's roles:
{
"user": {
"id": 1,
"username": "admin",
"is_admin": true,
"roles": [{ "id": 1, "name": "Administrator", "slug": "administrator" }],
"created_at": "23-07-2026 10:00:00",
"created_at_human": "3 weeks ago"
},
"impersonated_by": null
}impersonated_by is {id, username} when an administrator is currently
viewing the panel as this user, and null otherwise — see
Impersonation. It is set by
the session, so it is always null for a bearer client.
What a credential is allowed to do
Authentication proves who; permissions decide what. Being authenticated is never enough on its own:
- Read endpoints need the feature's view grant; anything that changes
state needs manage.
manageimpliesview. - Everything under
/admin/*additionally needs theaccess-adminpermission. - There is no administrator bypass. Administrators pass these checks because they hold the Administrator role, which grants everything — not because the code exempts them.
A refusal names the missing grant rather than failing blankly, so a 403
message is worth surfacing to the user:
{ "message": "Your role does not include management for Applications." }The feature is named by its localised title, so this sentence follows
Accept-Language too; the ability reads either view access or management.
A handful of refusals that are not permission checks fall back to
"Your role does not allow this action."
Roles and the full permission catalogue are documented under Roles & permissions.
Rate limits
Sign-in is throttled on three keys at once, all of them applied — so changing the username does not buy a fresh budget, and a password list spread across many machines still cannot grind one account:
| Limit | Key |
|---|---|
| 5 / minute | this username and this IP address |
| 20 / minute | this IP address, across all usernames |
| 10 / minute | this username, across all IP addresses |
Authenticated requests draw on a shared budget of 180 per minute per user
(RATE_LIMIT_API). Unauthenticated requests get 20 per minute per IP
(RATE_LIMIT_GUEST). Progress-polling endpoints — provisioning, deployments,
sync runs — are exempt from that budget and have their own, much larger one,
because they are meant to be polled while a job runs.
Some individual endpoints carry an extra limit of their own on top of that budget — the lower of the two always wins. A per-endpoint limit counts that endpoint only, per user: requests for two different backups both spend the same counter.
Over any limit the answer is 429. Respect the Retry-After header rather
than retrying immediately — a retry inside the window spends the next one too.
Error responses
| Status | Body | Means |
|---|---|---|
401 | {"message": "Unauthenticated."} | No credential, or it is wrong, revoked or expired. |
403 | {"message": "…"} | Authenticated, but not permitted. The message names what is missing. |
404 | {"message": "Not Found."} | No such resource — or a feature that does not exist for this site type. |
422 | {"message": "…", "errors": {"field": ["…"]}} | Validation failed, including wrong credentials on login. |
429 | {"message": "Too many attempts. Try again in 42 seconds."} | Rate limited. The same number is in the Retry-After header. |
500 | {"message": "…", "reference": "…"} | Server-side failure. reference locates the entry in the error log. |
Wrong credentials are a 422 on the username field, not a 401:
{
"message": "These credentials do not match our records.",
"errors": { "username": ["These credentials do not match our records."] }
}Endpoints that need no credential
Five, and they are deliberate:
| Endpoint | Why it is open |
|---|---|
GET /health | A liveness probe. An update calls it on localhost after switching releases, when there is no session to present. Reports status and version, nothing else. |
GET /basic-info | What a client needs before it can log in — see below. |
GET /branding | Name, logos, favicon and primary_color, so the sign-in screen can brand itself before anyone has signed in. |
POST /auth/login | Where credentials are exchanged for one. |
POST /auth/register | Only while the panel has no account at all. |
POST /webhooks/deploy/{identifier} is a sixth, but not an open one: GitHub,
GitLab and Bitbucket carry no token, so an HMAC signature over the raw request
body is the credential instead. That belongs with
Git deployment rather than here —
nothing on this page applies to it.
Reading the panel's own policy
GET /basic-info needs no credential and reports what a client needs to know
before it can authenticate at all — useful for a sign-up form that would
otherwise hardcode its own description of the password rules and watch them
drift:
{
"basic_info": {
"registration_open": false,
"app_version": "7.0.18",
"locales_available": ["en", "es", "de", "fr", "pt", "ja", "ru", "hi"],
"cookie_auth_enabled": true,
"password_policy": {
"min_length": 10,
"requires_mixed_case": true,
"requires_number": true,
"requires_symbol": false
}
}
}Handling tokens safely
- A token is a password with full account privileges. Keep it in an environment variable or a secret store — never in a repository, a URL query string, or a log line.
- Give an integration its own panel user with a role scoped to what it
actually does. A deploy script does not need
access-admin. - The panel must be reached over HTTPS. A bearer token sent over plain HTTP is readable by anything on the path.
- Rotate by logging in again and discarding the old token;
POST /auth/logoutwith the old one revokes it immediately. - Account activity, including sign-ins, is recorded in the activity log.