Skip to documentation

JavaScript & React SDK

eli.ai's package family separates wire contracts, a headless HTTP client, and React UI. You can use only JSON types, drive every public API operation from a server, or embed complete capabilities in a React application without importing Next.js.

Implemented in the repository

@eli-ai/contracts, @eli-ai/client, and @eli-ai/react are implemented as version 0.1.0 workspace packages. A public package-registry release is a separate distribution step, so confirm your registry before using the install commands below. The OpenAPI reference remains the wire source of truth.

Three layers, one wire contract

React builds on the headless client; the client builds on JSON-safe contracts; all server authority remains in /api/v1.
@eli-ai/contracts

Framework-free request, response, error, and SSE event types through capability subpaths.

@eli-ai/client

A standards-based Fetch client covering all 56 current operations, including authenticated SSE.

@eli-ai/react

An injected provider, generic async hooks, complete capability components, and an optional CSS export.

Headless client

The client accepts a deployment origin, a credential resolver, a workspace resolver, and an injectable Fetch implementation. Dates stay ISO strings, currency stays a decimal string, and route-specific response envelopes stay intact.

package installationbash
npm install @eli-ai/client @eli-ai/contracts
server-side structured queryts
import { createEliClient, EliApiError } from "@eli-ai/client";
import type { StructuredAnswer } from "@eli-ai/contracts/query";

const eli = createEliClient({
  baseUrl: "https://eli.ai",
  apiKey: () => process.env.ELI_API_KEY!,
  workspaceId: process.env.ELI_WORKSPACE_ID,
});

let answer: StructuredAnswer;
try {
  answer = await eli.query.ask({
    question: "Which controls changed this quarter?",
    includeData: true,
  });
} catch (error) {
  if (error instanceof EliApiError && error.status === 403) {
    throw new Error(`Not authorized: ${error.code}`);
  }
  throw error;
}

Capability subpaths

Contracts are imported by grouped capability, so a document integration does not need React or agent stream types. The contracts root deliberately re-exports only shared metadata (common, auth, capabilities, and operations); payload contracts use explicit subpaths. Stable paths are /common, /auth, /query, /documents, /graph, /governance, /data, /agents, /quality, /reports, /connectors, /capabilities, and /operations.

types-only subsetts
import type {
  DocumentMeta,
  CreateDocumentInput,
  DocumentSaveResult,
} from "@eli-ai/contracts/documents";

import type {
  QueuedRun,
  RunEvent,
} from "@eli-ai/contracts/agents";

Authentication and workspace selection

The client adds protocol headers; it does not make an authorization decision. The server still resolves the key, checks the operation's scope, narrows user keys against the owner's live workspace role, and runs tenant reads and writes beneath FORCE row-level security.

The SDK transports identity; the server remains the trust boundary.

Workspace selection is deterministic: a headless method's per-call workspaceIdoverrides the client transport default. React hooks and components pass the provider's workspace as that per-call override when supplied; otherwise the injected transport's default applies. An explicit x-workspace-id custom header takes final precedence, so do not pass untrusted arbitrary headers through an integration wrapper.

KeyWorkspace rule
eli_sk_…Fixed to one workspace. A different X-Workspace-Id is rejected.
eli_uk_… · one workspaceThe single allowed workspace is selected automatically.
eli_uk_… · multiple/allEvery request must include X-Workspace-Id inside the key grant and live membership.

Do not put a workspace key in browser code

Never ship an eli_sk_ key in a bundle, HTML, local storage, or public runtime configuration. The current API also installs no cross-origin CORS policy. Browser React consumers should use a same-origin server-side BFF or reverse proxy that injects a Bearer key. A shared origin alone does not reuse the application session: /api/v1 still returns 401 without a Bearer credential. Direct browser use is only appropriate for a narrowly scoped user credential under an operator-controlled setup. The injected transport/custom Fetch is the trust boundary.

Durable runs over Fetch-based SSE

Starting an agent returns HTTP 202 with a durable run id and stream path. The worker executes independently; agents.streamRun() attaches to a database-backed replay/tail. Fetch is required instead of native EventSource because the stream needs Authorization and, for some user keys, X-Workspace-Id.

A disconnected client does not stop or restart the durable run.
durable-run APIts
import type { RunEvent } from "@eli-ai/contracts/agents";

const accepted = await eli.agents.createRun("control-reviewer", {
  input: "Review the latest policy changes.",
});

for await (const event of eli.agents.streamRun(accepted.runId, { signal })) {
  if (event.type === "model_call" || event.type === "tool_call") {
    renderProgress(event);
  }

  if (event.type === "suspended") {
    renderApproval(event.approvalId, event.toolName);
    break;
  }

  if (event.type === "final") {
    renderAnswer(event.text, event.citations);
    break;
  }

  if (event.type === "error") {
    renderError(event.message);
    break;
  }
}

Reconnects replay progress

