ServerAvatarDocs
API Reference

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 whenHow
Bearer tokenScripts, CI, another server, anything that is not a browser.Authorization: Bearer <token>
Cookie sessionA 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:

EventEffect
POST /auth/logoutRevokes the token used to make that call only. Other tokens for the same user keep working.
PUT /auth/passwordRevokes every other token for that user and returns a fresh one in {"token": "…"}. A bearer client must switch to it.
ExpiryAfter 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.

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.

curl -i https://panel.example.com/sanctum/csrf-cookie

Responds 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. manage implies view.
  • Everything under /admin/* additionally needs the access-admin permission.
  • 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:

LimitKey
5 / minutethis username and this IP address
20 / minutethis IP address, across all usernames
10 / minutethis 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

StatusBodyMeans
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:

EndpointWhy it is open
GET /healthA 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-infoWhat a client needs before it can log in — see below.
GET /brandingName, logos, favicon and primary_color, so the sign-in screen can brand itself before anyone has signed in.
POST /auth/loginWhere credentials are exchanged for one.
POST /auth/registerOnly 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/logout with the old one revokes it immediately.
  • Account activity, including sign-ins, is recorded in the activity log.

On this page