helixordevelopers

HTTP API

The decision service puts the embedded engine behind REST, Server-Sent Events and WebSocket, so services in any language can use it. This page lists every endpoint, its JSON and its current limits.

Runtime 0.2.1Service version 1.1.0

About the example

The examples on this page use the built-in data-protection pack so you can run everything without writing a pack first. The same calls work for any decision pack: eligibility, limits, routing and so on. See Core concepts.

Running the service#

HowEngineDefault address
helixor-pack serve --pack … --license …Your compiled pack127.0.0.1:18734
python -m helixor_runtime.server [--pack … --license …]Your compiled pack, or the built-in example pack without --pack127.0.0.1:18734
start_server(engine) from PythonThe engine you pass; the built-in example pack if none127.0.0.1, random free port

The service evaluates in-process: no model calls and no outbound network traffic. Base URL in the examples: http://127.0.0.1:18734. See Services and Serve decisions to other languages.

Responses contain the raw matched values

Every evaluation response includes triggers[].matched_items: the exact values that matched. With the example pack, those are the unredacted SSNs, card numbers and addresses that were found. Treat responses as sensitive. Don't log response bodies, and strip matched_items before passing a result on to anything else.

Authentication and origins#

  • Bearer token. When the service has a token (--token, token= or HELIXOR_SERVICE_TOKEN), every request except GET /v1/health and CORS preflight must send Authorization: Bearer <token>. Otherwise HTTP answers 401 with {"detail": "Missing or invalid bearer token"} and a WWW-Authenticate: Bearer header, and a WebSocket is closed with code 1008. Without a token the service has no authentication.
  • Binding. The default host is 127.0.0.1. Starting on any non-loopback address without a token fails with ServerConfigurationError.
  • CORS. Off by default. HELIXOR_CORS_ORIGINS takes a comma-separated list of browser origins; credentials are allowed only for those. An origin containing * is rejected at startup.
  • One engine. A service that serves a compiled pack answers 400 to a request that sets license_key, so a caller cannot swap in another engine.

Known issue in 0.2.1

WebSocket streams fail open after a block. After a stream_token with "blocked": true, the next stream_chunk on the same connection starts a new session with default settings, and the text after the fatal value is streamed back. Close the connection on a blocked token.

Fixed in the next release. The connection remembers the block: every later stream_chunk is refused with an error message whose code is STREAM_BLOCKED, and nothing more is released until you send stream_start. A block found at stream_flush counts too. See WS /v1/ws/decision.

Errors#

StatusWhenBody
400Unknown domain in POST /v1/decision, or license_key sent to a service that serves a compiled pack.{"detail": "Unknown decision function: …"} or {"detail": "This service executes sealed pack '…' under license '…'; per-request license_key is not accepted."}
401The service has a token and the request did not send it.{"detail": "Missing or invalid bearer token"}
403license_data fails verification, or does not allow the pack or solver.{"detail": "Invalid license: …"} or an entitlement message
422The request body fails validation, for example a missing text.Standard validation error list under detail.
500An unhandled error, for example an invalid license_key sent to a service running the built-in pack.Plain text.

See Errors. A client must treat any non-200 response, timeout or connection failure as "do not proceed".

GET /v1/health#

Liveness and metadata. It needs no token. pack_id and engine_edition (the license tier) describe the engine actually being served, so a readiness check can compare them with what you deployed.

{
  "status": "healthy",
  "service": "helixor-decision-demos",
  "version": "1.1.0",
  "engine_edition": "COMMUNITY",
  "pack_id": "compliance.regulatory_pii_guard.v1",
  "supported_protocols": ["REST", "SSE", "WebSocket"],
  "supported_regulations": ["GLBA", "PCI-DSS", "HIPAA", "GDPR", "CCPA"],
  "decision_functions": ["evaluate", "solve_constraints", "vrp", "bin_packing"]
}

POST /v1/evaluate#

Evaluates one payload and returns the full result.

FieldTypeRequiredMeaning
textstringyesThe payload.
license_keystringnoA Helixor key string, not a .hxlic file. On a service running the built-in pack, a new built-in engine is created for this request with that key. A service running a compiled pack rejects it with 400. Most deployments omit it.
POST /v1/evaluate HTTP/1.1
Content-Type: application/json

{"text": "Employee note: SSN is 123-45-6789, email jane@example.com."}
{
  "pack_id": "compliance.regulatory_pii_guard.v1",
  "action": "block_glba_ssn_leakage",
  "invariants_passed": false,
  "reason": "Statutory violation: Social Security Number (GLBA / FCRA) detected",
  "triggers": [
    {
      "rule_id": "RULE-GLBA-SSN-BLOCK",
      "law": "Gramm-Leach-Bliley Act & FCRA",
      "severity": "FATAL",
      "matched_items": ["123-45-6789"]
    },
    {
      "rule_id": "RULE-GDPR-EMAIL-REDACT",
      "law": "GDPR Article 6 & CCPA",
      "severity": "WARNING",
      "matched_items": ["jane@example.com"]
    }
  ],
  "remedy": {
    "clean_text": "Employee note: SSN is [REDACTED_SSN], email [REDACTED_EMAIL].",
    "redactions_count": 2,
    "redacted_categories": ["SSN (GLBA/FCRA)", "Email (GDPR/CCPA)"]
  },
  "latency_us": 70.12,
  "tokens_spent": 0,
  "egress_bytes": 0,
  "receipt_hash": "hx_proof_5b46797af4f9af1e982eabe8",
  "license_status": "COMMUNITY_PERPETUAL",
  "active_key_id": "hx_lic_community_developer_ep1_0",
  "active_epoch": 1,
  "edition": "COMMUNITY",
  "upgrade_notice": "Free Community Edition: …"
}

