Authentication

Authentication API
Cliqer authenticates with an httpOnly session cookie (session_token), minted by POST /api/auth/login. Browsers send it automatically on every request; server-to-server tooling keeps it in a cookie jar. Studio and Enterprise teams can additionally mint API keys for server-to-server access, sent as Authorization: Bearer cliqer_<key> — see below.
Session Model
| Credential | Where it lives | How it is obtained |
|---|---|---|
Session cookie (session_token) | httpOnly, Secure cookie on your Cliqer host | Set by POST /api/auth/login, rotated by POST /api/auth/refresh |
The cookie is HttpOnly, so it is not readable from JavaScript. Its lifetime follows the server session TTL and is renewed transparently whenever you call GET /api/auth/me or POST /api/auth/refresh.
Session Lifecycle
Using the Session
Browsers attach the cookie automatically. For server-to-server clients, persist the cookie the login response sets and send it on subsequent requests:
# Save the session cookie on login, then reuse it
curl -X POST "$BASE_URL/api/auth/login" \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{"email":"user@example.com","password":"your_password"}'
curl -X GET "$BASE_URL/api/auth/me" -b cookies.txt
// Same-origin $fetch sends the httpOnly cookie automatically.
// Cross-origin fetch must opt in with credentials: 'include'.
const me = await $fetch('/api/auth/me')
import requests
session = requests.Session() # persists the session cookie
session.post('$BASE_URL/api/auth/login',
json={'email': 'user@example.com', 'password': 'your_password'})
me = session.get('$BASE_URL/api/auth/me').json()
jar, _ := cookiejar.New(nil)
client := &http.Client{Jar: jar}
// Login stores the session cookie in the jar; later requests reuse it.
client.Post("$BASE_URL/api/auth/login", "application/json", body)
resp, _ := client.Get("$BASE_URL/api/auth/me")
$ch = curl_init('$BASE_URL/api/auth/login');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'email' => 'user@example.com', 'password' => 'your_password'
]));
curl_exec($ch);
// reqwest with cookie_store persists the session cookie across calls
let client = reqwest::Client::builder()
.cookie_store(true)
.build()?;
client.post("$BASE_URL/api/auth/login")
.json(&serde_json::json!({"email": "user@example.com", "password": "your_password"}))
.send().await?;
let me = client.get("$BASE_URL/api/auth/me").send().await?;
API Key Authentication
Studio and Enterprise teams can mint long-lived API keys for server-to-server access, an alternative to holding a user's login credentials. Create and revoke them in the dashboard under Team → API keys (/dash/team/api-keys); minting requires the team owner (or a custom role granted the API-keys permission). Key management is available on the Studio and Enterprise plans only.
A key is shown once at creation, in the form cliqer_<prefix>_<secret>. Store it securely — it cannot be recovered, so revoke and re-create if it is lost. Send it as a Bearer token on any endpoint that accepts a session:
curl -X GET "$BASE_URL/api/licenses/status?license_key=YOUR_LICENSE_KEY" \
-H "Authorization: Bearer cliqer_ab12cd34_your_secret_here"
The key authenticates as the user who created it, scoped to that user's team and plan. A revoked or unknown key returns 401, exactly like a missing session. Admin-only endpoints always reject API keys with 403. Keys are subject to the same rate limits as session requests.
Log In
Exchange user credentials for a session. On success the response sets the session_token cookie.
Endpoint: POST /api/auth/login
| Parameter | Type | Required | Description |
|---|---|---|---|
email | string | Yes | User email (a WordPress username is also accepted) |
password | string | Yes | User password |
otp | string | No | TOTP code, when the account uses an authenticator app |
email_otp | string | No | Emailed PIN, when the account uses email 2FA |
request_email_otp | boolean | No | Ask the server to send an email PIN (for both method) |
curl -X POST "$BASE_URL/api/auth/login" \
-H "Content-Type: application/json" \
-c cookies.txt \
-d '{"email":"user@example.com","password":"your_password"}'
interface LoginResponse {
data: {
expires: number | null // session lifetime in ms
}
}
const res: LoginResponse = await $fetch('/api/auth/login', {
method: 'POST',
body: { email: 'user@example.com', password: 'your_password' }
})
// The session_token cookie is set by the response; no token is in the body.
import requests
session = requests.Session()
data = {'email': 'user@example.com', 'password': 'your_password'}
res = session.post('$BASE_URL/api/auth/login', json=data)
# The session cookie now lives in `session`.
jar, _ := cookiejar.New(nil)
client := &http.Client{Jar: jar}
payload := map[string]string{"email": "user@example.com", "password": "your_password"}
body, _ := json.Marshal(payload)
client.Post("$BASE_URL/api/auth/login", "application/json", bytes.NewBuffer(body))
$ch = curl_init('$BASE_URL/api/auth/login');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'email' => 'user@example.com',
'password' => 'your_password'
]));
$response = curl_exec($ch);
use serde::Serialize;
#[derive(Serialize)]
struct LoginRequest { email: String, password: String }
let client = reqwest::Client::builder().cookie_store(true).build()?;
let response = client
.post("$BASE_URL/api/auth/login")
.json(&LoginRequest { email: "user@example.com".into(), password: "your_password".into() })
.send()
.await?;
Response (success): sets the session_token cookie and returns the session metadata.
{
"data": {
"expires": 604800000
}
}
Response (2FA required): a 200 control-flow signal, not an error. Resubmit POST /api/auth/login with the same credentials plus otp (authenticator) or email_otp (emailed PIN).
{
"requires_otp": true,
"method": "totp"
}
method is one of totp, email, or both. For email/both the server emails a PIN (set request_email_otp: true to trigger it on the both method).
401. A wrong or expired 2FA code also returns 401.Refresh Session
Rotate the session cookie before it expires. Uses the inbound cookie, there is no request body.
Endpoint: POST /api/auth/refresh
curl -X POST "$BASE_URL/api/auth/refresh" -b cookies.txt -c cookies.txt
// Sends the session cookie; the response rotates it.
await $fetch('/api/auth/refresh', { method: 'POST' })
session.post('$BASE_URL/api/auth/refresh')
req, _ := http.NewRequest("POST", "$BASE_URL/api/auth/refresh", nil)
client.Do(req) // client carries the cookie jar
$ch = curl_init('$BASE_URL/api/auth/refresh');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_COOKIEJAR, 'cookies.txt');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_exec($ch);
client.post("$BASE_URL/api/auth/refresh").send().await?;
Response: rotated session_token cookie plus { "data": { "expires": 604800000 } }. Returns 401 when there is no session to refresh.
Current User
Return the authenticated user's profile, or { "data": null } when there is no valid session (anonymous-friendly, never a 401).
Endpoint: GET /api/auth/me
curl -X GET "$BASE_URL/api/auth/me" -b cookies.txt
const { data } = await $fetch('/api/auth/me')
if (data) console.log(data.email, data.is_admin)
me = session.get('$BASE_URL/api/auth/me').json()
resp, _ := client.Get("$BASE_URL/api/auth/me")
$ch = curl_init('$BASE_URL/api/auth/me');
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
$response = curl_exec($ch);
let me = client.get("$BASE_URL/api/auth/me").send().await?;
Response:
{
"data": {
"id": "uuid",
"email": "user@example.com",
"first_name": "Jane",
"last_name": "Doe",
"role": "uuid",
"tfa_method": "none",
"is_admin": false,
"biometric_unlocked": false
}
}
Log Out
Terminate the session for the current cookie and clear it from the browser.
Endpoint: POST /api/auth/logout
curl -X POST "$BASE_URL/api/auth/logout" -b cookies.txt -c cookies.txt
await $fetch('/api/auth/logout', { method: 'POST' })
session.post('$BASE_URL/api/auth/logout')
req, _ := http.NewRequest("POST", "$BASE_URL/api/auth/logout", nil)
client.Do(req)
$ch = curl_init('$BASE_URL/api/auth/logout');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_COOKIEFILE, 'cookies.txt');
curl_exec($ch);
client.post("$BASE_URL/api/auth/logout").send().await?;
Response:
{
"success": true
}
Security Best Practices
- Use HTTPS only - The session cookie is
Secure; all requests must use TLS 1.2+. - Never read the cookie from JavaScript - It is
HttpOnlyby design; keep it that way. - Refresh proactively - Call
POST /api/auth/refresh(or rely onGET /api/auth/me) before the session expires on long-lived tabs. - Log out to revoke -
POST /api/auth/logoutterminates the session server-side immediately. - Store credentials, not sessions - Server-to-server integrations should hold the login credentials in a secret manager and re-establish a session, never persist the cookie in source.