← IoT Island
IoT Island API
Tokens

Drive IoT Island from your own code

The one metered step - the check of one household's device list and router settings - is available over HTTP. A natural use is a home-inventory spreadsheet or a router-setup helper: your code sends the device list with the page's map, and gets back a zone for every device (trusted, smart devices, guest & work), an allow or block decision for each link that crosses zones, the router changes, the steps in order and a sound / split / lockdown verdict.

The split first: the page maps; the model judges; the page checks the model's plan against the map. Reading the list, guessing each device's kind, the proposed zones, today's mixed networks, the classified links and the setting ratings come from netkit.js, which runs in node as well as in the browser. Its check function then holds the reply to the list: every placement must name a device on the list and quote its line as written, devices the reply skips keep the page's zone, every link that still crosses zones after the reply's placements needs a decision, every setting rated warn or bad must be addressed, and a verdict that ignores a critical setting is flagged.

Get a token on the token page (step 1 below also shows how to mint a guest token in code).

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": "...", "status": 402, "details": { ... } } }

The token is minted for this app (the guest call takes {"slug":"iot-island"}), so no slug header is needed afterwards. Send it as Authorization: Bearer …. The run body is the input object, sent bare: never wrap it as {"input": {…}}.

StatusCodeMeaning
400validation_errorThe body is not a JSON object, or task is missing.
401unauthorizedNo token, or a stale one. Mint a guest token or sign in again.
402insufficient_creditsThe balance is under min_credits. Price with /estimate first.
403forbiddenA guest token tried to run: the check is metered and needs a personal token.
404not_foundUnknown job id.
429rate_limitedBack off and retry.
5xxserver_errorTransient. Retry with the same Idempotency-Key so a retry never double-bills.

The task field comes first

