1 Endpoint reference
Every endpoint a user or admin account can reach, in one place. This is a lookup
chapter rather than a tutorial: the tables give the shape of each call and the one thing
about it that catches people out. The chapters before this one explain why you would make
the call at all.
1.1 Conventions
Every path in the tables is relative to /api. GET /init means
GET https://<device>/api/init. The prefix is left out to keep the columns narrow.
Authentication is a session cookie. POST /login sets session=<uuid>; send it on
everything else. A missing cookie is 400 with code 2-1-4, and an expired or unknown
one is 400 with 2-1-2. Nothing in this API answers 401, so a client that watches for
401 will never re-authenticate. An account that lacks the rights for a route gets 403 and
2-1-5.
The Role column reads: any for both user and admin, admin for admin only,
none for endpoints that work without a session at all.
Any request that carries a body must send Content-Type: application/json. Without
it the request never reaches the endpoint; you get 415 and an error that does not mention
the header you forgot. Requests without a body do not need the header.
Errors are double-encoded. The body is an object whose error field holds another
JSON document as a string:
{"error": "{\"code\": \"2-11-12\", \"message\": \"The sequence needs to be at least 8 bases long!\"}"}
Parse twice, and branch on code. A few responses put a plain string in error instead;
see the error codes chapter.
Listings are paged. GET /processRuns and GET /processRuns/queue accept from and
limit as query parameters and default to limit=15. A limit below 1 or a negative
from is an error, not a clamp.
Ids are opaque strings such as 2026-08-09T22:05:25Z-0be17442. They look like
timestamps and are not: do not parse, sort or construct them. They contain colons, so
URL-encode them if your HTTP library does not.
Timestamps are RFC 3339, durations are integer milliseconds. duration: 5400000 is
ninety minutes.
1.2 Session endpoints
| Endpoint | Role | Body | Returns | Notes |
|---|---|---|---|---|
POST /login |
none | {"username", "password"} |
session object | username is the account id, not the display name |
PUT /logout |
any | none | empty | Optional, but a reconnecting service should call it |
1.3 Device state endpoints
| Endpoint | Role | Body | Returns | Notes |
|---|---|---|---|---|
GET /init |
any or none | none | snapshot object | Without a cookie you get only status, not an error |
GET /status |
any or none | none | machine status | Unauthenticated it returns two fields |
GET /subscribe |
any | none | text/event-stream |
Sends no CORS headers; open it from a backend |
GET /init is the batch snapshot and the right way to start. For a user or admin
session its top-level keys are exactly ccSettings, errors, kbDevMode,
processRunQueue, processRuns, sensorState, session, settingsUser and status.
Without a session the whole body is {"status": {"deviceName": ..., "serverTime": ...}},
which is also what GET /status gives you unauthenticated — enough for a health check
and nothing more.
processRunQueue in that snapshot is the queue in the order the machine plans to run it.
GET /processRuns/queue returns storage order, so prefer /init when order matters.
1.4 Queue endpoints
| Endpoint | Role | Body | Returns | Notes |
|---|---|---|---|---|
POST /processRuns/queue |
any | ProcessRun template | your request echoed | The reply has no id; re-read the queue |
GET /processRuns/queue |
any | none | ProcessRun array | Storage order, not queue order |
GET /processRuns/queue/:id |
any | none | ProcessRun | Queued entries only; a started run is not here |
PUT /processRuns/queue/:id |
any | complete ProcessRun | updated entry | Fields you omit are cleared, not kept |
DELETE /processRuns/queue/:id |
any | none | empty | Cannot remove a run that already started |
POST /processRuns/queueImportBrowser |
any | {"content", "processRun"} |
concatenated objects | The reply is not valid JSON; re-read the queue |
The echo from POST /processRuns/queue is your request, not the stored entry: it carries
no id, no processIds and no duration. Read the queue afterwards and diff against the
ids you had before. That read-back is also the only confirmation that the machine could
plan the run at all.
POST /processRuns/queueImportBrowser takes a whole FASTA file in content and one
template in processRun, and queues one entry per record with the header line as its
title. One cartridge and chip pair applies to the entire file. The response body is a
series of JSON objects written one after another with no enclosing array, so most parsers
will read the first and stop or fail; ignore it.
priority is a sort key, not an address. After any change the machine renumbers the whole
queue from 1, so the number you set is not the number you will read back.
1.5 Run endpoints
| Endpoint | Role | Body | Returns | Notes |
|---|---|---|---|---|
POST /processRuns |
any | {"id": "<queue entry id>"} |
started ProcessRun | Only the first entry in the queue may start |
GET /processRuns |
any | none | ProcessRun array | Paged at 15; pass from to walk backwards |
GET /processRuns/:id |
any | none | ProcessRun | Started runs only; queued ids give 404 |
PUT /processRuns/:id |
any | ProcessRun | updated run | Applies title and metadata and nothing else |
PUT /processRuns/:id/control |
any | {"answers": {...}} |
updated run | Answer values are strings, never booleans |
Starting is the fussiest call in the API. The body only needs the id of a queue entry;
everything else in it is ignored. If that entry is not at the front of the queue you get
1-1-9, and if anything is already running you get 1-1-8. To run something sooner, move
it to the front with PUT /processRuns/queue/:id and start again.
PUT /processRuns/:id/control answers the machine's checkpoint questions. The keys are
answer ids taken from pendingAnswers in the status, and confirmations must be the
literal string "true" — a JSON boolean is rejected, and "false" on a confirmation is
rejected as well. Send one answer at a time and wait for it to leave pendingAnswers
before sending the next.
1.6 Cancel endpoints
| Endpoint | Role | Body | Returns | Notes |
|---|---|---|---|---|
GET /control/cancel |
any | none | machine status | Takes the lease and stops the machine immediately |
PUT /control/cancel/confirm |
any | none | machine status | A recovery run follows; the run is not over yet |
PUT /control/cancel/abort |
any | none | machine status | Releases the lease and lets the run continue |
Canceling is deliberately two calls. The first takes a lease: the machine goes to
emergencyStop at once and the returned status carries cancelLeaseExpiresAt, which
every connected client can see. The lease runs for five minutes. The second call either
commits the cancel or releases it.
All three refuse with 1-1-17 while any error is active, which is the opposite of what
you would expect, since an error is often exactly when you want to stop. Clear the error
first with PUT /errors/handle/:id.
1.7 Error endpoints
| Endpoint | Role | Body | Returns | Notes |
|---|---|---|---|---|
PUT /errors/handle/:id |
any | {"cancelRun", "error"} |
the stored error | The echo still says active: true; re-read instead |
POST /startEmergencyRun |
none | none | ProcessRun | Only valid in sensor emergency; otherwise 1-2-15 |
PUT /resetEmergencyMode |
none | none | empty | Resets sensor counters, not the active errors |
cancelRun decides the fate of whatever was running: true abandons it, false
acknowledges the error and leaves the run in place. error is the error object you read
from /init or the errors event, sent back whole. An unknown error id answers 404 with
the plain string form of the error envelope rather than the usual double-encoded one.
The two emergency endpoints need no session, so a person at a locked-out machine can
recover it. They are not a general stop button: startEmergencyRun only works when the
machine has already put itself into a sensor emergency state.
1.8 Settings endpoints
| Endpoint | Role | Body | Returns | Notes |
|---|---|---|---|---|
GET /settingsUser/systemInformation |
any | none | system information | Cheapest check that you are connected and authenticated |
PUT /settingsUser/systemInformation |
any | system information | stored object | Only localization and timezone, unless userCommand is set |
GET /settingsUser/oligoYield |
any | none | yield settings | Display figures only; synthesis ignores them |
PUT /settingsUser/oligoYield |
any | {"resuspensionVolume", "oligoYield"} |
stored object | Replaces both values every time |
GET /settingsUser/security |
admin | none | security settings | remoteAccess is read from disk, not stored here |
PUT /settingsUser/security |
admin | security settings | current settings | factoryReset: true erases the device |
GET /usbFiles |
admin | none | file tree | Fails with 2-14-16 when no stick is inserted |
POST /settingsUser/sslFiles |
admin | SSL file paths | your request echoed | Restarts the listener; your connection drops |
POST /settingsUser/wifiScan |
admin | {"enableWifiScan"} |
empty | Starts a scan; results arrive on another call |
POST /settingsUser/getWifiConfig |
admin | network request | network request | A read, despite being a POST with a body |
PUT /settingsUser/networkRequest |
admin | network request | your request echoed | Changing the address can cut you off mid-call |
GET /settingsUser/networkResponse |
admin | none | network settings | Live state, not what you last asked for |
GET /settingsUser/systemInformation is worth knowing well: it carries serialNr,
softwareVersion, firmwareVersion, userTimezone, systemTime, and the
recommendedCartridgeType and recommendedChipKind this device was configured for.
Quote the serial number and software version in any support request.
PUT /settingsUser/systemInformation normally stores only localization and
userTimezone. Setting userCommand to changeSystemTime makes it a clock change
instead, and then setTime must be formatted 2006-01-02 15:04:05 exactly.
1.9 Account endpoints
| Endpoint | Role | Body | Returns | Notes |
|---|---|---|---|---|
GET /users |
admin | none | user array | Password hashes are blanked; dev is hidden |
POST /users |
admin | complete user array | the array back | Every account you leave out is deleted |
POST /changeAccount/:id |
any | {"PWDNew", "PWDConfirm"} |
blanked echo | Your own id only; anything else gives 2-16-10 |
POST /confirmPWD/:id |
admin | {"username", "password"} |
empty | Checks a password and changes nothing |
POST /users is a whole-collection replace, not a create. Read GET /users, modify the
array, and post all of it back. An account missing from the array you send is deleted, so
a client that posts one new user wipes every other account. Leave password empty on an
existing account to keep the current one; a non-empty password is hashed and replaces
it. Accounts with the dev role cannot be created and are never deleted this way.
Ordinary users change their own password through POST /changeAccount/:id, where :id is
their own account id. An admin cannot use it to change somebody else's.
1.10 Object fields
ProcessRun
One entry in the queue, and the same object once it has started. On a queue POST the machine keeps only the fields marked below and rebuilds everything else from its own state, so there is no point sending more.
| Field | Type | Set by client | Meaning |
|---|---|---|---|
id |
string | no | Opaque identifier, assigned when the entry is stored |
createdAt |
timestamp | no | When the entry was created |
updatedAt |
timestamp | no | Last modification |
title |
string | yes | Free text. Defaults to a generated name such as Oligo 2026-08-09.3 |
processType |
string | yes | synthesis for an oligo. Other values drive machine operations such as manualCleaning |
cartridgeType |
string | yes | Numeric string. 2 is the standard cartridge, 3 the 6-FAM one |
chipKind |
string | yes | Chip identifier. 2 is the standard chip, 3-BHQ1 the dual-labeled one |
priority |
number | yes | Position in the queue. Renumbered from 1 after every change |
answers |
object | sequence only |
On a queue POST, only answers.sequence is meaningful. Later answers go through the control endpoint |
metadata |
array | on started runs | Free-form labels. See below |
processIds |
array | no | The processes the machine planned to reach your run |
processTypes |
array | no | One entry per planned process, in order |
cartridgeId |
string | no | The cartridge that was in the machine when the run was planned or started |
cartridgeExpiresAt |
timestamp | no | Expiry of that cartridge |
totalBasesCountBefore |
number | no | Bases left on the cartridge before this run |
duration |
number | no | Estimated total, in milliseconds |
durations |
array | no | Estimate per planned process, in milliseconds |
startedAt |
timestamp | no | When the run left the queue |
finishedAt |
timestamp | no | While running this is the projected end, not a fact. It becomes the real time when the run ends |
abortedAt |
timestamp | no | Set instead of finishedAt when a run is canceled |
isContinuedRun |
boolean | no | The run followed straight on from another |
isCanceling |
boolean | no | A cancel is in progress for this run |
PUT /processRuns/queue/:id accepts title, metadata, answers, cartridgeType,
chipKind and priority, and overwrites each of them with what you send — including with
nothing. PUT /processRuns/:id, on a run that has already started, applies title and
metadata only.
Metadata
A list of small labeled values carried with a run, seeded from a device-wide default list. Use it to write your own order number or sample id onto a run.
| Field | Type | Set by client | Meaning |
|---|---|---|---|
id |
string | yes | Your key for the entry |
name |
string | yes | Label shown at the Kilobaser |
type |
string | yes | Free text describing the kind of value |
data |
string | yes | The value itself, always a string |
unit |
string | yes | Optional unit |
Metadata written to a queued entry does not survive. The machine rebuilds queue
entries whenever anything in the queue changes, and the rebuild does not carry your
metadata across. It sticks only once the run has started, through PUT /processRuns/:id.
If you need an external identifier on a queued entry, put it in title.
Session
Returned by POST /login and repeated under session in /init.
| Field | Type | Set by client | Meaning |
|---|---|---|---|
id |
string | no | The session id, the same value as the cookie |
createdAt |
timestamp | no | When you logged in |
expiresAt |
timestamp | no | One month out. Optimistic, see below |
deviceName |
string | no | The Kilobaser that issued the session |
user.id |
string | no | The account id, which is what you log in with |
user.name |
string | no | Display name, which is often not the id |
user.fullName |
string | no | Longer name, may be empty |
user.role |
string | no | user or admin |
Sessions live in the device's memory. A restart or a software update destroys all of them
long before expiresAt, so treat that field as an upper bound and handle 2-1-4 and
2-1-2 at any moment.
Machine status
The object under status in /init, returned by GET /status, and pushed as the
status event. These are the fields a client actually reads; the rest are for the
Kilobaser's own interface.
| Field | Type | Set by client | Meaning |
|---|---|---|---|
deviceName |
string | no | The Kilobaser's name |
serverTime |
timestamp | no | Device clock at the moment of the reply |
currentState.mode |
string | no | What the machine is doing. ready when idle, then working, synthesis, check, canceling, emergencyStop and others |
currentState.cartridgeType |
string | no | Cartridge the machine believes is loaded |
currentState.chipKind |
string | no | Chip the machine believes is loaded |
currentProcessRunId |
string | no | The run in progress. Empty when nothing is running |
pendingAnswers |
array | no | Answer ids the machine is waiting for. Empty most of the time |
pendingProcessRunIds |
array | no | Runs queued to start immediately after this one |
currentRemainingTime |
number | no | Milliseconds left in the current step, when known |
currentCheckpointProgress |
number | no | How far the current step has got |
currentRunIndex |
number | no | Which of runIds is executing |
runIds |
array | no | The planned processes of the current run |
cancelLeaseExpiresAt |
timestamp | no | Present only while a cancel lease is held |
lidOpenWarningTime |
timestamp | no | Set when the lid has been open too long |
supportedOPModes |
object | no | Cartridge type to list of chip kinds. What this device can actually run |
currentHWDState.chipKind |
string | no | Chip the sensor sees right now |
currentHWDState.cartridgePresent |
boolean | no | Whether a cartridge is physically in place |
currentHWDState.pressureHigh |
number | no | Supply pressure reading |
updateRunning |
boolean | no | A software update is running; expect a restart |
versionError |
object | no | Non-empty when the device configuration does not match the software |
kbDevMode |
string | no | Developer mode of the device. Kilobaser reports the empty value |
A checkpoint is currentState.mode == "check" and a non-empty pendingAnswers.
Testing pendingAnswers alone will make you answer while the machine is still moving.
1.11 HTTP status codes
| Status | What it means here |
|---|---|
| 200 | Success. Some endpoints answer with an empty body, and a few echo what you sent rather than what was stored |
| 400 | The catch-all failure: bad input, wrong machine state, and a missing or expired session |
| 403 | Wrong credentials on login, or a role without rights for this route |
| 404 | An id that does not exist in the collection this endpoint looks in |
| 409 | A physical condition blocks the request, such as the lid or the cartridge not being where it must be |
| 415 | A body arrived without Content-Type: application/json |
| 500 | The device failed internally. A handful of plainly invalid requests also land here |
| 503 | The Kilobaser is still starting up. Retry-After is set and the body carries a state field |
The one worth repeating: there is no 401. An expired session is a 400, in exactly the
same shape as a malformed sequence. A client that decides when to log in again by looking
at the status code will loop forever; decide by looking at the code.
Do not infer meaning from the status alone in the other direction either. 2-16-18, a
username with invalid characters, is a 500. 2-11-34, starting a run that is not in the
queue, is a 404. The code is the reliable part.
1.12 Endpoints this guide does not describe
The device serves a second set of routes used by Kilobaser to develop and service the
Kilobaser: /processes, /protocols, /runs, /ccsettings, /instructions,
/functions, /valveMappings, /settings, /fakehwd, /setfakehwd, /clearhwdqueue,
/resetSettings, /errors/resumeProtocolCall, /errors/makeErrorCall,
/errors/removeErrorCall and /errorSettings.
They are developer-only. A user or admin session gets 403 and 2-1-5 from all of
them, they are not part of the supported interface, and they change without notice. They
appear in older reference material, which is the only reason they are named here. Nothing
in this guide needs them.
Two further endpoints exist only on the Kilobaser's own touchscreen port and are not
reachable over the network at all: importing a sequence file from a USB stick, and reading
a file off that stick. Use POST /processRuns/queueImportBrowser instead, which takes the
file contents in the request.