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.
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#
| How | Engine | Default address |
|---|---|---|
helixor-pack serve --pack … --license … | Your compiled pack | 127.0.0.1:18734 |
python -m helixor_runtime.server [--pack … --license …] | Your compiled pack, or the built-in example pack without --pack | 127.0.0.1:18734 |
start_server(engine) from Python | The engine you pass; the built-in example pack if none | 127.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=orHELIXOR_SERVICE_TOKEN), every request exceptGET /v1/healthand CORS preflight must sendAuthorization: Bearer <token>. Otherwise HTTP answers401with{"detail": "Missing or invalid bearer token"}and aWWW-Authenticate: Bearerheader, and a WebSocket is closed with code1008. 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 withServerConfigurationError. - CORS. Off by default.
HELIXOR_CORS_ORIGINStakes 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
400to a request that setslicense_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#
| Status | When | Body |
|---|---|---|
| 400 | Unknown 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."} |
| 401 | The service has a token and the request did not send it. | {"detail": "Missing or invalid bearer token"} |
| 403 | license_data fails verification, or does not allow the pack or solver. | {"detail": "Invalid license: …"} or an entitlement message |
| 422 | The request body fails validation, for example a missing text. | Standard validation error list under detail. |
| 500 | An 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.
| Field | Type | Required | Meaning |
|---|---|---|---|
text | string | yes | The payload. |
license_key | string | no | A 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.
| Field | Type | Default | Meaning |
|---|---|---|---|
text | string | required | The complete text. |
chunk_size | int | 4 | Characters per simulated fragment (minimum 1). |
lookahead_chars | int | 28 | Held-back window (minimum 16). |
block_on_fatal | bool | true | Stop the stream on a fatal match. |
license_key | string | none | As 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.
| Field | Type | Default | Meaning |
|---|---|---|---|
domain | string | evaluate | Which function to run. evaluate (aliases pii, safety) is documented here. |
parameters | object | {} | For evaluate: {"text": "…"}. |
license_key | string | none | As for /v1/evaluate. |
license_data | object or string | none | Your .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 send | You 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}