API Documentation

Reference for integrating the ZayForge API into your Launcher (Electron/JS) and game (Love2D/Lua).

Authentication

All auth-protected endpoints accept the JWT token in two ways:

  • Bearer headerAuthorization: Bearer <token> (use this in the launcher/game)
  • Cookiezayforge_token httpOnly cookie (set automatically by web login)

Electron/JS example

javascript
// 1. Login
const { token } = await fetch('https://zayforge.xyz/api/auth/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email: 'user@email.com', password: 'mypass' })
}).then(r => r.json());

// 2. Store token
localStorage.setItem('zayforge_token', token);

// 3. All subsequent requests
const headers = { 'Authorization': `Bearer ${token}` };
const me = await fetch('https://zayforge.xyz/api/auth/me', { headers }).then(r => r.json());

Love2D / Lua example

lua
-- ZayForge API client for Love2D
-- Requires: luasocket (https://github.com/lunarmodules/luasocket)
--           dkjson    (https://github.com/LuaDist/dkjson)

local http = require("socket.http")
local ltn12 = require("ltn12")
local json = require("dkjson")

local API = "https://zayforge.xyz/api"

-- Login and get a token
local function login(email, password)
  local body = json.encode({ email = email, password = password })
  local resp = {}
  local _, status = http.request({
    url = API .. "/auth/login",
    method = "POST",
    headers = {
      ["Content-Type"] = "application/json",
      ["Content-Length"] = #body
    },
    source = ltn12.source.string(body),
    sink = ltn12.sink.table(resp)
  })
  if status ~= 200 then return nil, "Login failed" end
  local result = json.decode(table.concat(resp))
  return result.token, result.user
end

-- Make an authenticated request
local function authRequest(method, path, token, data)
  local headers = {
    ["Authorization"] = "Bearer " .. token,
    ["Content-Type"] = "application/json"
  }
  local body = data and json.encode(data) or nil
  if body then headers["Content-Length"] = #body end

  local resp = {}
  local _, status = http.request({
    url = API .. path,
    method = method,
    headers = headers,
    source = body and ltn12.source.string(body) or nil,
    sink = ltn12.sink.table(resp)
  })
  return json.decode(table.concat(resp)), status
end

-- Usage in love.load()
function love.load()
  local token, user = login("player@email.com", "password123")
  if token then
    -- Get profile
    local me = authRequest("GET", "/auth/me", token)
    print("Logged in as: " .. me.user.username)
  end
end
GET/api/pingnone
Health check. Call this on launcher startup to verify the API is reachable.

Response

json
{
  "ok": true,
  "ping": "pong",
  "name": "ZayForge API",
  "version": "1.0.0",
  "time": "2026-06-07T00:00:00.000Z"
}
POST/api/auth/registernone
Create a new account. Auto-logs you in (returns token + sets cookie).

Body

json
{
  "username": "PlayerName",   // 3-20 chars, letters/numbers/underscores
  "email":    "a@b.com",      // valid email
  "password": "secret123"     // min 6 chars
}

Response 200

json
{
  "ok": true,
  "user": { "id": "...", "username": "PlayerName", "email": "a@b.com", "avatar": null, "createdAt": "..." },
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}

Errors

text
400 - "Username must be at least 3 characters"
400 - "Invalid email address"
409 - "A user with that email already exists"
POST/api/auth/loginnone
Log in with email and password. Returns a JWT token.

Body

json
{ "email": "a@b.com", "password": "secret123" }

Response 200

json
{
  "ok": true,
  "user": { "id": "...", "username": "PlayerName", "email": "a@b.com", "avatar": null, "createdAt": "..." },
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}
GET/api/auth/meBearer or Cookie
Get the current user's profile.

Response 200

json
{
  "ok": true,
  "user": { "id": "...", "username": "PlayerName", "email": "a@b.com", "avatar": "data:image/png;base64,...", "createdAt": "..." }
}
PATCH/api/auth/meBearer or Cookie
Change your username.

Body

json
{ "username": "NewName" }

Response 200

json
{
  "ok": true,
  "user": { "id": "...", "username": "NewName", "email": "...", "avatar": "...", "createdAt": "..." }
}

Errors

text
400 - "Username must be at least 3 characters"
400 - "Username must be at most 20 characters"
400 - "Username can only contain letters, numbers, and underscores"
409 - "Username already taken"
DELETE/api/auth/meBearer or Cookie
Permanently delete your account and all game saves.

Response 200

json
{ "ok": true, "deleted": true }
POST/api/auth/logoutCookie
Clears the session cookie (web only). For launcher/game, just discard the token.

Response 200

json
{ "ok": true, "success": true }
POST/api/auth/me/avatarBearer or Cookie
Upload a 16×16 PNG profile picture. Send raw PNG bytes with Content-Type: image/png.

Constraints

  • Must be a valid PNG file
  • Exactly 16×16 pixels
  • Max 4 KB

Electron/JS

javascript
const file = document.querySelector('input[type=file]').files[0];
const res = await fetch('https://zayforge.xyz/api/auth/me/avatar', {
  method: 'POST',
  headers: { 'Content-Type': 'image/png', 'Authorization': `Bearer ${token}` },
  body: file
});
const { user } = await res.json();

Love2D / Lua

lua
-- Read PNG from save directory and upload as avatar
local function uploadAvatar(token, filepath)
  local file = io.open(filepath, "rb")
  if not file then return nil, "Cannot open file" end
  local data = file:read("*a")
  file:close()

  local resp = {}
  local _, status = http.request({
    url = API .. "/auth/me/avatar",
    method = "POST",
    headers = {
      ["Authorization"] = "Bearer " .. token,
      ["Content-Type"] = "image/png",
      ["Content-Length"] = #data
    },
    source = ltn12.source.string(data),
    sink = ltn12.sink.table(resp)
  })
  return json.decode(table.concat(resp)), status
end

Response 200

json
{
  "ok": true,
  "user": { "id": "...", "username": "...", "email": "...",
            "avatar": "data:image/png;base64,iVBORw0KG...", "createdAt": "..." }
}
DELETE/api/auth/me/avatarBearer or Cookie
Remove your profile picture.

Response 200

json
{ "ok": true, "user": { ..., "avatar": null, ... } }
POST/api/game/saveBearer or Cookie
Save game state to a slot (0-9). Upserts — creates if new, updates if exists.

Body

json
{
  "slot":     0,              // 0-9
  "name":     "World 1",      // display name
  "data":     "{...}",        // JSON-encoded game state (required)
  "playTime": 3600,           // seconds (optional)
  "version":  "1.0"           // game version (optional)
}

Love2D / Lua example

lua
function saveGame(token, gameState, playTime)
  local body = json.encode({
    slot = 0,
    name = "My World",
    data = json.encode(gameState),
    playTime = playTime
  })
  local result, status = authRequest("POST", "/game/save", token, {
    slot = 0,
    name = "My World",
    data = json.encode(gameState),
    playTime = playTime
  })
  -- Actually use the helper correctly:
  local resp = {}
  local _, status = http.request({
    url = API .. "/game/save",
    method = "POST",
    headers = {
      ["Authorization"] = "Bearer " .. token,
      ["Content-Type"] = "application/json",
      ["Content-Length"] = #body
    },
    source = ltn12.source.string(body),
    sink = ltn12.sink.table(resp)
  })
  return json.decode(table.concat(resp)), status
end

Response 200

json
{
  "ok": true,
  "save": { "id": "...", "slot": 0, "name": "World 1", "version": "1.0",
            "playTime": 3600, "updatedAt": "..." }
}
GET/api/game/loadBearer or Cookie
Load game saves. Pass ?slot=N for a specific slot, or omit for all saves.

Examples

text
GET /api/game/load          → all saves
GET /api/game/load?slot=0   → just slot 0

Love2D / Lua example

lua
function loadGame(token, slot)
  local resp = {}
  local url = API .. "/game/load"
  if slot then url = url .. "?slot=" .. slot end
  local _, status = http.request({
    url = url,
    headers = { ["Authorization"] = "Bearer " .. token },
    sink = ltn12.sink.table(resp)
  })
  local result = json.decode(table.concat(resp))
  if result.ok and #result.saves > 0 then
    local gameState = json.decode(result.saves[1].data)
    return gameState
  end
  return nil
end

Response 200

json
{
  "ok": true,
  "saves": [
    { "id": "...", "slot": 0, "name": "World 1",
      "data": "{\"playerX\":100}", "version": "1.0",
      "playTime": 3600, "updatedAt": "..." }
  ]
}
DELETE/api/game/savesBearer or Cookie
Delete a specific save slot.

Query

text
DELETE /api/game/saves?slot=0

Response 200

json
{ "ok": true, "deleted": true, "slot": 0 }
GET/api/downloadsnone
Fetch the latest releases from GitHub.

Query

text
GET /api/downloads                    → launcher + game
GET /api/downloads?type=launcher      → launcher only
GET /api/downloads?type=game          → game only

Response 200

json
{
  "ok": true,
  "launcher": {
    "tag": "release", "name": "v1", "publishedAt": "...",
    "htmlUrl": "https://github.com/.../releases/tag/release",
    "assets": [
      { "name": "ZayForge.Launcher.1.0.0.exe", "size": 104319553,
        "browser_download_url": "https://...",
        "platform": "windows", "type": "portable" }
    ]
  }
}
Base URL: https://zayforge.xyz  |  All responses include CORS headers.  |  Tokens expire after 30 days.