Reference
HTTP API
Post rows to Evlat from any language: the socket, fields and answers.
evlat signal is a thin client over a local endpoint. Anything that speaks HTTP over a unix socket can post rows directly.
Endpoint
| Socket | ~/.config/evlat/run/evlat.sock |
|---|---|
| Request | POST /signal, with Host: 127.0.0.1 |
| Body | JSON, up to 1 MiB |
Evlat opens no network port for this: every request goes through the socket. The socket’s folder can be entered by you alone, so no key is needed. The examples still write http://127.0.0.1:48151/signal, but only to fill the Host header; nothing connects to that port. The Host must be 127.0.0.1, localhost or [::1], and a request that carries an Origin header is refused, so web pages cannot reach it.
curl
curl 7.40 or newer, for --unix-socket:
curl -s --unix-socket "$HOME/.config/evlat/run/evlat.sock" \
http://127.0.0.1:48151/signal -H 'Content-Type: application/json' \
-d '{"id":"render","ttl":900,"phase":"working","label":"Render","progress":0.4}'Python
import http.client, json, os, socket
SOCK = os.path.expanduser("~/.config/evlat/run/evlat.sock")
class EvlatConnection(http.client.HTTPConnection):
def __init__(self):
super().__init__("127.0.0.1", timeout=2)
def connect(self):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.sock.connect(SOCK)
def signal(**row):
try:
conn = EvlatConnection()
conn.request("POST", "/signal", json.dumps(row), {"Content-Type": "application/json"})
conn.getresponse().read()
conn.close()
except OSError:
pass # Evlat isn't running: carry on
signal(id="train", ttl=900, phase="working", label="Training", progress=0.25)Node
import { request } from 'node:http';
import { homedir } from 'node:os';
const socketPath = `${homedir()}/.config/evlat/run/evlat.sock`;
export function signal(row) {
return new Promise((resolve) => {
const req = request({
socketPath, path: '/signal', method: 'POST',
headers: { 'Content-Type': 'application/json' },
timeout: 2000,
}, (res) => { res.resume(); res.on('end', resolve); });
req.on('timeout', () => req.destroy());
req.on('error', resolve); // Evlat isn't running: carry on
req.end(JSON.stringify(row));
});
}
await signal({ id: 'export', ttl: 900, phase: 'working', label: 'Export', progress: 0.5 });Fields
| Field | Rules |
|---|---|
id | Required. Letters, digits, . _ -; 1–64 characters. |
ttl | Required. Whole seconds, 0–86400. 0 removes the row and nothing else is read. It is how long a working or waiting row lives; a done or failed row stays until it has been seen, up to 12 hours. |
phase | Required unless ttl is 0: working, waiting, done or failed. |
progress | Optional number from 0 to 1. |
label | Optional, up to 80 characters. Defaults to the id. |
detail | Optional, up to 200 characters. |
sender | Optional, up to 24 characters. |
Longer text is cut, not refused. Line breaks and tabs become spaces. null counts as absent. Each request replaces the whole row, as with evlat signal.
Answers
| Status | Means |
|---|---|
200 {} | Taken. |
400 | A field is wrong. error.code names it: invalidId, invalidTtl, invalidPhase, invalidProgress, invalidLabel, invalidDetail, invalidSender. A body that isn’t a JSON object is badRequest. |
403 | A request from a browser (it has an Origin header), or a Host that isn’t 127.0.0.1, localhost or [::1]. |
404 | No such route: check the path, or the Evlat that answers is older than this page. |
{"error":{"code":"invalidProgress","message":"progress must be a number from 0 to 1"}}An X-Evlat-Key header, which older versions asked for, is ignored.
Another socket
If Evlat was started with EVLAT_SOCKET, post to that path instead; with EVLAT_HOME, the socket is under that home. evlat signal and evlat watch follow the same variables. See Limits & environment.
The sandbox port
While Watch sandboxes is on, Evlat also listens on 127.0.0.1:48152, for the hooks of Claude Code in Docker sandboxes. It takes hooks only: /signal answers 404 there, as do the routes that wait for an answer on a card. Post your rows to the socket.