AtlasAuth - HTTP API Reference (v1)
This is the raw HTTP reference for callers who are not using an official SDK. It restates the frozen wire contract (CONTRACT.md, v1) directly. The SDKs implement exactly this. If you build your own client, you must verify signatures and nonces exactly as described here. Treat any response you cannot verify as a failed, hostile call.
- Base URL:
https://atlasauth.cc/api/v1 - Method: every endpoint is
POSTwithContent-Type: application/json. - Every request includes
app_idand a freshnonce. Authenticated calls also includesession.
1. The signed envelope
Every security-relevant response has HTTP 200 and exactly this body:
{ "payload": "<compact-json-string>", "sig": "<base64-p1363-sig>" }payload is a JSON string whose value is itself compact JSON. sig is the signature over the UTF-8 bytes of that payload string.
- Algorithm: ECDSA, curve P-256 (prime256v1 / secp256r1), hash SHA-256.
- Signature encoding: raw IEEE P1363
r || s, exactly 64 bytes, then base64 (standard, with padding). *Not* DER. - Public key you embed: the app's public key in SPKI DER, base64. (The server signs with the matching private key, which never leaves the server.)
Every payload object contains at minimum:
{ "v": 1, "t": <server unix seconds>, "nonce": "<echo of request nonce>", "ok": <bool> }A signed ok:false (wrong password, expired key, banned, and so on) is still a signed truth. Read error / code for messaging. An unsigned { "error": "...", "code": "..." } with a 4xx/5xx status is only for transport-level failures. Treat any unverifiable response as failure.
How to verify (every signed response)
- Read
payloadas an opaque UTF-8 string. Do not re-serialize it. The signature is over those exact bytes. (When you pull it out of the envelope JSON, only JSON-*unescape* the string value. Never round-trip it through a serializer.) - base64-decode
sigto get exactly 64 bytes (IEEE P1363r || s). - Verify
ECDSA-P256-SHA256(sig, utf8(payload), embeddedPublicKey). If it is invalid, abort (do not parse the payload). - Only then JSON-parse the payload.
- Check that
payload.nonceequals the nonce you sent. If not, it is a replay or tamper, so abort. - (Advisory) Check
abs(localUnix - payload.t) <= 60. On drift, warn but do not hard-fail.
Verify with the OpenSSL CLI
The CLI verifies a DER signature, so convert the 64-byte P1363 r||s to DER first. You need the public key in pub.pem (SPKI), the exact payload bytes in payload.bin, and the raw 64-byte signature in sig.p1363.bin:
# r = first 32 bytes, s = last 32 bytes.
# Build a DER ECDSA-Sig-Value: SEQUENCE { INTEGER r, INTEGER s }.
# (Use any P1363->DER helper; many languages expose this directly. Pseudo-DER below.)
# der = SEQ( INT(r), INT(s) ) # mind the leading-zero rule for high-bit-set integers
openssl dgst -sha256 -verify pub.pem -signature sig.der payload.bin
# -> "Verified OK" (or "Verification Failure")If your public key is in base64 SPKI DER (the form the SDK embeds), wrap it to PEM first:
{ echo "-----BEGIN PUBLIC KEY-----"; echo "<base64-spki>" | fold -w64; \
echo "-----END PUBLIC KEY-----"; } > pub.pemVerify (language-agnostic pseudo-code)
function handleResponse(httpStatus, body, sentNonce, pubKeySpkiDer):
if httpStatus != 200:
# unsigned transport error { error, code } - never trusted
return FAIL(parseUnsignedError(body))
env = jsonParse(body)
if env.payload is not String or env.sig is not String:
return FAIL("unsigned/unstructured response")
payloadStr = env.payload # opaque; DO NOT re-serialize
payloadBytes = utf8(payloadStr)
sig = base64decode(env.sig)
if length(sig) != 64: return FAIL("bad signature length")
if not ecdsaP256Sha256Verify(sig_p1363 = sig,
message = payloadBytes,
key = importSpki(pubKeySpkiDer)):
return FAIL("signature verification failed") # HOSTILE - abort
p = jsonParse(payloadStr) # only now
if p.v != 1: return FAIL("bad envelope version")
if p.nonce != sentNonce: return FAIL("nonce mismatch - replay/tamper")
if abs(localUnix() - p.t) > 60: warn("clock skew") # advisory only
return OK(p) # trust p.ok / p.code / fields2. Nonce rules
- Generate a fresh, cryptographically-random nonce for every request: ≥16 bytes, encoded as hex or base64url (the official SDKs use 16 random bytes turned into lowercase hex).
- Put it in the request body as
nonceand hold it until the matching response is verified. - The server echoes it inside the signed payload (
payload.nonce). After you verify the signature, you must confirm the echo equals what you sent. A mismatch means a replayed or tampered response, so abort. This is what makes captured responses non-replayable.
3. Session lifecycle
initreturns an opaquesessiontoken plus app status. Required before anything else.login/register/licenseauthenticates the session (server marks itactive).check(heartbeat) runs everyheartbeatseconds. Kick the user when the signed reply says the session, app, or key is no longer valid.logout(optional) ends the session.
The session token is opaque server-side state. Send it in the JSON body of every later call. Killing it server-side invalidates it on the next check.
4. Endpoints
All requests include app_id and nonce. All signed responses include v, t, nonce, ok. Fields marked ? are optional or conditional. expiry is unix seconds or null (null = lifetime).
POST /init
Establish a session and read app status / heartbeat interval.
Request
{ "app_id": "<uuid>", "nonce": "<n>", "version": "<client app version, optional>" }Signed payload
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
"session": "<token>",
"app_name": "AtlasApp",
"app_status": "active", // "active" | "maintenance" | "disabled"
"status_message": "<text shown to user when not active>",
"heartbeat": 10, // seconds between /check calls
"hwid_required": true,
"version_ok": true, // false if app forces a version and it mismatches
"latest_version": "1.4.0" }If app_status != "active", show status_message and exit. If version_ok == false and the app forces a version, show an update notice and exit.
POST /register
Self-signup with a license (only if the app allows registration).
Request
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>",
"username": "<u>", "password": "<p>", "license": "<key>", "hwid": "<hwid>",
"email": "<optional>" }Signed payload
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
"error": "<optional>", "code": "<optional>",
"expiry": null, "username": "<u>" }Codes: ok · register_disabled · username_taken · invalid_license · license_used · license_banned · hwid_banned · ip_banned · bad_input.
POST /login
Username + password.
Request
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>",
"username": "<u>", "password": "<p>", "hwid": "<hwid>" }Signed payload
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
"error": "<optional>", "code": "<optional>",
"username": "<u>",
"expiry": null, // null = lifetime
"level": 0,
"created_at": 0,
"last_login": 0,
"remaining_seconds": null }Codes: ok · invalid_credentials · user_banned · hwid_mismatch · hwid_banned · ip_banned · license_expired · no_subscription · bad_input.
POST /license
License-key-only login (no username/password).
Request
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>",
"license": "<key>", "hwid": "<hwid>" }Signed payload
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
"error": "<optional>", "code": "<optional>",
"expiry": null, "level": 0, "remaining_seconds": null }Codes: ok · invalid_license · license_expired · license_banned · hwid_mismatch · hwid_banned · ip_banned · bad_input.
POST /check - heartbeat
Call every heartbeat seconds.
Request
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>" }Signed payload
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
"valid": true, // session still alive & authed & not killed
"app_status": "active", // re-checked every beat
"status_message": "",
"key_valid": true, // bound key not expired / not banned
"banned": false, // user/hwid/ip banned since login
"expiry": null,
"remaining_seconds": null,
"reason": "" } // when ok/valid false: killed | expired | app_disabled | app_maintenance | bannedKick the user immediately if ok == false OR valid == false OR app_status != "active" OR key_valid == false OR banned == true.
POST /var
Fetch an app variable.
Request
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>", "name": "<var name>" }Signed payload
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true,
"found": true, "value": "<string>", "code": "<optional>" }The server enforces visibility. A variable marked auth-required returns ok:false, code:"auth_required" unless the session is authenticated. Public variables return on any valid session.
POST /log
Client-side logging to the app log and an optional Discord webhook. Best-effort, and it never blocks the client. session is optional.
Request
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token, optional>",
"level": "info", // "info" | "warn" | "error"
"message": "<text>" }Signed payload
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true }POST /logout
End the session.
Request
{ "app_id": "<uuid>", "nonce": "<n>", "session": "<token>" }Signed payload
{ "v": 1, "t": 0, "nonce": "<n>", "ok": true }5. Transport errors (unsigned)
Transport-level failures return an unsigned body with a 4xx/5xx status:
{ "error": "<human>", "code": "<machine>" }HTTP statuses: 400 / 401 / 403 / 404 / 429 / 500. Common codes: unknown_app · bad_request · rate_limited (with a Retry-After header) · server_error. These are not signed. The client must treat any non-verifiable response as a failed call and must not trust its contents.
6. Versioning
The path is /api/v1. Breaking changes bump the path. payload.v is the envelope version (currently 1).
7. Public pingable endpoints (unsigned, informational)
Unlike every endpoint above, these are plain public GETs. No app_id/nonce in a body, no session, no signed envelope, no signature to verify. They carry only display and monitoring data and are safe to call from a website, launcher, uptime monitor, or Discord bot. Both send Cache-Control: public, max-age=15.
Informational only. Never gate execution on these. A fake or MITM server can return anything because there is nothing to verify. Security still rides on the signed /check heartbeat (§4); use that for enforcement. These endpoints just surface status and news to humans.
GET /api/v1/status/{app_id}
App status, status message, and a live online count.
Response
200 { "ok": true, "app_id": "...", "name": "AtlasApp",
"status": "active", // "active" | "maintenance" | "disabled"
"status_message": "...",
"online": 0, // active sessions, last 5 min
"time": 0 } // server unix seconds
404 { "ok": false, "error": "unknown_app" }GET /api/v1/news/{app_id}
Published news/changelog items, ordered pinned-first then newest.
Response
200 { "ok": true, "app_id": "...",
"news": [
{ "id": "...", "title": "...", "body": "...",
"pinned": false, "created_at": 0, "updated_at": 0 }
],
"latest": { "id": "..." }, // first item, or null when there is no news
"time": 0 } // server unix seconds
404 { "ok": false, "error": "unknown_app" }latest is the first element of news (a pinned item if any, otherwise the newest), or null when the list is empty.
GET /status/{app_id}
A human-readable public status and changelog HTML page (not JSON). It shows the same status and news as above, rendered for a browser. Link it from your site or Discord. It needs no SDK and no signing.
