JavaScript and TypeScript
sdk/typescript is a TypeScript client with no dependencies: it uses the fetch built into Node, Deno, Bun and browsers, and Node 22.18+ runs its source directly. Keep the key on your server; never ship it to a browser.
Install it from a clone of https://github.com/jepelasweden/jepela (npm links the folder, and Node runs the TypeScript there):
git clone https://github.com/jepelasweden/jepela
npm install ./jepela/sdk/typescript # then: import { JepelaClient } from "jepela-sdk"
import { JepelaClient, choice, noul, score } from "./sdk/typescript/src/index.ts";
const jepela = new JepelaClient(); // JEPELA_API_KEY; the key's region picks the address
const r = await jepela.systemOne(
{ message: "We were charged twice for September again.", account: "ACME Logistics" },
{
route: choice("Which team should handle this?", { billing: "Charges and refunds", support: "Product problems", other: "None of these" }),
refund: noul("Is the customer asking for money back?"),
urgency: score("How urgent is this?", ["Can wait a week", "Should be handled today", "Blocking the customer now"]),
},
{ subject: "acme", memory: { compare: true } },
);
r.answers.route.choice; // typed: "billing" | "support" | "other"
r.answers.refund.noul; // probability of yes
r.answers.route.answer_confidence; // the probability of the answer given (see Confidence)
r.memory?.without_memory; // the same call without the memory
r.warnings; // known traps in this request
r.usage.answers_billed;
new JepelaClient({ apiKey, baseUrl, timeoutMs, maxRetries, backoffMs }): the key falls back to JEPELA_API_KEY, the gateway to JEPELA_BASE_URL and then the API of the key's region (see Regions); by default 120 000 ms per call, 2 retries, 500 ms backoff.
Every route has a method: remember, forget, delete, memory, rulesAdd, rules, rulesDelete, pin, exclude, aliases, aliasesDelete, feedback, quality, calibrationFit, calibration, goldenAdd, golden, goldenRun, goldenDelete, decisionsDelete, batchCreate, batchUpload, batch, batches, batchResults, batchWait, finetune, finetunes, finetuneJob, finetuneWait, finetuneDelete, models, usage, and signup(email, { inviteCode }), which needs no key. request(method, path, body) calls any route.
await jepela.memory("acme", true); // lines and rules, word for word
await jepela.rulesAdd("mia", "session_result <= -30", "Mia's stop-loss is reached: she stops playing now.", { ttlSeconds: 3600 });
await jepela.pin("acme", "ACME pays for premium support.");
await jepela.exclude("player-9", ["gold_coins"], "Coin bug.", { question: "reward" });
await jepela.aliases("acct-1", ["Globex Corporation"]);
await jepela.aliasesDelete("acct-1", ["Globex Corporation"]); // those names out again, none added
await jepela.forget("acme", "Ann Lindqvist", { preview: true }); // what would go, and related; nothing removed
await jepela.goldenDelete(["refund-1"]); // [] deletes nothing; no argument deletes every case
await jepela.request("GET", "/v1/usage");
With memory: { explain: true } the answer says what each memory line did; pins and exclusions show up too:
const e = await jepela.systemOne(state, questions, { subject: "player-9", memory: { explain: true } });
e.memory?.pinned; // ids of the pinned rules sent
e.memory?.excluded; // { reward: { gold_coins: "Coin bug." } }
e.memory?.exclusions_skipped; // exclusions that would have removed every option
e.memory?.explain; // [{ line, kind: "pinned" | "rule" | "recalled", effect: { question: move } }]
// (and measured_on_first where the second tier gave the answer: see Memory)
Fine-tuning trains a model of your own on your golden cases (at least 20; see Fine-tuning):
await jepela.finetunes(); // { jobs, min_cases: 20, bases: ["english", "multilingual", "typed-decisions"] }
const job = await jepela.finetune("english"); // { job, model, cases, held_out, state: "running" }
const done = await jepela.finetuneWait(job.job); // polls finetuneJob(job.job); default 3 600 000 ms, every 10 000 ms
if (done.usable) await jepela.systemOne(state, questions, { model: job.model });
await jepela.finetuneDelete(job.model); // its files are deleted and its name stops working
Options on systemOne: model, subject, memory (use, top_k, min_share, compare, placebo, names, question_words, focus, explain; see Request options), robust (true or { orders: 1-5 }, choices of up to 20 options), windows (true or { combine: { question_id: "max" | "mean" | "min" } }), derive, values, cache, method ("auto", the default, matches choices of more than 20 options by vectors and asks the engine the rest; "engine" reads every option; "match" matches every choice), quality ("fast", the default; "balanced" and "best" send the answers the engine is least sure of to a larger, slower second tier: each answer then has right_probability, those answered again tier: 2 and first, and the response quality; see Request options). Each answer has method ("engine" or "match"), and a matched one similarity ({ best, second }). Measured on BANKING77 (600 bank messages, 77 intents) on a GPU test server, 2026-09-27: matching 65.2% right in a median of 12 ms, the engine reading all 77 options 62.2% in 57 ms (Choice).
Errors are classes per status (AuthenticationError, PaymentRequiredError, PermissionDeniedError, NotFoundError, ConflictError, BadRequestError, RateLimitError with retryAfter, EngineError, ServerError, ConnectionFailed). The client retries 429, 502 and 503 with backoff, and a connection that failed before the request left; a decision that may have arrived is never sent again, so it cannot be billed twice.
The source is not type-checked in this repository (tsc is not installed here); tsconfig.json has the settings for checking it. Its tests run against the real engine: python tests/real_steps/test_jepela_typescript_sdk.py.
Without the SDK
const res = await fetch(`${process.env.JEPELA_BASE_URL}/v1/systemone`, {
method: "POST",
headers: { Authorization: `Bearer ${process.env.JEPELA_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ state, questions, subject }),
});
const data = await res.json();
if (!res.ok) throw new Error(`${res.status} ${data.error.type}: ${data.error.message}`);