WebSocket Protocol
All real-time communication goes through a single WebSocket endpoint. Both ESP32 firmware (agent connections) and client apps (Android, CLI) connect here. The server acts as a blind relay — it never decrypts EWSP payloads, it only routes them.
Connection
wss://wakelink-project.org/wss Plain HTTP GET /wss returns 426 Upgrade Required. Always use WebSocket upgrade. TLS required in production.
After connecting, the server waits 30 seconds for an auth message. If none arrives, the connection is closed with code 1008.
Authentication Handshake
Send an auth message immediately after connecting. The api_token is the persistent token from your account ( wl_ format), or use a short-lived session_token obtained via POST /api/v1/auth/session.
{
"type": "auth",
"api_token": "wl_abc123...", # persistent token, OR:
"session_token": "...", # short-lived 24h token (preferred)
"client_type": "client"
}{
"type": "auth",
"api_token": "wla_abc123...", # per-agent token (wla_ prefix), NOT the user wl_ token
"client_type": "agent", # or "firmware" — both accepted
"agent_id": "WL35080814" # must match registered agent_id
} Firmware must use the per-agent token (wla_...), obtained via GET /api/v1/agents/{agent_id}/export or rotated with POST /api/v1/agents/{agent_id}/rotate-token. Do not use the user account token (wl_...) in firmware.
{
"type": "auth_response",
"status": "authenticated",
"session_token": "...",
"expires_in": 86400,
"max_requests": 10000
}{
"type": "auth_response",
"status": "failed",
"error": "Invalid token",
"retry_after": 600 # seconds (after 5 failures, 10-min lockout)
}Rate limit: 5 failures per IP per 300 seconds → 10-minute lockout.
Message Types
# Server -> any (every 30 seconds)
{ "type": "ping" }
# Client/Agent -> Server (must respond)
{ "type": "pong" }Connections that miss two consecutive pings are closed.
{
"type": "agent_status",
"agent_id": "WL35080814",
"online": true
}Sent to all connected clients of the same user when an ESP32 connects or disconnects.
{
"type": "relay",
"payload": "...", # EWSP packet as JSON string (opaque to server)
"signature": "...", # optional HMAC-SHA256 of payload
"agent_id": "WL35080814",
"request_id": "X7K2M9P1" # optional tracking ID
}The server validates agent ownership, then blindly forwards the payload to the ESP32. It cannot read the encrypted content.
{
"v": "1.0",
"sid": "a1b2c3d4e5f6a7b8", # session ID (routes to correct agent)
"seq": 42,
"p": "base64url(ciphertext+tag)"
} EWSP packets can be sent directly (without relay envelope). Server routes by sid field. Payload is always encrypted.
{
"type": "error",
"error": "Session request limit exceeded",
"code": "LIMIT_EXCEEDED"
}
# Connection closed with code 1008Each session allows up to 10,000 requests. After that, reconnect to get a new session.
Session Tokens (Recommended for Clients)
Instead of sending your long-lived api_token over WebSocket, create a short-lived session token via REST first. This limits exposure if a session is compromised.
POST /api/v1/auth/session
Authorization: Bearer wl_abc123...
-> {
"session_token": "...",
"expires_at": "2026-01-04T12:00:00Z",
"expires_in": 86400,
"max_requests": 10000
}Session tokens expire after 24 hours or 10,000 requests. They are stored in Redis and never touch the database.
Connection Lifecycle
auth with api_token or session_token + client_typeauth_response — status: authenticated or failed