The first evaluation after start-up is slower than later ones; the latency above is from a first call. The fields are described under EvaluationResult in the Python reference.

curl -s http://127.0.0.1:18734/v1/evaluate \
  -H 'Content-Type: application/json' \
  -d '{"text": "Card 4111-1111-1111-1111"}' | jq '{action, clean: .remedy.clean_text}'

POST /v1/evaluate/stream#

Returns text/event-stream. You send the whole text; the server cuts it into chunk_size-character pieces and pushes them through a streaming session. That simulates a token stream, which makes the endpoint useful for testing streaming behavior. It cannot filter a stream your application is receiving; use the WebSocket for that.

FieldTypeDefaultMeaning
textstringrequiredThe complete text.
chunk_sizeint4Characters per simulated fragment (minimum 1).
lookahead_charsint28Held-back window (minimum 16).
block_on_fatalbooltrueStop the stream on a fatal match.
license_keystringnoneAs for /v1/evaluate.

Each event is one line, data: <StreamChunkResult JSON>, followed by a blank line. After a "blocked": true event the stream ends.

data: {"chunk_index": 1, "text": "Call ", "is_final": false, "redacted": false, "blocked": false, "action": "stream_token", "reason": null, "triggers": []}

data: {"chunk_index": 2, "text": "[REDACTED_PHONE] today, ask for Jane.", "is_final": true, "redacted": true, "blocked": false, "action": "stream_token", "reason": null, "triggers": []}

That is the real output for {"text": "Call (415) 555-0100 today, ask for Jane.", "chunk_size": 6}. Chunk boundaries fall at delimiters the lookahead window allows, never inside a value or a redaction token. redacted is true on exactly the chunks whose text was changed. Concatenate chunk text before you display or store it; the result equals the /v1/evaluate clean_text.

POST /v1/decision#

A single dispatcher for decision functions, with optional license checks on each request.

FieldTypeDefaultMeaning
domainstringevaluateWhich function to run. evaluate (aliases pii, safety) is documented here.
parametersobject{}For evaluate: {"text": "…"}.
license_keystringnoneAs for /v1/evaluate.
license_dataobject or stringnoneYour .hxlic JSON. When given, it is verified, and the request is refused with 403 unless the license allows the served pack. Sending it sends your content key; only do so on a loopback or private link.
{"domain": "evaluate", "parameters": {"text": "Contact jane@example.com"}}
{
  "function": "evaluate",
  "status": "success",
  "latency_us": 31.75,
  "result": { "action": "redact_and_permit_contact_pii", "remedy": {"clean_text": "Contact [REDACTED_EMAIL]", "…": "…"}, "…": "…" }
}

result is the same object /v1/evaluate returns. The other domain values (vrp/fleet, bin_packing/packing, solve_constraints/solver) call engines that are out of scope for this edition of the reference. Unknown values return 400.

WS /v1/ws/decision#

A persistent connection for many evaluations or for live stream filtering. Send and receive one JSON object per message. The connection uses the service's engine (your pack under serve); license_key is not accepted here. When the service has a token, send it in the Authorization header of the upgrade request.

You sendYou receive
{"type": "ping"}{"type": "pong", "time": <unix seconds>}
{"type": "evaluate", "text": "…"}{"type": "verdict", "action", "invariants_passed", "clean_text", "latency_us", "egress_bytes", "receipt_hash"}. No triggers, so no raw values.
{"type": "stream_start", "lookahead_chars": 28, "block_on_fatal": true}{"type": "stream_started", "lookahead_chars": 28}. Starts a new streaming session for this connection.
{"type": "stream_chunk", "token": "…"}Zero or more {"type": "stream_token", "token", "blocked", "action", "reason"}. After "blocked": true the session ends. A chunk with no session starts one with default settings, including the next chunk after a block (the known issue above). In the next release, a chunk after a block gets {"type": "error", "code": "STREAM_BLOCKED", "message", "action", "reason"} instead, until the next stream_start.
{"type": "stream_flush"}The remaining stream_token messages, then {"type": "stream_completed"}. In the next release a flush after a block releases nothing and does not end the block; only stream_start does.
Any other type{"type": "error", "message": "Unknown message type: '…'"}

A message without type is treated as evaluate. Messages are handled in order on each connection. A {"type": "solve"} message also exists; it is out of scope for this edition. If a handler raises, the server sends {"type": "error"} and closes the connection.

→ {"type": "stream_start"}
← {"type": "stream_started", "lookahead_chars": 28}
→ {"type": "stream_chunk", "token": "Reach me at jane@example.com or on the phone any time this week please."}
← {"type": "stream_token", "token": "Reach me at [REDACTED_EMAIL] or on the ", "blocked": false, "action": "stream_token", "reason": null}
→ {"type": "stream_flush"}
← {"type": "stream_token", "token": "phone any time this week please.", "blocked": false, "action": "stream_token", "reason": null}
← {"type": "stream_completed"}

The same connection after a block, in the next release:

→ {"type": "stream_start"}
← {"type": "stream_started", "lookahead_chars": 28}
→ {"type": "stream_chunk", "token": "SSN 123-45-6789 and more text to pass the window"}
← {"type": "stream_token", "token": "", "blocked": true, "action": "block_glba_ssn_leakage", "reason": "Statutory violation: Social Security Number (GLBA / FCRA) detected"}
→ {"type": "stream_chunk", "token": " Patient in room 402."}
← {"type": "error", "code": "STREAM_BLOCKED", "message": "The stream was blocked by a fatal invariant; send stream_start to begin a new stream.", "action": "block_glba_ssn_leakage", "reason": "Statutory violation: Social Security Number (GLBA / FCRA) detected"}
→ {"type": "stream_start"}
← {"type": "stream_started", "lookahead_chars": 28}