Drive Lamp Layers from your own code
The one metered task, light, is available over HTTP. It takes one room - its size, its lamps and the spots where people read, relax, work or eat - plus the page's light estimates, and returns a lighting plan: the verdict, a headline, the layers in plain words, one dimmer scene per spot, the lamps worth adding and a check to make first.
The division of labour first: the page estimates; the model plans. The page works out each lamp's share of light at each spot (whole lux that add up), the totals with every lamp at full against each spot's target, the ambient, task and accent layers and the verdict, and sends them as facts. The model never changes those numbers. After the reply, the page re-adds every scene's light from its own shares, places every suggested lamp on the plan and lists anything that does not fit. Estimates from a plan, not professional advice.
Base URL and the envelope
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response uses one envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"lamp-layers"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required field (task, room, lamps, spots, facts) is missing. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: the lighting plan is metered and needs a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field, the input and the rules
Every run sends task: light (the one task is light); a missing or different one is still answered as light. All fields are strings.
| Field | Required | What it holds |
|---|---|---|
task | yes | Always light. |
room | yes | kind - W m wide x D m deep, ceiling H m | walls: light, mid-tone or dark | name: ... | note: .... Name and note are none when empty. |
lamps | yes | One lamp a line: L1. name | type | layer | lumens | colour temperature | dimmable or not | where it is. One to ten lamps. |
spots | yes | One spot a line: S1. name | reading, relaxing, desk, dining, counter or bedside | morning, midday, evening or night | target range | where it is. One to four spots. |
facts | yes | The page's numbers: each lamp's share of light at each spot at full, the totals and their status against the target, the layers and the missing ones, which lamps dim, colour temperatures, the largest lamp, the short spots shortest first and the verdict. Build it with LampKit.buildInput. |
retry_note | no | Only on a reformat retry: why the previous reply was unusable. |
The verdict, checked in order: short-of-light when a spot stays under the low end of its target with every lamp at full; otherwise missing-a-layer when there is no ambient lamp, no accent lamp, or no task lamp while the room has a reading, desk or counter spot; otherwise ready-for-scenes.
The reply
One JSON object, no prose around it:
{"task": "light", "verdict": "missing-a-layer", "headline": "...", "layers_note": "...",
"scenes": [{"spot": "S1", "name": "...", "time": "night", "levels": {"L1": 10, "L2": 60}, "lux": 52, "why": "..."}],
"additions": [{"type": "strip", "layer": "accent", "lumens": 400, "cct": 2700, "near": "S1", "why": "..."}],
"check_first": "..."}
| Field | What it holds |
|---|---|
verdict | short-of-light, missing-a-layer or ready-for-scenes - always the page's. |
headline | One sentence on what the verdict means for this room. |
layers_note | The lamps in each layer by name, and the missing layers. |
scenes | One per spot, in spot order: the spot id, a name, the time of day, a level (0-100 in tens) for every lamp, the light at the spot and why. |
additions | Lamps worth adding: a catalogue type, its layer, lumens, colour temperature and the spot it goes near; one or two when short of light, one per missing layer, none when ready. |
check_first | One practical check before changing anything: an electrician for a new fixed light, dimmable bulbs for dimmed lamps, or bulb wattage. |
The page reads the reply with ReplyKit.normalize and checks it with ReplyKit.check, which lists, under "The page's check", a verdict that differs from the page's, a scene whose light does not add up or misses its target, a level a lamp cannot take, an added lamp outside the catalogue or that would still leave its spot short, and numbers or ids in the prose.
Worked examples - every verdict
These are the free samples on the page: the exact input the page sends and the reply that replays for free. Together they cover every verdict - short of light, a layer missing and ready for scenes - a lamp that cannot dim, mixed colour temperatures, and spots used in the evening, at night and at midday.
A front room with one ceiling light - short-of-light
Input:
{
"task": "light",
"room": "Living room - 4.2 m wide x 3.6 m deep, ceiling 2.4 m | walls: mid-tone | name: The front room | note: We read and watch TV here most evenings.",
"lamps": "L1. Ceiling light | Ceiling light (flush) | ambient | 1500 lm | 4000 K | not dimmable | 2.1 m from the left wall, 1.8 m from the back wall\nL2. Lamp on the side table | Table lamp | ambient | 450 lm | 2700 K | dimmable | 3.7 m from the left wall, 3.0 m from the back wall",
"spots": "S1. Armchair by the window | reading | evening | target 300-500 lux | 0.7 m from the left wall, 0.7 m from the back wall\nS2. Sofa facing the TV | relaxing | evening | target 150-300 lux | 3.0 m from the left wall, 3.0 m from the back wall",
"facts": "Light at each spot with every lamp at 100% (estimated lux on the spot's surface; each lamp's share already includes its light bounced off the walls, so the shares add up; a lamp at a lower level gives its share x level / 100):\nS1 Armchair by the window (reading, evening, target 300-500): L1 49 + L2 5 = 54 at full - short by 246\nS2 Sofa facing the TV (relaxing, evening, target 150-300): L1 62 + L2 24 = 86 at full - short by 64\nLayers: ambient: L1, L2 | task: none | accent: none (the room has a task spot)\nMissing layers (ambient always counts; task only when there is a reading, desk or counter spot; accent always counts): task, accent\nDimmable: L1 no, L2 yes\nColour temperature: L1 4000 K, L2 2700 K (spread 1300 K)\nLargest single lamp: L1, 77% of all lumens\nShort spots, shortest first (light at full as a share of the target's low end): S1 18%, S2 57%\nVerdict: short-of-light (rule, checked in order: short-of-light when any spot is under its target's low end with every lamp at full; otherwise missing-a-layer when a layer above is missing; otherwise ready-for-scenes)"
}
Reply:
{
"task": "light",
"verdict": "short-of-light",
"headline": "The armchair by the window reaches about 54 lux at full power, short of its target's low end of 300 lux, so the room needs another lamp.",
"layers_note": "The ambient layer has the ceiling light and the lamp on the side table, but the room is missing a task layer and an accent layer.",
"scenes": [
{
"spot": "S1",
"name": "Armchair by the window",
"time": "evening",
"levels": {
"L1": 100,
"L2": 100
},
"lux": 54,
"why": "The ceiling light does most of the work here, but even with every lamp at full the armchair by the window stays below its target until the added lamp is in."
},
{
"spot": "S2",
"name": "Sofa facing the TV",
"time": "evening",
"levels": {
"L1": 100,
"L2": 100
},
"lux": 86,
"why": "The ceiling light does most of the work here too, but even with every lamp at full the sofa facing the TV stays below its target until the added lamp is in."
}
],
"additions": [
{
"type": "reader",
"layer": "task",
"lumens": 500,
"cct": 2700,
"near": "S1",
"why": "A reading lamp placed by the armchair by the window adds task light so the spot can reach its target."
},
{
"type": "floor",
"layer": "ambient",
"lumens": 950,
"cct": 2700,
"near": "S2",
"why": "A floor lamp placed by the sofa facing the TV adds ambient light so the spot can reach its target."
}
],
"check_first": "Keep each bulb within its fitting's maximum wattage."
}
A bedroom with bedside lamps - missing-a-layer
Input:
{
"task": "light",
"room": "Bedroom - 3.6 m wide x 3.2 m deep, ceiling 2.4 m | walls: light | name: Main bedroom | note: none",
"lamps": "L1. Pendant | Pendant | ambient | 1000 lm | 2700 K | dimmable | 1.8 m from the left wall, 1.6 m from the back wall\nL2. Bedside lamp, left | Table lamp | ambient | 400 lm | 2700 K | dimmable | 0.8 m from the left wall, 0.4 m from the back wall\nL3. Bedside lamp, right | Table lamp | ambient | 400 lm | 2700 K | dimmable | 2.8 m from the left wall, 0.4 m from the back wall\nL4. Reading lamp by the chair | Reading lamp (angled) | task | 600 lm | 2700 K | dimmable | 3.1 m from the left wall, 2.5 m from the back wall",
"spots": "S1. Left side of the bed | bedside | night | target 50-150 lux | 1.2 m from the left wall, 0.6 m from the back wall\nS2. Right side of the bed | bedside | night | target 50-150 lux | 2.4 m from the left wall, 0.6 m from the back wall\nS3. Chair in the corner | reading | evening | target 300-500 lux | 3.2 m from the left wall, 2.9 m from the back wall",
"facts": "Light at each spot with every lamp at 100% (estimated lux on the spot's surface; each lamp's share already includes its light bounced off the walls, so the shares add up; a lamp at a lower level gives its share x level / 100):\nS1 Left side of the bed (bedside, night, target 50-150): L1 55 + L2 77 + L3 9 + L4 13 = 154 at full - over by 4 (dimming brings it into range)\nS2 Right side of the bed (bedside, night, target 50-150): L1 55 + L2 9 + L3 77 + L4 13 = 154 at full - over by 4 (dimming brings it into range)\nS3 Chair in the corner (reading, evening, target 300-500): L1 29 + L2 9 + L3 9 + L4 351 = 398 at full - inside the target\nLayers: ambient: L1, L2, L3 | task: L4 | accent: none (the room has a task spot)\nMissing layers (ambient always counts; task only when there is a reading, desk or counter spot; accent always counts): accent\nDimmable: L1 yes, L2 yes, L3 yes, L4 yes\nColour temperature: L1 2700 K, L2 2700 K, L3 2700 K, L4 2700 K (spread 0 K)\nLargest single lamp: L1, 42% of all lumens\nShort spots, shortest first (light at full as a share of the target's low end): none\nVerdict: missing-a-layer (rule, checked in order: short-of-light when any spot is under its target's low end with every lamp at full; otherwise missing-a-layer when a layer above is missing; otherwise ready-for-scenes)"
}
Reply:
{
"task": "light",
"verdict": "missing-a-layer",
"headline": "Every spot in this bedroom can reach its target, but the room is missing an accent layer.",
"layers_note": "Ambient light comes from the Pendant, the Bedside lamp, left and the Bedside lamp, right, task light comes from the Reading lamp by the chair, and the room has no accent layer yet.",
"scenes": [
{
"spot": "S1",
"name": "Left side of the bed",
"time": "night",
"levels": {
"L1": 0,
"L2": 100,
"L3": 0,
"L4": 0
},
"lux": 77,
"why": "The Bedside lamp, left gives the light for the left side of the bed on its own."
},
{
"spot": "S2",
"name": "Right side of the bed",
"time": "night",
"levels": {
"L1": 0,
"L2": 0,
"L3": 100,
"L4": 0
},
"lux": 77,
"why": "The Bedside lamp, right gives the light for the right side of the bed on its own."
},
{
"spot": "S3",
"name": "Chair in the corner",
"time": "evening",
"levels": {
"L1": 10,
"L2": 0,
"L3": 0,
"L4": 100
},
"lux": 354,
"why": "The Reading lamp by the chair does most of the work for the chair in the corner, with the Pendant adding a little background ambient light."
}
],
"additions": [
{
"type": "strip",
"layer": "accent",
"lumens": 400,
"cct": 2700,
"near": "S1",
"why": "A hidden LED strip would add the accent layer this room is missing, near the left side of the bed."
}
],
"check_first": "Because one of these scenes dims a lamp partway instead of fully on or off, check that lamp has a dimmable bulb fitted to a compatible dimmer before setting up the scenes."
}
A kitchen-diner - ready-for-scenes
Input:
{
"task": "light",
"room": "Kitchen-diner - 5.0 m wide x 3.5 m deep, ceiling 2.5 m | walls: light | name: Kitchen-diner | note: Cooking in the evening; the table doubles as a desk at midday.",
"lamps": "L1. Downlight over the hob | Downlight | ambient | 500 lm | 3000 K | dimmable | 1.0 m from the left wall, 0.8 m from the back wall\nL2. Downlight over the sink | Downlight | ambient | 500 lm | 3000 K | dimmable | 2.4 m from the left wall, 0.8 m from the back wall\nL3. Strip under the cupboards | Under-cabinet strip | task | 400 lm | 4000 K | not dimmable | 1.6 m from the left wall, 0.3 m from the back wall\nL4. Pendant over the table | Pendant | ambient | 1000 lm | 2700 K | dimmable | 3.8 m from the left wall, 2.3 m from the back wall\nL5. Strip on the open shelf | Hidden LED strip | accent | 300 lm | 2700 K | dimmable | 4.9 m from the left wall, 1.0 m from the back wall\nL6. Desk lamp on the table | Desk lamp | task | 500 lm | 4000 K | dimmable | 3.2 m from the left wall, 2.8 m from the back wall",
"spots": "S1. Worktop by the hob | counter | evening | target 300-750 lux | 1.4 m from the left wall, 0.5 m from the back wall\nS2. Dining table | dining | evening | target 150-300 lux | 3.8 m from the left wall, 2.3 m from the back wall\nS3. Table as a desk | desk | midday | target 300-500 lux | 3.4 m from the left wall, 2.6 m from the back wall",
"facts": "Light at each spot with every lamp at 100% (estimated lux on the spot's surface; each lamp's share already includes its light bounced off the walls, so the shares add up; a lamp at a lower level gives its share x level / 100):\nS1 Worktop by the hob (counter, evening, target 300-750): L1 106 + L2 50 + L3 357 + L4 16 + L5 6 + L6 7 = 542 at full - inside the target\nS2 Dining table (dining, evening, target 150-300): L1 8 + L2 15 + L3 6 + L4 226 + L5 6 + L6 12 = 273 at full - inside the target\nS3 Table as a desk (desk, midday, target 300-500): L1 9 + L2 14 + L3 6 + L4 144 + L5 6 + L6 567 = 746 at full - over by 246 (dimming brings it into range)\nLayers: ambient: L1, L2, L4 | task: L3, L6 | accent: L5 (the room has a task spot)\nMissing layers (ambient always counts; task only when there is a reading, desk or counter spot; accent always counts): none\nDimmable: L1 yes, L2 yes, L3 no, L4 yes, L5 yes, L6 yes\nColour temperature: L1 3000 K, L2 3000 K, L3 4000 K, L4 2700 K, L5 2700 K, L6 4000 K (spread 1300 K)\nLargest single lamp: L4, 31% of all lumens\nShort spots, shortest first (light at full as a share of the target's low end): none\nVerdict: ready-for-scenes (rule, checked in order: short-of-light when any spot is under its target's low end with every lamp at full; otherwise missing-a-layer when a layer above is missing; otherwise ready-for-scenes)"
}
Reply:
{
"task": "light",
"verdict": "ready-for-scenes",
"headline": "The lamps already cover every spot, so what's left is choosing the right dimmer scene for each one.",
"layers_note": "Ambient light comes from the downlight over the hob, the downlight over the sink and the pendant over the table, task light comes from the strip under the cupboards and the desk lamp on the table, and accent light comes from the strip on the open shelf.",
"scenes": [
{
"spot": "S1",
"name": "Worktop by the hob",
"time": "evening",
"levels": {
"L1": 100,
"L2": 0,
"L3": 100,
"L4": 0,
"L5": 10,
"L6": 0
},
"lux": 464,
"why": "The strip under the cupboards does most of the work and stays on despite running cooler than the evening tone, because the worktop can't reach its target without it, while the downlight over the hob adds the rest of the light."
},
{
"spot": "S2",
"name": "Dining table, evening",
"time": "evening",
"levels": {
"L1": 0,
"L2": 0,
"L3": 0,
"L4": 80,
"L5": 10,
"L6": 0
},
"lux": 181,
"why": "The pendant over the table provides most of the light, dimmed for a softer evening glow, while the strip on the open shelf adds a touch of accent light."
},
{
"spot": "S3",
"name": "Table as a desk",
"time": "midday",
"levels": {
"L1": 0,
"L2": 0,
"L3": 0,
"L4": 10,
"L5": 0,
"L6": 70
},
"lux": 411,
"why": "The desk lamp on the table does most of the work, dimmed down from full so the desk doesn't run over its target, with the pendant over the table adding a little ambient fill."
}
],
"additions": [],
"check_first": "Since several of these scenes dim lamps partway rather than leaving them fully on or off, check that each dimmed lamp has a dimmable bulb fitted on a compatible dimmer before using them."
}
Build the input yourself
Load the engine in node (global.window = global; require("./lampkit.js"); require("./replykit.js")), then:
const K = LampKit, R = ReplyKit;
const built = K.buildInput(K.form({ name: "The front room", kind: "living", w: 4.2, d: 3.6, h: 2.4, walls: "mid", note: "",
lamps: [{ label: "Ceiling light", type: "ceiling", layer: "ambient", lm: 1500, k: 4000, dim: false, x: 2.1, y: 1.8 }],
spots: [{ label: "Armchair by the window", kind: "reading", when: "evening", x: 0.7, y: 0.7 }] })); // built.ok is false, with built.note, until there is a lamp and a spot
const wire = K.toWire(built.input); // POST this to /estimate and /run; built.analysis holds rows (shares, totals), layers, missing, verdict
// then: const reply = R.normalize(R.parseReply(job.output.output));
// R.check(reply, built).notes // the page's re-check of the reply
// R.md(built, reply, K.title(built.form)) // the lamps, the spots and the plan as Markdown
Lamp keys: label, type (ceiling, pendant, downlight, floor, table, reader, desk, cabinet, uplight, picture, strip), layer, lm, k, dim, and x/y in metres from the left and back walls. Spot keys: label, kind, when, x, y. Room keys: name, kind, w, d, h, walls, note.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the envelope. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="paste-your-token-here" # from https://lamp-layers.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://lamp-layers.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// lamp-layers.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "paste-your-token-here"; // from https://lamp-layers.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "paste-your-token-here" // from https://lamp-layers.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// LampLayers.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class LampLayers {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "paste-your-token-here"; // from https://lamp-layers.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "paste-your-token-here" # from https://lamp-layers.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "paste-your-token-here"; // from https://lamp-layers.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "paste-your-token-here"; // from https://lamp-layers.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"lamp-layers"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "lamp-layers"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "lamp-layers" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "lamp-layers"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "lamp-layers"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "lamp-layers" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "lamp-layers"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "lamp-layers" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "light", "room": "Living room - 4.2 m wide x 3.6 m deep, ceiling 2.4 m | walls: mid-tone | ...", "lamps": "L1. Ceiling light | Ceiling light (flush) | ambient | 1500 lm | 4000 K | not dimmable | ...", "spots": "S1. Armchair by the window | reading | evening | target 300-500 lux | ...", "facts": "...", ...}
# - see the field table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "light", "room": "Living room - 4.2 m wide x 3.6 m deep, ceiling 2.4 m | walls: mid-tone | ...", "lamps": "L1. Ceiling light | Ceiling light (flush) | ambient | 1500 lm | 4000 K | not dimmable | ...", "spots": "S1. Armchair by the window | reading | evening | target 300-500 lux | ...", "facts": "...", ...}
# - see the field table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "light", room, lamps, spots, facts} (see the field table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses lamp-layers:<task>:<inputHash>:a<attempt> (the one task is light), and its one reformat retry (with retry_note added) is attempt 2, a new key and a second run. The job's output.output is the reply text; truncated: true means the balance cut it short.
KEY="lamp-layers:light:$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
import hashlib, time
key = "lamp-layers:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply.get("verdict"), len(reply.get("scenes", [])))
import { createHash } from "node:crypto";
const key = `lamp-layers:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.verdict, (reply.scenes || []).length);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "lamp-layers:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
fmt.Println(j["charged_credits"], j["truncated"], j["output"].(map[string]any)["output"])
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "lamp-layers:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply);
require "digest"
key = "lamp-layers:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["verdict"], (reply["scenes"] || []).length].inspect
$key = "lamp-layers:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", ($reply["verdict"] ?? ""), " ", count($reply["scenes"] ?? []), "\n";
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"lamp-layers:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>());
Console.WriteLine($"{j["charged_credits"]} {reply}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: lamp-layers:light:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "light", "room": "Living room - 4.2 m wide x 3.6 m deep, ceiling 2.4 m | walls: mid-tone | ...", "lamps": "L1. Ceiling light | Ceiling light (flush) | ambient | 1500 lm | 4000 K | not dimmable | ...", "spots": "S1. Armchair by the window | reading | evening | target 300-500 lux | ...", "facts": "...", ...}
# - see the field table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "lamp-layers:light:" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `lamp-layers:light:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "lamp-layers:light:"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "lamp-layers:light:" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "lamp-layers:light:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: lamp-layers:light:" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"lamp-layers:light:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);