API Documentation
Reference for integrating the ZayForge API into your Launcher (Electron/JS) and game (Love2D/Lua).
Endpoints
Authentication
All auth-protected endpoints accept the JWT token in two ways:
- Bearer header —
Authorization: Bearer <token>(use this in the launcher/game) - Cookie —
zayforge_tokenhttpOnly 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
endGET
/api/pingnoneHealth 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/registernoneCreate 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/loginnoneLog 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 CookieGet 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 CookieChange 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 CookiePermanently delete your account and all game saves.
Response 200
json
{ "ok": true, "deleted": true }POST
/api/auth/logoutCookieClears 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 CookieUpload 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
endResponse 200
json
{
"ok": true,
"user": { "id": "...", "username": "...", "email": "...",
"avatar": "data:image/png;base64,iVBORw0KG...", "createdAt": "..." }
}DELETE
/api/auth/me/avatarBearer or CookieRemove your profile picture.
Response 200
json
{ "ok": true, "user": { ..., "avatar": null, ... } }POST
/api/game/saveBearer or CookieSave 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
endResponse 200
json
{
"ok": true,
"save": { "id": "...", "slot": 0, "name": "World 1", "version": "1.0",
"playTime": 3600, "updatedAt": "..." }
}GET
/api/game/loadBearer or CookieLoad 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 0Love2D / 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
endResponse 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 CookieDelete a specific save slot.
Query
text
DELETE /api/game/saves?slot=0Response 200
json
{ "ok": true, "deleted": true, "slot": 0 }GET
/api/downloadsnoneFetch 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 onlyResponse 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.