Developers
Use your own model
Register your own decision server, and a model you set up takes its answers from it. Helm signs each request, then seals every answer, scores it on your outcomes and can calibrate it.
How it works
- On the API page in the app, under Endpoints, register your server’s address. Helm shows its signing secret once: keep it on your server, as you would a key. The page lists each endpoint with its id.
- In Start now, choose Answered by your own model and pick the endpoint. Test sends it one signed sample request and shows the answer it read, or why it was refused; a test saves and charges nothing.
- Every call of that model, on
POST /v1/models/{model}/forecastsorPOST /v1/systemone, sends your server one request per record. Helm seals each answer before it returns it, as it seals its own. - Helm scores the answers on the outcomes you record, and from enough of them calibrates them. It does not compute per-field reasons for a model your server answers, and each answer says so in its
warnings.
Where Helm sends it
- An
httpsaddress on port 443 or 8443, under a public host name: no IP address, and no user name or password in it. Helm checks it again on every request. - Helm never follows a redirect: it would send the request and its signature to a host you did not register.
The request
One POST per record to the address you registered, with content-type: application/json, a Helm-Signature and a Helm-Request-Id:
POST https://models.example.com/v1/systemone
Content-Type: application/json
Helm-Signature: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIs…
Helm-Request-Id: dreq_…
{"model": "late_orders",
"state": {"ordered_on": "2026-09-01", "region": "China",
"value": 18250.0, "expedite": true},
"questions": {"late_orders": {
"type": "noul",
"instructions": "Will this order arrive late?",
"criteria": {"true": "Arrives after the promised date",
"false": "On time"}}}}modelis the model’s short name, andquestionsasks its one question under that name:noulfor yes or no,choicefor one of several answers.instructionsis the question as written, andcriteriadescribes each answer:trueandfalsefor yes or no, each answer’s id for a choice.stateholds the record’s facts by name: a category as its listed value, a number as a number, a date asYYYY-MM-DD, yes or no astrueorfalse, and text after Helm removes contact details and account numbers (and, in Compliance mode, the listed protected terms). The as-of date is there under the model’s own name for it. A fact the record left out is not sent, and neither is its outcome key.Helm-Request-Idnames the request: it is the signature’ssub.
Checking the signature
Helm-Signatureis an HS256 JWS, a JWT signed with your endpoint’s secret: the wholehmsec_…string is the key. Itskidis the secret’s first 14 characters, as the API page shows it.- Claims:
iss(helm-express),aud(your endpoint’s id),sub(theHelm-Request-Id),jti(fresh on every request),iat,exp(five minutes afteriat),url(the address it was sent to) andbh, the hex SHA-256 of the raw body. Hash the bytes as they arrived, before any JSON parse. - Refuse a request whose signature does not check, whose
audis not your endpoint’s id, whosebhis not its body’s hash, or whoseexphas passed. To refuse a replay too, keep eachjtiuntil itsexpand refuse one you have seen.
The code below needs nothing installed. Call it with the header, the raw body, your secret and your endpoint’s id; it returns the claims, or raises (throws) when the request is not Helm’s.
In Python:
import base64
import hashlib
import hmac
import json
import time
def _b64url(part):
return base64.urlsafe_b64decode(part + "=" * (-len(part) % 4))
def verify_signature(header, body, secret, endpoint_id, url=None, now=None, leeway=60):
"""Check a Helm-Signature header against the raw request body: the bytes, before
any JSON parse. Returns the claims; raises ValueError if the request is not Helm's."""
if isinstance(body, str):
body = body.encode("utf-8")
now = time.time() if now is None else now
key = secret.encode("utf-8")
bh = hashlib.sha256(body).hexdigest()
for jws in header.split(","):
parts = jws.strip().split(".")
if len(parts) != 3:
continue
try:
head = json.loads(_b64url(parts[0]))
claims = json.loads(_b64url(parts[1]))
given = _b64url(parts[2])
signing = (parts[0] + "." + parts[1]).encode("ascii")
except ValueError:
continue
signed = hmac.new(key, signing, hashlib.sha256).digest()
if not isinstance(head, dict) or head.get("alg") != "HS256":
continue
if not hmac.compare_digest(signed, given):
continue
if not isinstance(claims, dict) or claims.get("iss") != "helm-express":
raise ValueError("not issued by Helm")
if claims.get("aud") != endpoint_id:
raise ValueError("signed for another endpoint")
exp, iat = claims.get("exp"), claims.get("iat")
if not isinstance(exp, int) or exp + leeway < now:
raise ValueError("expired")
if not isinstance(iat, int) or iat - leeway > now:
raise ValueError("issued in the future")
if claims.get("bh") != bh:
raise ValueError("signed over another body")
if url is not None and claims.get("url") != url:
raise ValueError("signed for another URL")
return claims
raise ValueError("no valid Helm-Signature")
In TypeScript, on Node 18 or later:
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
export type HelmClaims = {
iss: string; aud: string; sub: string; jti: string;
iat: number; exp: number; bh: string; url: string;
};
/** Check a Helm-Signature header against the raw request body (the bytes, before any JSON
* parse). Returns the claims; throws when the request is not Helm's. */
export function verifySignature(
header: string,
body: string | Uint8Array,
secret: string,
endpointId: string,
opts: { url?: string; now?: number; leeway?: number } = {},
): HelmClaims {
const now = opts.now ?? Date.now() / 1000;
const leeway = opts.leeway ?? 60;
const bh = createHash("sha256").update(body).digest("hex");
const refuse = (why: string): never => {
throw new Error(why);
};
for (const jws of header.split(",")) {
const parts = jws.trim().split(".");
if (parts.length !== 3) continue;
const [h, p, s] = parts as [string, string, string];
let head: { alg?: unknown } | null;
let claims: Partial<HelmClaims> | null;
try {
head = JSON.parse(Buffer.from(h, "base64url").toString("utf8"));
claims = JSON.parse(Buffer.from(p, "base64url").toString("utf8"));
} catch {
continue;
}
const signed = createHmac("sha256", secret).update(`${h}.${p}`).digest();
const given = Buffer.from(s, "base64url");
if (head?.alg !== "HS256" || given.length !== signed.length) continue;
if (!timingSafeEqual(given, signed)) continue;
if (claims?.iss !== "helm-express") refuse("not issued by Helm");
const c = claims as HelmClaims;
if (c.aud !== endpointId) refuse("signed for another endpoint");
if (typeof c.exp !== "number" || c.exp + leeway < now) refuse("expired");
if (typeof c.iat !== "number" || c.iat - leeway > now) refuse("issued in the future");
if (c.bh !== bh) refuse("signed over another body");
if (opts.url !== undefined && c.url !== opts.url) refuse("signed for another URL");
return c;
}
throw new Error("no valid Helm-Signature");
}
Any JWT library does the same if it pins the algorithm to HS256, the issuer to helm-express and the audience to your endpoint’s id; then compare bh yourself.
Your reply
Answer 200 with JSON: one answer, to the one question, under the model’s short name.
{"answers": {"late_orders": {"type": "noul", "noul": 0.72}}}
# a choice: a chance for every answer id; choice, if sent, is the likeliest
{"answers": {"carrier": {"type": "choice", "choice": "sea",
"probabilities": {"air": 0.2, "sea": 0.7, "road": 0.1}}}}- Yes or no:
noulis the chance oftrue, from 0 to 1. - A choice:
probabilitiesgives every answer id a chance from 0 to 1, together summing to 1 within 0.02.choice, if you send it, must be the likeliest of them;confidenceis kept as well. - Helm keeps
type,noul,choice,confidenceandprobabilitiesof the answer, and nothing else of the reply. - A redirect, a status other than
2xx, a reply over 256 KiB, one that is not JSON, or one that does not answer exactly the question asked fails the whole call. Helm does not retry a request.
Limits
timed_out.deadline, or endpoint_busy while a record still waits its turn.429 rate_limited with Retry-After; nothing is sent.Helm runs several engine machines, each with its own count, so more than 8 requests can reach your server at once. Limit what your server takes as well: when it is full, answer 429 or 503, and the call fails with nothing sealed or charged.
Price
Each forecast is billed per input token at $0.10 per million: its facts and the question, counted as Helm counts its own. Output is free, a test is free, and a call that fails is not charged.
When a call fails
502 endpoint_bad_reply: Your server answered, and not with an answer to every record in time: an error status, a redirect, a reply too large, not JSON or not the answer asked, a request past 20 s, or the call past 60 s.reasonsays which,indexnames the record, andstatusis the status your server answered.503 endpoint_unavailable: Helm could not ask your server: no connection could be made (its name, its address, TLS), Helm’s requests to it were all in flight for other calls (endpoint_busy), the endpoint was revoked, or answers from your own model are not on for the workspace. A wait carriesRetry-After.
In both, nothing is sealed and nothing is charged, and Helm’s own model is never asked in its place.
Next
- QuickstartA key, a first call to Helm’s general model, then a model of your own: forecasts and outcomes.
- API referenceEvery route of the Helm Decision API, with its method, path, scope and what it does.
- MCP connectorRead-only tools over JSON-RPC 2.0, for agents and model clients.
- WebhooksEvent types, how a delivery is signed, and the delivery log.
- LimitsRate, concurrency and the daily allowance, and the answer each limit gives.