Skip to content

Access from outside the SPA

Most people meet Helix as the operator’s screen. But the same system is reachable from a shell script, a nightly job, or the legacy host you are integrating with — over the same HTTP API the screen itself uses. There is no second, lesser API for machines.

This page is for whoever is writing that caller.

Helix authenticates with a session cookie, not an API key, and not a bearer token. Your client needs a cookie jar; in curl that is -c to save and -b to send.

Terminal window
curl -s -c jar.txt -X POST https://helix.example.com/auth/login \
-H 'Content-Type: application/json' \
-d '{"name":"nightly-import","password":"…","remember":true}'
{ "status": "success", "user": { "id": 42, "name": "nightly-import", "permissions": ["dispatch"] } }

Then send the jar with everything else:

Terminal window
curl -s -b jar.txt -X POST https://helix.example.com/json \
-H 'Content-Type: application/json' \
-d '{"update":[{"project":{"id":118,"data":{"note":"seen by the importer"}}}]}'

"remember": true matters for a script. Without it the cookie has no expiry and is a session cookie — fine for a browser, awkward for a process that has no session. With it the cookie carries an 8-hour lifetime, which matches the token inside it.

A few consequences worth knowing before you build on it:

  • The session lasts 8 hours and is not renewed by use. A long-running process must sign in again. The signal is a 401, and the right reaction to a 401 is always “log in, then retry once”.
  • There is no API key. Your client holds a username and password. Give it its own user account — not a person’s — so that the audit trail names the integration rather than whoever’s credentials it borrowed, and so that revoking it costs nobody their login.
  • Grant only the permissions it needs (customer, dispatch, admin, privileged, contact, engineer). An importer that writes projects needs dispatch; it almost certainly does not need privileged, which is the permission that can stop nodes.
  • POST /auth/logout ends the session at the client end. The token stays technically valid until it expires — it is stateless — so treat logout as tidiness, not revocation.

You can check a session at any time with curl -s -b jar.txt https://helix.example.com/auth/me.

Any of them. Every node serves the whole system, and a write is forwarded internally to whichever node is currently the writer. Nothing at your end changes when that writer moves. On a single-node deployment there is nothing further to think about, and you can stop reading here.

In a cluster you can do better than “any”, because the nodes will tell you where you ought to be:

Terminal window
curl -s -b jar.txt https://helix.example.com/api/bind
{
"preferred": "https://leeds1.example.com:8001",
"why": "network",
"members": [
{ "url": "https://london1.example.com:8001", "site": "london", "clients": 12 },
{ "url": "https://leeds1.example.com:8001", "site": "leeds", "clients": 3 }
]
}

Use preferred and keep members as your fallback list: if the node you are using stops answering, work down the rest until one does. That list is also the honest answer to “what should I put in my configuration?” — a single hostname, or several, and the cluster corrects you from there.

why says which rule produced the recommendation, which is useful when it surprises you:

whywhat it means
learneda browser on your network measured the nodes and this was the fastest for it
networkyour address is in the same IP network as that node’s
answeringnothing is known about you, so you were offered the site you reached
loadno locality is configured anywhere; you were sent to the least busy node
only-memberthere is one node to offer

Two things follow from learned that are worth knowing. The cluster gets better at placing you over time, because the operator browsers measure the nodes and report what they find — so a script that never measures anything still benefits from what the office around it discovered. And an administrator can overrule all of it with HELIX_BIND_ALLOW on a node, which restricts what that node will offer; the restriction is applied at the server, so a client cannot opt out of it.

/api/bind needs a session, like everything else — node addresses are not published to anonymous callers. Sign in first, then ask.

POST /json is the command surface, and it takes the same commands the operator’s screen sends. The body is one JSON object; several commands can travel in one call.

Terminal window
curl -s -b jar.txt -X POST https://helix.example.com/json \
-H 'Content-Type: application/json' \
-d '{"add":[{"project":{"type":"breakdown","cust":41,"loc":{"at":[53.79,-1.54]}}}]}'

The reply is a status object; anything other than success carries a message saying why.

Mutations need a session; reads do not. add, update, delete, coerce and force are refused with 401 unless you are signed in, because each one is recorded in the audit trail against a named user.

Expect 503, and retry. A write needs a live optimiser. While one is starting, restarting or has failed, writes are refused with 503 and the message says so. It is a “try again shortly”, not a failure — nothing was saved, and the same request will work once the optimiser is back. A few seconds is the usual wait. Build the retry in; it is the one transient condition a caller will actually meet in normal running.

set: {schedule: …} is refused from outside. It is Helix’s own internal command and sending it would leave every connected client waiting on a rebuild that is not happening.

A Helix node can be run as a fed primary: the whole stack, but with the command stream arriving on standard input from a host application rather than from people and scripts. The host pipes its transactions in and reads the results back out, and Helix persists, serves and displays what passes through. This is how a legacy system adopts Helix without moving anything at first.

The API and the operator’s screen are both there on such a node, and everything above still applies. One thing is different, and it is the important one: the host owns the stream, so what a client may change is the host’s decision, not yours or ours. It declares that in its domain file — a list of entities the console and the API may not write while it is feeding.

Ask for one of those and you get 403, naming the entity:

{ "status": "refuse", "code": 403, "message": "resource cannot be edited while Helix is fed from stdin" }

It is a policy answer, not a transient one: retrying will not help, and the list only changes when the deployment’s owner changes it. Everything not on the list behaves exactly as this page describes. On a typical deployment that means the host keeps what its optimiser plans — resources, projects, desks — while Helix keeps what the host has no opinion about.

The list refuses authoring — add, update and delete. A force or coerce is a placement (a stop’s resource, departure, lock; a resource’s desk), which Spiral takes from any feeder and the host reads back tagged 0, so it passes whatever the list says — the console’s drag-and-drop and Auto Allocate ride on it. See helix.fed.

A write you ask to have tagged comes back as tag 0. Tags number the host’s own transactions, and Helix will not issue one from a sequence that is not its to issue. Tag 0 is the agreed marker for “this transaction is not yours” — it lets the host recognise a change that came in beside its feed, and ignore it. Ask for no tag and the write carries none.

Notification replies and inbound messages arrive on their own doors, and these are deliberately not session-authenticated — a telephony or messaging provider cannot hold a cookie:

  • POST /notify/reply/:token — authenticated by the token itself: 192 random bits, single use, expiring.
  • POST /notify/inbound/:channel — authenticated by the channel adapter’s own signature check (Twilio HMAC, Meta sha256), because the signing credentials belong to the client, not to Helix.

These are for providers. Do not use them as a general way in.

A non-expiring machine credential. Today an integration stores a password and signs in again every eight hours. That works, and it is what the sections above describe, but it is not what you would design for a machine. If this matters to your deployment, say so — it is a known gap rather than a settled decision.