Every run sends task. There is one task, split. It returns title, verdict, summary; zones [{device, zone, says, why}] (one per device, in the list's order); links [{from, to, decision, how, why}] (one per link that crosses zones); settings [{id, change_to, why}]; steps (3-7, in order); caveats (0-3).

VerdictMeaning
soundNo critical setting, no network mixing trusted devices with smart or guest ones, no setting worth changing: the household is already split.
splitNo critical setting, but a mixed network, a smart device that needs to reach into the trusted zone, or a setting to change (WPS, UPnP, WPA2 only, no second network).
lockdownA critical setting - the default admin password, open or WEP security, admin from the internet, firmware out of support. Fix those first; the split comes next.
FieldValuesWhat the page does with it
zones[].zonetrusted, smart, guestRedraws the plan with that device on that island. A camera, doorbell, lock, light, plug, sensor or hub kept in trusted is flagged.
links[].decisionallow, blockDraws the link green with an arrow (allowed one way) or red dashed with a cross. Allowing a smart device into trusted, or a link across the guest zone, is flagged.
settings[].idwpa, admin, wps, upnp, firmware, remote, extra_netMatched to the page's rating; a setting rated warn or bad that the reply leaves out is flagged.

The declared input schema carries scalars only, so object-valued fields travel JSON-encoded as strings (NetKit.toWire does this). Only task is required by the schema.

Input fields

FieldTypeNotes
taskstringAlways split.
todaystringYYYY-MM-DD.
devicesstringThe device list as pasted (device | kind | network now | needs to reach). Past 6,000 characters the middle is cut on whole lines around a marker.
factsstring (JSON){household, devices, by_zone, kinds_guessed, networks[{name, devices, trusted, smart, guest, mixed}], mixed_networks, smart_with_trusted, guest_with_trusted, links[{from, to, from_zone, to_zone, kind}], settings[{id, label, value, state}], critical, needs_attention, unknown}. Link kind is same, one-way, risky or guest; setting state is good, ok, warn, bad or unknown.
device_liststring (JSON)[{name, kind, kind_guessed, page_zone, network_now, needs}].
page_readsstring (JSON)[{level, code, text}]; level is fix or info.
page_verdictstringThe page's own reading: sound, split or lockdown.
notesstringOptional, the household's own comments; past 1,200 characters the middle is cut.
retry_notestringOnly on a reformat retry.

The reply

The job's output.output is a string holding one JSON object. Parse it with NetKit.parseReply, then normalize, then check it with check(reply, state), which returns {rows, final, links, after, cross, settings, byZone, notes}: one row per placement (or why it was not counted), the final zone of every device, the reply's link decisions with the kind each link has under those zones, the setting changes beside the page's ratings, and the notes (ok, info, warn, fix) the page shows under "Checked by the page".

Worked examples

Replies produced against the app's prompt for the sample homes (made-up households and devices; lists shortened; the full replies ship in example-result.js). The page replays each of them free from its sample buttons. Inputs are shown with the long fields folded.

task: split - Studio, already split - sound

Input:

{
 "task": "split",
 "today": "2026-10-20",
 "devices": "My laptop | laptop | Home |\nMy phone | phone | Home | Living room TV\nPartner's tablet | tablet | Home | Living room TV\nLiving room TV | tv | Things |\nKitchen sp ... (290 characters)",
 "facts": "{\"household\":\"Studio apartment, two of us\",\"devices\":8,\"by_zone\":{\"trusted\":3,\"smart\":4,\"guest\":1},\"kinds_guessed\":0,\"networks\":[{\"name\":\"Home\",\"devices\":3,\"trusted\":3,\"smart\":0,\"guest\":0,\"mixed\":false},{\"name\":\"Things\",\"devices\":4,\"trusted\":0,\"smart\":4,\"guest\":0,\"mixed\":false},\"... 1 more\"],\"mixed_networks\":0,\"smart_with_trusted\":[],\"guest_with_trusted\":[],\"links\":[{\"from\":\"My phone\",\"to\":\"Living room TV\",\"from_zone\":\"trusted\",\"to_zone\":\"smart\",\"kind\":\"one-way\"},{\"from\":\"Partner's tablet\",\"to\":\"Living room TV\",\"from_zone\":\"trusted\",\"to_zone\":\"smart\",\"kind\":\"one-way\"}],\"settings\":[{\"id\":\"wpa\",\"label\":\"Wireless security\",\"value\":\"WPA3\",\"state\":\"good\"},{\"id\":\"admin\",\"label\":\"Router admin password\",\"value\":\"Changed from the default\",\"state\":\"good\"},\"... 5 more\"],\"critical\":[],\"needs_attention\":[],\"unknown\":[]}",
 "device_list": "[{\"name\":\"My laptop\",\"kind\":\"computer\",\"kind_guessed\":false,\"page_zone\":\"trusted\",\"network_now\":\"Home\",\"needs\":[]},{\"name\":\"My phone\",\"kind\":\"phone\",\"kind_guessed\":false,\"page_zone\":\"trusted\",\"network_now\":\"Home\",\"needs\":[\"Living room TV\"]},\"... 6 more\"]",
 "page_reads": "[{\"level\":\"info\",\"code\":\"one-way\",\"text\":\"2 links from trusted to smart (My phone to Living room TV and Partner's tablet to Living room TV) will need a one-way allowance after the split - casting and printing usually do.\"}]",
 "page_verdict": "sound",
 "notes": "We cast from our phones to the TV most nights."
}

Reply:

{
 "task": "split",
 "title": "Studio Apartment Already Split And Sound",
 "verdict": "sound",
 "summary": "The studio's three networks already match the right split: Home holds the three trusted devices, Things holds the four smart devices, and Visitors holds the guest phone, with no mixed networks and all seven router settings already good. Nothing critical needs fixing and no smart device needs to reach into the trusted zone, so the network is sound as it stands. The one useful change is a narrow one-way allow rule so the phone and tablet can keep casting to the Living room TV without opening any path back the other way.",
 "zones": [
  {
   "device": "My laptop",
   "zone": "trusted",
   "says": "My laptop | laptop",
   "why": "a household computer, already on the Home network"
  },
  {
   "device": "My phone",
   "zone": "trusted",
   "says": "My phone | phone",
   "why": "a personal phone that casts to the TV"
  },
  {
   "device": "Partner's tablet",
   "zone": "trusted",
   "says": "Partner's tablet | tablet",
   "why": "the partner's own tablet, used to cast to the TV"
  },
  "..."
 ],
 "links": [
  {
   "from": "My phone",
   "to": "Living room TV",
   "decision": "allow",
   "how": "one-way rule from Trusted to Smart; turn on mDNS / discovery forwarding if casting fails",
   "why": "lets the phone start a cast to the TV without the TV reaching back into the trusted network"
  },
  {
   "from": "Partner's tablet",
   "to": "Living room TV",
   "decision": "allow",
   "how": "one-way rule from Trusted to Smart; turn on mDNS / discovery forwarding if casting fails",
   "why": "lets the tablet start a cast to the TV without the TV reaching back into the trusted network"
  }
 ],
 "settings": [],
 "steps": [
  "Keep the three networks as they are - Home for the trusted devices, Things for the smart devices, Visitors for the guest phone.",
  "Add a one-way allow rule from Home to Things so the phone and tablet can start a connection to the Living room TV.",
  "Leave Things unable to start a connection back into Home.",
  "..."
 ],
 "caveats": [
  "Router menu names for a one-way inter-network rule differ between routers.",
  "If casting doesn't find the TV right away, check that discovery forwarding between the two networks is on."
 ]
}

task: split - Family house, one network - split

Input:

{
 "task": "split",
 "today": "2026-10-20",
 "devices": "Dad's laptop | laptop | Home | Printer\nMum's phone | phone | Home | Living room TV\nKids' tablet | tablet | Home | Living room TV\nFamily NAS | storage | Home |\nL ... (454 characters)",
 "facts": "{\"household\":\"Family of four, two floors\",\"devices\":13,\"by_zone\":{\"trusted\":4,\"smart\":8,\"guest\":1},\"kinds_guessed\":0,\"networks\":[{\"name\":\"Home\",\"devices\":13,\"trusted\":4,\"smart\":8,\"guest\":1,\"mixed\":true}],\"mixed_networks\":1,\"smart_with_trusted\":[\"Living room TV\",\"Game console\",\"Video doorbell\",\"Backyard camera\",\"Hallway thermostat\",\"Smart plugs (4)\",\"Light hub\",\"Printer\"],\"guest_with_trusted\":[\"Work laptop\"],\"links\":[{\"from\":\"Dad's laptop\",\"to\":\"Printer\",\"from_zone\":\"trusted\",\"to_zone\":\"smart\",\"kind\":\"one-way\"},{\"from\":\"Mum's phone\",\"to\":\"Living room TV\",\"from_zone\":\"trusted\",\"to_zone\":\"smart\",\"kind\":\"one-way\"},\"... 2 more\"],\"settings\":[{\"id\":\"wpa\",\"label\":\"Wireless security\",\"value\":\"WPA2/WPA3 mixed\",\"state\":\"ok\"},{\"id\":\"admin\",\"label\":\"Router admin password\",\"value\":\"Changed from the default\",\"state\":\"good\"},\"... 5 more\"],\"critical\":[],\"needs_attention\":[\"wps\",\"upnp\"],\"unknown\":[]}",
 "device_list": "[{\"name\":\"Dad's laptop\",\"kind\":\"computer\",\"kind_guessed\":false,\"page_zone\":\"trusted\",\"network_now\":\"Home\",\"needs\":[\"Printer\"]},{\"name\":\"Mum's phone\",\"kind\":\"phone\",\"kind_guessed\":false,\"page_zone\":\"trusted\",\"network_now\":\"Home\",\"needs\":[\"Living room TV\"]},\"... 11 more\"]",
 "page_reads": "[{\"level\":\"fix\",\"code\":\"mixed\",\"text\":\"'Home' carries your trusted devices alongside 9 smart or guest devices: Living room TV, Game console, Video doorbell, Backyard camera, Hallway thermostat and 4 more.\"},{\"level\":\"fix\",\"code\":\"warn-wps\",\"text\":\"WPS is on - its PIN can be guessed; pair devices with the password instead.\"},\"... 3 more\"]",
 "page_verdict": "split",
 "notes": "I work from home two days a week on the work laptop. Someone told me the console needs UPnP for online games. The backyard camera saves clips to the NAS."
}

Reply:

{
 "task": "split",
 "title": "Family of Four's Mixed Home Network",
 "verdict": "split",
 "summary": "This family-of-four network needs a split, not a lockdown: the router's admin password, firmware updates and remote access are already set correctly, so nothing critical has to be fixed first. All 13 devices sit on one 'Home' network today - the 4 trusted devices share it with 8 smart devices and the work laptop - and WPS and UPnP are both left on. The single most useful change is turning on the guest network, the router's only second network, and moving every smart device and the work laptop onto it, keeping the one-way casting and printing allowances and a single narrow rule for the camera's clips to the NAS.",
 "zones": [
  {
   "device": "Dad's laptop",
   "zone": "trusted",
   "says": "Dad's laptop | laptop",
   "why": "a household laptop that needs to reach the printer, not a smart or guest device"
  },
  {
   "device": "Mum's phone",
   "zone": "trusted",
   "says": "Mum's phone | phone",
   "why": "a household phone casting to the living room TV"
  },
  {
   "device": "Kids' tablet",
   "zone": "trusted",
   "says": "Kids' tablet | tablet",
   "why": "a household tablet casting to the living room TV"
  },
  "..."
 ],
 "links": [
  {
   "from": "Dad's laptop",
   "to": "Printer",
   "decision": "allow",
   "how": "one-way rule from Trusted to Smart so the laptop can print; the printer may not start a connection back",
   "why": "printing only needs the laptop to initiate the connection"
  },
  {
   "from": "Mum's phone",
   "to": "Living room TV",
   "decision": "allow",
   "how": "one-way rule from Trusted to Smart for casting; turn on mDNS / discovery forwarding if casting fails",
   "why": "casting only needs the phone to start the connection, not the TV reaching back into trusted"
  },
  "..."
 ],
 "settings": [
  {
   "id": "wps",
   "change_to": "off",
   "why": "WPS's PIN can be guessed; devices should pair with the network password instead"
  },
  {
   "id": "upnp",
   "change_to": "off first; if online play breaks, open only the specific ports the console's own support page lists",
   "why": "UPnP lets any device open a port to the internet by itself, and the console may not actually need it"
  },
  {
   "id": "extra_net",
   "change_to": "turn on the guest network and use it as the smart devices' network, since it's the only second network this router can make",
   "why": "there's no way to make a separate IoT network, so splitting trusted from smart has to use the existing guest network"
  }
 ],
 "steps": [
  "Turn off WPS and turn off UPnP on the router tonight, before anything else.",
  "Turn on the guest network - the router's only second network - to become the home for every smart device and the work laptop.",
  "Move the smart devices onto it one at a time (TV, console, doorbell, camera, thermostat, plugs, hub, printer), re-pairing each as it asks; move the work laptop onto it too.",
  "..."
 ],
 "caveats": [
  "The work laptop and every smart device end up sharing the same guest network, since this router can only make one extra network.",
  "Most smart devices will need to be re-paired to the new network name once they move.",
  "..."
 ]
}

task: split - Shared flat, router as delivered - lockdown

Input:

{
 "task": "split",
 "today": "2026-10-20",
 "devices": "Laptop | laptop | |\nPhone | phone | | Smart TV\nOld tablet | tablet | |\nFlatmate's PC | computer | |\nSmart TV | tv | |\nHallway camera | camera | |\nFront door sma ... (282 characters)",
 "facts": "{\"household\":\"Shared flat, three flatmates\",\"devices\":10,\"by_zone\":{\"trusted\":4,\"smart\":5,\"guest\":1},\"kinds_guessed\":0,\"networks\":[{\"name\":\"Main network\",\"devices\":10,\"trusted\":4,\"smart\":5,\"guest\":1,\"mixed\":true}],\"mixed_networks\":1,\"smart_with_trusted\":[\"Smart TV\",\"Hallway camera\",\"Front door smart lock\",\"Cheap smart plugs\",\"Air purifier\"],\"guest_with_trusted\":[\"Visiting friend's laptop\"],\"links\":[{\"from\":\"Phone\",\"to\":\"Smart TV\",\"from_zone\":\"trusted\",\"to_zone\":\"smart\",\"kind\":\"one-way\"},{\"from\":\"Visiting friend's laptop\",\"to\":\"Smart TV\",\"from_zone\":\"guest\",\"to_zone\":\"smart\",\"kind\":\"guest\"}],\"settings\":[{\"id\":\"wpa\",\"label\":\"Wireless security\",\"value\":\"WPA2 only\",\"state\":\"warn\"},{\"id\":\"admin\",\"label\":\"Router admin password\",\"value\":\"Still the default / on the sticker\",\"state\":\"bad\"},\"... 5 more\"],\"critical\":[\"admin\",\"firmware\",\"remote\"],\"needs_attention\":[\"wpa\",\"wps\",\"upnp\"],\"unknown\":[\"extra_net\"]}",
 "device_list": "[{\"name\":\"Laptop\",\"kind\":\"computer\",\"kind_guessed\":false,\"page_zone\":\"trusted\",\"network_now\":\"Main network\",\"needs\":[]},{\"name\":\"Phone\",\"kind\":\"phone\",\"kind_guessed\":false,\"page_zone\":\"trusted\",\"network_now\":\"Main network\",\"needs\":[\"Smart TV\"]},\"... 8 more\"]",
 "page_reads": "[{\"level\":\"fix\",\"code\":\"critical-admin\",\"text\":\"The router admin password is still the default - anyone on the network (or a guest) can try the one printed on the sticker.\"},{\"level\":\"fix\",\"code\":\"critical-firmware\",\"text\":\"The router no longer gets firmware updates - known holes stay open; plan a replacement.\"},\"... 9 more\"]",
 "page_verdict": "lockdown",
 "notes": "The router came with the flat. I can log into it with the password on the sticker. Nobody has changed anything since we moved in."
}

Reply:

{
 "task": "split",
 "title": "Shared Flat Network Needs Lockdown First",
 "verdict": "lockdown",
 "summary": "This is a lockdown: the router's admin password is still the default, admin is reachable from the internet, and the firmware no longer updates, so those come before anything else. All 10 devices currently sit on one 'Main network,' putting 6 smart and guest devices alongside the 4 trusted ones. Once the critical settings are fixed, splitting the smart devices onto their own network - while keeping the one-way Phone-to-Smart-TV cast working - finishes the job. The single most useful change tonight is replacing the admin password and turning off remote admin access, since right now anyone who knows the sticker password could manage the router from anywhere.",
 "zones": [
  {
   "device": "Laptop",
   "zone": "trusted",
   "says": "Laptop | laptop",
   "why": "a household computer belongs with the trusted devices"
  },
  {
   "device": "Phone",
   "zone": "trusted",
   "says": "Phone | phone",
   "why": "a personal phone is trusted hardware"
  },
  {
   "device": "Old tablet",
   "zone": "trusted",
   "says": "Old tablet | tablet",
   "why": "a tablet stays with the household's own devices"
  },
  "..."
 ],
 "links": [
  {
   "from": "Phone",
   "to": "Smart TV",
   "decision": "allow",
   "how": "One-way rule from Trusted to Smart; turn on mDNS / discovery forwarding if casting fails.",
   "why": "The Phone starting a cast to the Smart TV is a trusted device reaching into smart, the normal one-way allowance."
  },
  {
   "from": "Visiting friend's laptop",
   "to": "Smart TV",
   "decision": "block",
   "how": "Keep the guest network internet-only; don't open a route from the guest laptop to the Smart TV.",
   "why": "Anything crossing into the guest zone is blocked by default so visiting devices only reach the internet, keeping the smart and trusted devices safe from guest hardware."
  }
 ],
 "settings": [
  {
   "id": "admin",
   "change_to": "a long unique password stored in a password manager",
   "why": "The router admin password is still the default / on the sticker, so anyone who sees it can log in and change settings."
  },
  {
   "id": "firmware",
   "change_to": "updated firmware, or a replacement router if updates are no longer available",
   "why": "The router no longer gets firmware updates, leaving known security holes open."
  },
  {
   "id": "remote",
   "change_to": "off",
   "why": "Admin access from the internet is on, letting anyone outside try to reach the router's admin page."
  },
  "..."
 ],
 "steps": [
  "Change the router admin password from the sticker default to a long unique one, and turn off admin access from the internet.",
  "Note that the firmware no longer updates; plan to replace the router since this can't be fixed from the admin page alone.",
  "Create a separate network (or VLAN) for the smart devices, apart from the trusted devices and from the guest network.",
  "..."
 ],
 "caveats": [
  "Router menus and setting names differ between routers.",
  "Some smart devices only join 2.4 GHz networks, so check before moving them.",
  "..."
 ]
}

Build the input yourself

Load the script in node (global.window = {}; require("./netkit.js")), then:

const K = NetKit;
const st = K.state({
  today: "2026-10-20", household: "Family of four",
  wpa: "mixed", admin: "changed", wps: "on", upnp: "on", firmware: "auto", remote: "off", extra_net: "guest",
  notes: "", devices: "Dad's laptop | laptop | Home | Printer\nPrinter | printer | Home |\nVideo doorbell | doorbell | Home |"
});
console.log(st.nets, st.links, st.verdict);
const { input } = K.buildInput(st, { today: "2026-10-20" });
const wire = K.toWire(input);           // POST this to /estimate and /run
// later: const reply = K.normalize(K.parseReply(job.output.output));
// const { final, links, settings, notes } = K.check(reply, st);

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://iot-island.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"}
}

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":"iot-island"}' | jq -r '.data.token')

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}' 

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": "split", "today": "2026-10-20", "devices": "Backyard camera | camera | Home | Family NAS ...", "facts": "{...}", "device_list": "[...]", "page_reads": "[...]", "page_verdict": "split"}
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}' 

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 iot-island:split:<inputHash>:a<attempt>, 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="iot-island:split:$(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}' 

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: iot-island:split:$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json