The current stream replays persisted steps from the beginning and does not implement Last-Event-ID. Reconnecting can repeat progress events. Abort closes the local reader but does not cancel the run; call agents.cancelRun(id) for cooperative cancellation. Streams close on suspended, final, error, or the current ten-minute safety timeout.

React composition

React receives an already-constructed transport, or the transport retained from an aggregate client. That keeps key retrieval, tenant selection, same-origin proxying, and tracing outside the render tree. Each capability component constructs only its matching client module from that transport. Complete components use scoped eli-* classes and the optional CSS export; hooks remain usable with application-owned markup. EliProvider deliberately has no API-key prop.

React installationbash
npm install @eli-ai/react @eli-ai/client react
provider + complete query surfacetsx
import { createEliTransport } from "@eli-ai/client/core";
import { EliProvider } from "@eli-ai/react/provider";
import { EliQuery } from "@eli-ai/react/query";
import "@eli-ai/react/styles.css";

const transport = createEliTransport({
  // Calls a same-origin BFF that injects the credential server-side.
  fetch: sameOriginProxyFetch,
});

export function KnowledgeAnswer({ workspaceId }: { workspaceId: string }) {
  return (
    <EliProvider transport={transport} workspaceId={workspaceId}>
      <EliQuery initialMode="ask" />
    </EliProvider>
  );
}
headless hook + your markuptsx
import { createQueryClient } from "@eli-ai/client/query";
import { useEliMutation } from "@eli-ai/react/hooks";

type QueryClient = ReturnType<typeof createQueryClient>;
type AskAnswer = Awaited<ReturnType<QueryClient["ask"]>>;

export function CompactAsk() {
  const query = useEliMutation<AskAnswer, string>({
    context: "compact-ask",
    mutation: ({ transport, workspaceId }, question, signal) =>
      createQueryClient(transport).ask(
        { question },
        { signal, ...(workspaceId ? { workspaceId } : {}) },
      ),
  });

  return (
    <form onSubmit={(event) => {
      event.preventDefault();
      const data = new FormData(event.currentTarget);
      void query.mutate(String(data.get("question") ?? ""));
    }}>
      <input name="question" aria-label="Question" />
      <button disabled={query.isPending}>Ask</button>
      {query.error instanceof Error && <p role="alert">{query.error.message}</p>}
      {query.data && <p>{query.data.answer}</p>}
    </form>
  );
}

No Next.js dependency

The React package uses ordinary React, URLs, callbacks, and an injected transport. It imports no Next router, Link, Image, font, server-action, or route-handler module.

Current capability coverage

The headless client covers every operation in the current OpenAPI document. React adds complete components by product capability; MCP remains a separate protocol at /api/mcp.

CapabilityCanonical scopesCurrent routesReact surface
Query & searchagents:run · kb:read/query · /searchEliQuery · EliSearch
Documentskb:read · kb:write/documents · verify · lineageEliDocuments · EliDocumentLineage
Entities & governancekb:read · kb:write/entities · /relations · /manifest · change requests · SKOSEliKnowledgeGraph · EliGovernance
Live datadata:read · data:run/data/connectors · /data/queriesEliLiveData
Reportsreports:read · reports:write/reportsEliReports
Agents & runsagents:run · runs:read/agents/{slug}/runs · /runs/{id}/*EliAgentRunner · EliRunDetail
Policy, evals & cachekb:read · kb:write/policy · /evals · /qrels · /cache/statsEliQuality · EliPolicyDecisions · EliEvalMetrics · EliCacheStats
Content connectorsdata:read · kb:write/connectors · /connectors/{id}/syncEliConnectors

Canonical versus accepted read scopes

The SDK operation catalog exposes both scope and acceptedScopes, matching OpenAPI's x-required-scope and x-accepted-scopes. OpenAPI recommends kb:read for graph, lineage, policy, eval, and cache reads; selected compatibility guards also accept older runs:read keys. New integrations should request the canonical scope.

Error behavior

Non-2xx responses become EliApiError, retaining status, machine code, validation issues, parsed details, and the x-request-id when present. Fetch failures become EliTransportError; invalid successful JSON or response content types become EliProtocolError; malformed JSON in an SSE frame becomes EliSseParseError. Abort keeps its native identity. A terminal SSE error payload remains a yielded RunEvent.

  • 400: fix request or workspace selection; do not retry unchanged input.
  • 401: missing, invalid, or revoked credential.
  • 402: monthly agent-spend admission blocked.
  • 403: insufficient scope, live capability, or workspace grant.
  • 404: unknown or cross-workspace resource; the distinction is intentionally hidden.
  • 409: required model is not configured.
  • 429: active-run capacity is full, not a general API throttle.

The current API has no universal pagination envelope, standard idempotency key, general rate limiter, or cross-origin CORS policy. Document enrichment is asynchronous after durable save, and run SSE is the only unified public push channel today.

Repository, registry, and HTTP use

Inside this repository, consume the version 0.1.0 workspace packages directly. Outside it, confirm the package-registry release before running the install commands. The HTTP/OpenAPI contract remains the interoperable fallback and source of truth: