> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modulex.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# JavaScript SDK setup & configuration

> Install the modulex-js SDK, construct the Modulex client with an mx_live_ API key and X-Organization-ID, and learn how the SDK injects headers and converts camelCase to snake_case on the wire.

export const MediaEmbed = ({id, type = 'screenshot', caption = '', ext, ratio = '16 / 9'}) => {
  const isVideo = type === 'video' || type === 'app_video';
  const resolvedExt = ext || (isVideo ? 'mp4' : type === 'screenshot' ? 'webp' : 'svg');
  const src = 'https://media.modulex.dev/' + id + '.' + resolvedExt;
  const [status, setStatus] = useState('loading');
  const [isDev, setIsDev] = useState(false);
  const [inView, setInView] = useState(false);
  const boxRef = useRef(null);
  useEffect(() => {
    if (typeof window === 'undefined') return;
    const h = window.location.hostname;
    setIsDev(h === 'localhost' || h === '127.0.0.1' || h.endsWith('.mintlify.app'));
  }, []);
  useEffect(() => {
    if (inView) return;
    if (typeof IntersectionObserver === 'undefined') {
      setInView(true);
      return;
    }
    const el = boxRef.current;
    if (!el) return;
    const io = new IntersectionObserver(entries => {
      if (entries.some(e => e.isIntersecting)) {
        setInView(true);
        io.disconnect();
      }
    }, {
      rootMargin: '300px'
    });
    io.observe(el);
    return () => io.disconnect();
  }, [inView]);
  if (status === 'missing') {
    if (!isDev) return null;
    return <div style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'center',
      gap: '0.4rem',
      padding: '1rem 1.25rem',
      margin: '1.25rem 0',
      width: '100%',
      aspectRatio: ratio,
      boxSizing: 'border-box',
      border: '1px dashed rgba(128,128,128,0.45)',
      borderRadius: '0.75rem',
      background: 'rgba(128,128,128,0.06)',
      color: 'currentColor',
      fontSize: '0.85rem',
      lineHeight: 1.45
    }}>
        <div style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      opacity: 0.75
    }}>
          <span aria-hidden="true">🎬</span>
          <code style={{
      fontSize: '0.75rem'
    }}>{id}</code>
          <span style={{
      fontSize: '0.65rem',
      textTransform: 'uppercase',
      letterSpacing: '0.04em',
      padding: '0.1rem 0.4rem',
      borderRadius: '0.4rem',
      background: 'rgba(128,128,128,0.18)'
    }}>
            {type}
          </span>
        </div>
        <div style={{
      opacity: 0.9
    }}>{caption || 'Media not uploaded yet.'}</div>
        <div style={{
      fontSize: '0.7rem',
      opacity: 0.5
    }}>
          Upload to R2 as <code>{id}.{resolvedExt}</code> — preview only, hidden in production.
        </div>
      </div>;
  }
  const mediaStyle = {
    display: status === 'loaded' ? 'block' : 'none',
    width: '100%',
    height: 'auto',
    borderRadius: '0.75rem'
  };
  const media = isVideo ? <video src={inView ? src : undefined} autoPlay loop muted playsInline preload="metadata" onLoadedData={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} /> : <img src={inView ? src : undefined} alt={caption} onLoad={() => setStatus('loaded')} onError={() => setStatus('missing')} style={mediaStyle} />;
  return <figure style={{
    margin: '1.25rem 0'
  }}>
      <div ref={boxRef} style={status === 'loaded' ? {
    width: '100%'
  } : {
    width: '100%',
    aspectRatio: ratio,
    borderRadius: '0.75rem',
    background: 'rgba(128,128,128,0.06)'
  }}>
        {media}
      </div>
      {status === 'loaded' && caption ? <figcaption style={{
    marginTop: '0.5rem',
    textAlign: 'center',
    fontSize: '0.85rem',
    opacity: 0.7
  }}>
          {caption}
        </figcaption> : null}
    </figure>;
};

