LYRICX API

Base (local): http://localhost/lyrics/api · Base (prod): https://api.getlyricx.com

Tout tourne sur l'hébergement PHP : l'écran (ESP32) fait un POST devices/sync.php par seconde, l'app (app/) interroge l'API toutes les 2 s. Référence complète : api/PROTOCOL.md.

PWA (app/) ──HTTPS 2 s──► PHP (cette API) ◄──HTTPS 1 POST/s── ESP32 (devices/sync.php : paroles + commandes + acks)

Auth Flow

GET /users/login-spotify.php Public

Start Spotify OAuth (PKCE + state), then redirect to Spotify.

curl -i "http://localhost/lyrics/api/users/login-spotify.php"
GET /users/spotify-callback.php Spotify callback

Called by Spotify with code and state. If account exists, auto-login and store app token in localStorage. If account is new, shows username setup form.

Do not call manually unless testing callback behavior.

POST /users/complete-signup.php Session required

Create local app account after Spotify callback for first-time users.

  • username (required, 3-24, letters/numbers/_)
  • is_public_profile (required, 1 or 0)
{
  "ok": true,
  "user": { "id": 1, "username": "my_name", "is_public_profile": true },
  "auth_token": "lx_...",
  "redirect": "http://localhost/lyrics/login.php?created=1"
}

User Endpoints

GET /users/me.php Bearer token required
curl -H "Authorization: Bearer lx_YOUR_TOKEN" "http://localhost/lyrics/api/users/me.php"
{ "ok": true, "user": { "id": 1, "username": "my_name", "spotify_display_name": "...", ... } }
GET /lyrics/?user_id=<id>&have=<track_id>&art=32 user_id or Bearer

Now playing + synced lyrics + album art grid (art = 16, 32 or 64) for a user. Used by the profile page. The ESP32 gets the same payload inside devices/sync.php. Logic lives in lyrics/_lyrics.php (lyrics_payload()).

Device Endpoints — used by the display (ESP32)

Auth = device_id + secret (sha256 stored at first contact). No user token.

POST /devices/sync.php device secret 1 request / second

The single point of contact of a display. Uploads acks / state / now / stats, downloads pairing status, queued commands, the "frames wanted" flag and the lyrics payload of the paired account.

curl -X POST "http://localhost/lyrics/api/devices/sync.php" -H "Content-Type: application/json" -d '{
  "device_id":"LX-A82F91","secret":"…","firmware":"2.0.0","model":"matrix-128x64","ip":"192.168.1.42","rssi":-52,
  "have":"","art":64,
  "acks":[{"id":123,"ok":true}],
  "state":{"brightness":92, "...":"..."}, "now":{"playing":true, "...":"..."}
}'
// not paired yet
{"success":false,"error":"Not paired","device":{"paired":false,"claim_code":"7K3M2A","commands":[],"frames":false,"server_time":1758367200}}

// paired: lyrics payload + device block
{"success":true,"track_id":"…","now_playing":{…},"lyrics":[…],
 "device":{"paired":true,"user":{"id":42,"username":"noah"},"name":"Salon",
           "commands":[{"id":125,"command":"brightness","value":70}],"frames":false,"server_time":1758367200}}

// refused
401 {"success":false,"error":"bad secret"}
POST /devices/frame.php X-Device-Id / X-Device-Secret

Raw body of 16 384 bytes (128×64 RGB565 LE): the current framebuffer. Sent ~2×/s only while sync.php answers "frames":true.

Device Endpoints — used by the app / profile

Auth: Authorization: Bearer lx_… (or ?auth_token=). Bodies accept JSON or form-data. A display is online when its last sync is < 15 s old.

GET /devices/mine.php Bearer

Displays linked to the account with their last state. The app polls it every 2 s.

curl -H "Authorization: Bearer lx_YOUR_TOKEN" "http://localhost/lyrics/api/devices/mine.php"
{
  "ok": true, "count": 1, "server_time": 1758367200,
  "devices": [{
    "device_id": "LX-A82F91", "name": "Salon", "online": true, "firmware": "2.0.0", "model": "matrix-128x64",
    "ip": "192.168.1.42", "rssi": -52, "last_seen": "2026-09-20 12:00:00", "created_at": "2026-09-20 11:00:00",
    "state": { "brightness": 70, "style": 2, ... }, "sys": { "ssid": "...", ... },
    "now": { "playing": true, "track": "...", "artist": "...", "progress": 45210, "duration": 271600, ... },
    "stats": { "totalMin": 1234, ... }
  }]
}
GET /devices/get.php?device_id=LX-… Bearer

One display (same shape as an item of mine.php).

POST /devices/command.php Bearer

Queues a command for the display and waits for its ack (default 8 s, max 12 s; timeout_ms: 0 = fire and forget → 202). The display picks it up at its next sync (≤ 1 s).

curl -X POST "http://localhost/lyrics/api/devices/command.php" \
  -H "Authorization: Bearer lx_YOUR_TOKEN" -H "Content-Type: application/json" \
  -d '{"device_id":"LX-A82F91","command":"brightness","value":70}'
200 {"ok":true,"ack":{"id":125,"ok":true,"latency_ms":640}}
200 {"ok":false,"error":"brightness: integer expected","ack":{"id":126,"ok":false,"error":"…","latency_ms":700}}
202 {"ok":true,"queued":true,"ack":{"id":127,"status":"pending"}}
404 {"ok":false,"error":"device offline","last_seen":"2026-09-20 12:00:00"}
504 {"ok":false,"error":"timeout","ack":{"id":128,"status":"sent"}}
commandvalueeffect
brightness5…100panel brightness
style0…12lyric style
autorotate, accentauto, showprogress, progresswave, showbreaks, nightbooltoggles
lead−1000…2000lyric offset (ms)
accent"#RRGGBB"manual accent color
titlestyle 0…2, transition 0…2, nolyrics 0…1, idlemode 0…2, clockstyle 0…5, colororder 0…5intscreens
pauseidle0…120minutes before idle screen (0 = never)
nightstart, nightend0…23night mode window
nightbri0…60night brightness (0 = off)
location{"lat":50.85,"lon":4.35}weather
set{ "brightness": 70, "style": 2, … }batch of settings
colortest1 / 2 / 3panel color test patterns
statsreset, reboot, wifireset, identify, get_state, get_stats—actions
ota"https://…/firmware.bin"download + flash + reboot
api"http://…/lyrics/api" or ""change API base (NVS) + reboot
POST /devices/claim.php Bearer

Link the display showing this pairing code to the account. The display learns it at its next sync and switches to lyrics.

curl -X POST "http://localhost/lyrics/api/devices/claim.php" \
  -H "Authorization: Bearer lx_YOUR_TOKEN" -H "Content-Type: application/json" \
  -d '{"code":"7K3M2A","name":"Salon"}'
200 { "ok": true, "device": { "device_id": "LX-A82F91", "name": "Salon", "online": true, ... } }
404 { "ok": false, "error": "Invalid or expired pairing code. ..." }
409 { "ok": false, "error": "This Matrix is not connected right now. ..." }
410 { "ok": false, "error": "This pairing code has expired. ..." }
POST /devices/unlink.php Bearer

Body {"device_id":"LX-…"}. Unlinks the display; at its next sync it shows a new pairing code. Pending commands are dropped.

POST /devices/rename.php Bearer

Body {"device_id":"LX-…","name":"Chambre"} (1–64 chars).

GET /devices/frame.php?device_id=LX-…&want=1 Bearer

Live LED preview. want=1 asks the display to stream its framebuffer for the next 15 s (the app calls this every 0.5 s). Returns 200 + 16 384 bytes (RGB565 LE) or 204 when no fresh frame (< 10 s) exists.

POST /devices/upsert.php Legacy

Old heartbeat endpoint (device_id + auth_token). Kept for compatibility; firmware 2.x uses sync.php.

Frontend Token Usage

// Save token
localStorage.setItem('lyricx_auth_token', authToken);

// Use token
const token = localStorage.getItem('lyricx_auth_token');
const me = await fetch('/lyrics/api/users/me.php', {
  headers: { Authorization: `Bearer ${token}` }
}).then(r => r.json());

// The app (app/) reads the same token, polls devices/mine.php and sends devices/command.php.

Testing without the UI

# simulated display (no ESP32 needed) — prints "PAIR CODE XXXXXX" and executes commands
API_BASE=http://localhost/lyrics/api node tools/fake-device.js

# full automated chain: register → claim → command → ack → state → frame → unlink
API_BASE=http://localhost/lyrics/api TOKEN=lx_test node tools/smoke.js

Proxy Helper

GET /templog.php Proxy callback relay

Spotify redirect relay endpoint (prod domain) forwarding callback query params to local callback target.