The `modulex-js` package is the official JavaScript and TypeScript SDK for ModuleX. It is a thin, fully typed client over the [ModuleX REST API](/api-reference/overview) and its SSE streams: every method maps to one HTTP call, so the SDK never runs a workflow locally. This page covers installing the package, constructing the client, and the wire conventions every method shares. For per-operation method docs, see [SDKs overview](/sdks/overview), [streaming & HITL](/sdks/streaming-hitl), [errors & retries](/sdks/errors-retries), and the [SDK to API parity matrix](/sdks/parity).

## Package facts

| Property             | Value                                                                  |
| -------------------- | ---------------------------------------------------------------------- |
| Package name         | `modulex-js`                                                           |
| Version              | `1.0.0`                                                                |
| License              | MIT                                                                    |
| Runtime dependencies | none                                                                   |
| Module formats       | dual ESM and CommonJS                                                  |
| Runtime requirement  | Node.js 18 or newer (uses `AbortSignal.timeout` and `AbortSignal.any`) |
| Surface              | 130 endpoints across 17 resource groups                                |

<Note>
  The SDK targets Node.js 18+ because it relies on the platform `fetch`, `AbortSignal.timeout`, and `AbortSignal.any`. In a browser, a current evergreen browser provides the same APIs. On older runtimes, pass a polyfilled `fetch` through the `fetch` config option (see [Configuration options](#configuration-options)).
</Note>

## Install

Install `modulex-js` from npm with your package manager of choice.

<CodeGroup>
  ```bash npm theme={null}
  npm install modulex-js
  ```

  ```bash pnpm theme={null}
  pnpm add modulex-js
  ```

  ```bash yarn theme={null}
  yarn add modulex-js
  ```
</CodeGroup>

The package ships both an ESM build and a CommonJS build, so both import styles resolve to the same client.

<CodeGroup>
  ```typescript ESM theme={null}
  import { Modulex } from "modulex-js";
  ```

  ```javascript CommonJS theme={null}
  const { Modulex } = require("modulex-js");
  ```
</CodeGroup>

## Initialize the client

Construct the client with `new Modulex(config)`. The constructor takes a single [`ModulexConfig`](#configuration-options) object. You must pass `apiKey` explicitly, and you almost always pass `organizationId` as well, because most ModuleX endpoints are organization-scoped.

Create an API key from the ModuleX dashboard at `https://app.modulex.dev`. ModuleX API keys carry the `mx_live_` prefix; the SDK sends the key as an `Authorization: Bearer` header. See [Authentication](/api-reference/authentication) and [Auth model: JWT vs API key](/security/authentication) for how keys differ from the Clerk JWT used by the web app.

The example below initializes the client and makes one read-only call (`auth.me`) so you can confirm the credentials resolve. The cURL tab shows the exact request the SDK sends under the hood.

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.modulex.dev/auth/me \
    -H "Authorization: Bearer mx_live_8f3c2a1b9d4e5f6a7b8c9d0e" \
    -H "X-Organization-ID: org_4f9a2c1e7b3d"
  ```

  ```typescript JavaScript theme={null}
  import { Modulex } from "modulex-js";

  const client = new Modulex({
    apiKey: "mx_live_8f3c2a1b9d4e5f6a7b8c9d0e",
    organizationId: "org_4f9a2c1e7b3d",
  });

  const me = await client.auth.me();
  console.log(me.email);
  ```

  ```python Python theme={null}
  from modulex import Modulex

  # The Python SDK is documented separately; see /sdks/python.
  client = Modulex(
      api_key="mx_live_8f3c2a1b9d4e5f6a7b8c9d0e",
      organization_id="org_4f9a2c1e7b3d",
  )

  me = await client.auth.me()
  print(me.email)
  ```
</CodeGroup>

<Note>
  The constructor only stores the resolved configuration. Each resource group (such as `client.auth` or `client.workflows`) is created lazily the first time you access it, so constructing the client is cheap and does not open a connection or validate the key against the server. Credentials are only checked when you make a request.
</Note>

<MediaEmbed id="MX-MEDIA-2010" type="screenshot" caption={"The API keys settings page in the ModuleX dashboard where a developer creates an `mx_live_` key."} />

### No environment-variable fallback in JavaScript

This is the most important difference between the JavaScript and Python SDKs, and a common source of confusion.

<Warning>
  The JavaScript SDK has **no environment-variable fallback**. `apiKey` and `organizationId` must be passed explicitly to the `Modulex` constructor. The SDK never reads `MODULEX_API_KEY`, `MODULEX_BASE_URL`, or `MODULEX_ORGANIZATION_ID` from the environment.
</Warning>

If you omit `apiKey` (or pass an empty string), the constructor throws synchronously, before any request is made:

```text Error message theme={null}
ModuleX API key is required. Pass `apiKey` to the Modulex constructor.
```

The thrown value is a plain `Error`. This differs from the Python SDK, which reads `MODULEX_API_KEY` / `MODULEX_BASE_URL` / `MODULEX_ORGANIZATION_ID` from the environment when the corresponding argument is omitted, and raises `ValueError` if neither a key nor the env var is set. See [Python SDK](/sdks/python) and the [parity matrix](/sdks/parity) for the full cross-SDK comparison.

If you want to drive the JavaScript client from environment variables, read them yourself in your own code and pass them in. The pattern below is caller code, not SDK behavior:

```typescript Read env vars yourself theme={null}
import { Modulex } from "modulex-js";

const apiKey = process.env.MODULEX_API_KEY;
if (!apiKey) {
  throw new Error("Set MODULEX_API_KEY in your environment.");
}

const client = new Modulex({
  apiKey,
  organizationId: process.env.MODULEX_ORG_ID,
});
```

## Configuration options

`ModulexConfig` is the single object you pass to `new Modulex(config)`. Only `apiKey` is required.

<ParamField path="apiKey" type="string" required>
  Your ModuleX API key, including the `mx_live_` prefix. Sent on every request as the `Authorization: Bearer <apiKey>` header. If this value is empty or missing, the constructor throws synchronously (see [No environment-variable fallback](#no-environment-variable-fallback-in-javascript)).
</ParamField>

<ParamField path="organizationId" type="string">
  The default organization context for every request, sent as the `X-Organization-ID` header. Most ModuleX endpoints are organization-scoped and return a `400` when no organization context is present, so set this unless you override it per request. Overridable per call through [`RequestOptions.organizationId`](#per-request-options). Defaults to `undefined` — when no organization id resolves, the `X-Organization-ID` header is omitted entirely.
</ParamField>

<ParamField path="baseUrl" type="string" default="https://api.modulex.dev">
  The root of the ModuleX REST API. Routers are mounted at the root with **no version prefix**, so do not append `/api` or `/v1`. Trailing slashes are stripped automatically. Point this at your dev server for local development, for example `http://localhost:8000`. See [Base URLs, environments & versioning](/get-started/environments).
</ParamField>

<ParamField path="timeout" type="number" default="30000">
  Per-request timeout in milliseconds, applied with `AbortSignal.timeout`. Overridable per call through [`RequestOptions.timeout`](#per-request-options).
</ParamField>

<ParamField path="maxRetries" type="number" default="3">
  Maximum number of automatic retries for transient failures (`429`, `500`, `502`, `503`) and network errors. The total number of attempts is `maxRetries + 1`. Non-transient statuses (`400`, `401`, `403`, `404`, `409`, `422`) are never retried. Retry timing and backoff are detailed in [Errors & retries](/sdks/errors-retries).
</ParamField>

<ParamField path="fetch" type="typeof globalThis.fetch" default="globalThis.fetch">
  A custom `fetch` implementation. Use this to supply a polyfill on older runtimes, route requests through a proxy, or inject a mock in tests. Defaults to the platform `globalThis.fetch`.
</ParamField>

The full set of defaults the SDK applies during `resolveConfig`:

<CodeGroup>
  ```typescript Defaults applied theme={null}
  const client = new Modulex({
    apiKey: "mx_live_8f3c2a1b9d4e5f6a7b8c9d0e", // required
    organizationId: "org_4f9a2c1e7b3d",          // default: undefined
    baseUrl: "https://api.modulex.dev",          // default: https://api.modulex.dev
    timeout: 30_000,                              // default: 30000 (ms)
    maxRetries: 3,                               // default: 3
    // fetch defaults to globalThis.fetch
  });
  ```
</CodeGroup>

<Info>
  `baseUrl` is normalized by stripping any trailing slashes. The SDK builds each request URL as `baseUrl + path`, where `path` already starts with a leading slash (for example `/auth/me`). Adding `/api` or `/v1` to `baseUrl` produces a wrong URL because the backend mounts routers at the root.
</Info>

## How the client is structured

The client exposes 17 resource groups as lazy getters. Each is instantiated on first access and reuses the same resolved configuration, so they all share your credentials, base URL, timeout, and retry policy.

<Expandable title="The 17 resource groups on the client">
  | Accessor               | Purpose                                                |
  | ---------------------- | ------------------------------------------------------ |
  | `client.auth`          | Authentication and user profile                        |
  | `client.apiKeys`       | API key management                                     |
  | `client.organizations` | Organization management                                |
  | `client.workflows`     | Workflow CRUD, builder details, and the changes stream |
  | `client.executions`    | Run, resume, cancel, get state, and listen (SSE)       |
  | `client.workflowRuns`  | Durable run history                                    |
  | `client.deployments`   | Deployment lifecycle                                   |
  | `client.chats`         | Chat sessions and messages, plus the chat-list stream  |
  | `client.credentials`   | Credentials, OAuth2, and MCP servers                   |
  | `client.integrations`  | Integration catalog browsing                           |
  | `client.knowledge`     | Knowledge bases, documents, and search                 |
  | `client.schedules`     | Cron and interval schedules, plus run history          |
  | `client.composer`      | The AI Composer workflow-builder agent (HITL)          |
  | `client.assistant`     | The Assistant chat agent (HITL)                        |
  | `client.dashboard`     | Logs and analytics                                     |
  | `client.notifications` | Notification feed                                      |
  | `client.system`        | Timezone utilities                                     |
</Expandable>

<Note>
  The JavaScript SDK does not include a `subscriptions` resource. Subscription and billing endpoints are only available in the [Python SDK](/sdks/python). The full method-by-method breakdown of which SDK covers which route is in the [parity matrix](/sdks/parity).
</Note>

## Headers the SDK injects

The SDK sets a small, fixed set of headers. You do not build these yourself.

<ResponseField name="Authorization" type="header">
  Always sent, as `Authorization: Bearer <apiKey>`. The API key is transmitted exclusively through this header; the SDK does not use the backend's alternative `X-API-KEY` header.
</ResponseField>

<ResponseField name="X-Organization-ID" type="header">
  Sent only when an organization id resolves (see [organization id precedence](#per-request-options)). When neither a per-request nor a client-level `organizationId` is set, this header is omitted. Note the capital `ID`.
</ResponseField>

<ResponseField name="Content-Type" type="header">
  Set to `application/json` on requests that carry a JSON body. For multipart uploads (such as uploading a knowledge document), the SDK omits `Content-Type` so the runtime sets the multipart boundary automatically.
</ResponseField>

<ResponseField name="Accept" type="header">
  Added for SSE streaming requests as `Accept: text/event-stream` (with `Cache-Control: no-cache` on GET streams). Covered in [streaming & HITL](/sdks/streaming-hitl).
</ResponseField>

<Warning>
  The authentication header is `Authorization: Bearer mx_live_…`, **not** `X-Authorization`. Every authenticated ModuleX request — REST, SDK, or otherwise — uses `Authorization: Bearer` plus `X-Organization-ID`. The backend also accepts `X-API-KEY` as an alternative, but the JavaScript SDK does not use it.
</Warning>

The SDK does **not** send a `User-Agent`, an `X-API-KEY`, or an API-version header.

## Per-request options

Every SDK method accepts an optional trailing `options` argument of type `RequestOptions`. Use it to override the organization context, add query parameters, cancel the request, or change the timeout for a single call.

<ParamField path="organizationId" type="string">
  Override the `X-Organization-ID` header for this one call. Takes precedence over the client-level `organizationId`.
</ParamField>

<ParamField path="params" type="Record<string, string | number | boolean | undefined>">
  Extra query parameters appended to the URL. Keys are converted from camelCase to snake\_case, and `undefined` values are skipped. For example, `{ pageSize: 20 }` becomes `?page_size=20`.
</ParamField>

<ParamField path="signal" type="AbortSignal">
  An abort signal to cancel the in-flight request or stream. It is combined with the timeout signal through `AbortSignal.any`, so either source can abort the call.
</ParamField>

<ParamField path="timeout" type="number">
  A per-request timeout in milliseconds that overrides the client-level `timeout` for this call only.
</ParamField>

### Organization id precedence

The organization context for a call resolves in this order:

<Steps>
  <Step title="Per-request option">
    `options.organizationId`, if provided on the call.
  </Step>

  <Step title="Client-level default">
    The `organizationId` you passed to the constructor.
  </Step>

  <Step title="No header">
    If neither is set, the SDK omits the `X-Organization-ID` header. Organization-scoped endpoints then return a `400`.
  </Step>
</Steps>

The example below sets a client-level default and overrides it for one call:

<CodeGroup>
  ```typescript JavaScript theme={null}
  const client = new Modulex({
    apiKey: "mx_live_8f3c2a1b9d4e5f6a7b8c9d0e",
    organizationId: "org_4f9a2c1e7b3d", // default for every call
  });

  // Uses the client-level organization.
  const { workflows } = await client.workflows.list({ status: "active" });

  // Overrides the organization for this one call.
  const other = await client.workflows.list(
    { status: "active" },
    { organizationId: "org_7b1d9e3a5c2f" },
  );
  ```
</CodeGroup>

<Info>
  Some request bodies also carry their own `organizationId` field (for example when creating an API key). That body field sets a resource scope and is independent of the `X-Organization-ID` header. Do not conflate the two.
</Info>

## Wire conventions: camelCase in, snake\_case out

The SDK lets you write idiomatic JavaScript on the way in, but it does not normalize the response on the way out. Knowing this asymmetry up front prevents a common surprise.

<Card title="Requests: camelCase is converted to snake_case" icon="arrow-right">
  Request bodies and query parameter keys you pass in camelCase are converted to snake\_case before the request is sent. For example, `workflowId` becomes `workflow_id` and `pageSize` becomes `page_size`. The conversion recurses through nested objects and arrays, and leaves `Date`, `Blob`, and `File` instances, plus `null`, `undefined`, and primitive values, unchanged.
</Card>

<Card title="Responses: snake_case is returned unchanged" icon="arrow-left">
  Responses are **not** converted back. The SDK returns the JSON exactly as the API sends it, so response fields stay snake\_case — you read `run_id`, `created_at`, and `thread_id`, not `runId` or `createdAt`. The SDK's own TypeScript response types declare these fields in snake\_case, so your editor reflects the real shape.
</Card>

A round trip showing both directions:

<CodeGroup>
  ```typescript JavaScript theme={null}
  // You pass camelCase. The SDK sends ?page_size=20 on the wire.
  const page = await client.workflows.list(
    { status: "active" },
    { params: { pageSize: 20 } },
  );

  // The response is snake_case. Read snake_case fields.
  for (const workflow of page.workflows) {
    console.log(workflow.id, workflow.created_at);
  }
  console.log(page.page_size, page.has_next);
  ```
</CodeGroup>

<Warning>
  Do not expect camelCase keys on responses. Reading `workflow.createdAt` returns `undefined`; the field is `workflow.created_at`. This is intentional: the SDK converts request input to snake\_case but returns responses verbatim.
</Warning>

## Errors and retries at a glance

When a response is not successful, the SDK parses the error body and throws a typed error. The importable error classes — including `ModulexError`, `AuthenticationError`, `PermissionError`, `NotFoundError`, `ValidationError`, and `RateLimitError` — support `instanceof` checks. The base `ModulexError` surfaces `code`, `reason`, and `layer` when the API returns the structured envelope.

Transient failures (`429`, `500`, `502`, `503`) and network errors are retried up to `maxRetries` times with exponential backoff that honors a `Retry-After` header when present. Non-transient statuses (`400`, `401`, `403`, `404`, `409`, `422`) are thrown immediately without retry. A timeout or an aborted request is mapped to a `TimeoutError`.

Operations that hit a metered surface — running a workflow, the Composer, the Assistant, or managed knowledge — can return the billing gate's `DenialEnvelope` as a `402`, `403`, or `429` with the shape `{code, layer, key, current, limit, reason}`. Note that the JavaScript SDK has no dedicated payment error class, so a `402` is thrown as the base `ModulexError` — branch on `error.code` or `error.layer` rather than the class. The complete error taxonomy, the three response envelope shapes, and the full retry policy are documented in [Errors & retries](/sdks/errors-retries) and [Errors & status codes](/api-reference/errors).

<MediaEmbed id="MX-MEDIA-2011" type="image" caption={"A diagram of the request lifecycle inside the SDK, from method call to typed result or typed error."} />

## Cancellation and timeouts

Each request runs under an abort signal that combines the effective timeout (`AbortSignal.timeout`) with any `signal` you pass in `RequestOptions` (`AbortSignal.any`). Either source can cancel the call: the request rejects with a `TimeoutError` on timeout or abort.

<CodeGroup>
  ```typescript JavaScript theme={null}
  const controller = new AbortController();

  // Cancel from elsewhere, for example on user navigation.
  setTimeout(() => controller.abort(), 1_000);

  try {
    const { workflows } = await client.workflows.list(
      { status: "active" },
      { signal: controller.signal, timeout: 5_000 },
    );
    console.log(workflows.length);
  } catch (error) {
    // Aborted or timed out -> TimeoutError.
    console.error(error);
  }
  ```
</CodeGroup>

The same `signal` cancels long-lived SSE streams (such as `executions.listen`). Streaming consumption is covered in [streaming & HITL](/sdks/streaming-hitl).

<MediaEmbed id="MX-MEDIA-2012" type="app_video" caption={"A short screen recording of a developer installing modulex-js, pasting an `mx_live_` key, and running the `auth.me` call to confirm setup."} />

## Next steps

<CardGroup cols={2}>
  <Card title="SDKs overview" icon="layer-group" href="/sdks/overview">
    See how the JavaScript and Python SDKs map onto every REST operation.
  </Card>

  <Card title="Streaming & HITL" icon="signal-stream" href="/sdks/streaming-hitl">
    Consume SSE run streams and answer human-in-the-loop prompts.
  </Card>

  <Card title="Errors & retries" icon="triangle-exclamation" href="/sdks/errors-retries">
    Full error classes, the retry policy, and idempotency behavior.
  </Card>

  <Card title="Parity matrix" icon="table-columns" href="/sdks/parity">
    The route-by-route map of JavaScript and Python methods, with gaps called out.
  </Card>
</CardGroup>
