> ## 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.

# ModuleX SDKs: JavaScript & Python overview

> Install, authenticate, and configure the official ModuleX JavaScript (modulex-js) and Python (modulex-python) SDKs, and learn how SDK operations map one-to-one to the REST API.

ModuleX ships two official client SDKs — `modulex-js` for JavaScript and TypeScript, and `modulex-python` for Python. Both are thin, typed clients over the same ModuleX REST API and Server-Sent Events streams: they do not run workflows locally, and every method maps to one HTTP call. Pick the SDK for your runtime, install it once, configure authentication and your organization context, and you have typed access to every operation in the platform.

This page covers installation, client configuration, the authentication scheme, and how SDK operations correspond to REST routes. For the side-by-side method-to-route table and the parity gaps between the two SDKs, see the [SDK to API parity matrix](/sdks/parity).

<Note>
  Both SDKs are at version `1.0.0`. The JavaScript SDK requires Node 18 or newer; the Python SDK requires Python 3.9 or newer and is async-only.
</Note>

## Install

Install the SDK for your language. The JavaScript package is published to npm as `modulex-js`; the Python package is published to PyPI as `modulex-python` and imported as `modulex`.

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

  ```bash pip theme={null}
  pip install modulex-python
  ```
</CodeGroup>

The JavaScript SDK has zero runtime dependencies and ships a dual ESM + CommonJS build, so it works with both `import` and `require`. The Python SDK is built on `httpx.AsyncClient` and exposes a single async client class.

<Warning>
  Do not install Python extras such as `modulex-python[all]` or per-tool extras like `modulex-python[github,slack]`. These extras are currently empty: requesting them makes pip emit a warning and installs only the core package. Integrations are loaded server-side by ModuleX, not bundled into the SDK, so the core install is all you need. See [installing and using integrations](/integrations/install) for how integrations load at runtime.
</Warning>

## Create a client

You need a ModuleX API key (prefix `mx_live_`) and, for any organization-scoped operation, your organization id. Create an API key from the ModuleX dashboard, then pass it to the client constructor. See [authentication](/api-reference/authentication) for how to obtain and scope a key.

<CodeGroup>
  ```bash cURL theme={null}
  # The SDKs wrap this request shape. With cURL you set the headers yourself.
  curl https://api.modulex.dev/auth/me \
    -H "Authorization: Bearer mx_live_your_api_key" \
    -H "X-Organization-ID: org_your_organization_id"
  ```

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

  async def main():
      async with Modulex(
          api_key="mx_live_your_api_key",
          organization_id="org_your_organization_id",
      ) as client:
          me = await client.auth.me()
          print(me["email"])

  asyncio.run(main())
  ```

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

  const client = new Modulex({
    apiKey: "mx_live_your_api_key",
    organizationId: "org_your_organization_id",
  });

  const me = await client.auth.me();
  console.log(me.email);
  ```
</CodeGroup>

The Python client is an async context manager. Using `async with` closes the underlying HTTP client for you on exit; if you construct the client without `async with`, call `await client.close()` when you are done. The JavaScript client needs no explicit teardown.

<Note>
  The Python client throws synchronously if no API key is found: `ValueError("api_key is required: pass api_key=... or set the MODULEX_API_KEY environment variable")`. The JavaScript client throws a plain `Error` with the message `ModuleX API key is required. Pass apiKey to the Modulex constructor.` (the literal property name is `apiKey`).
</Note>

### Configuration options

Both SDKs accept the same set of settings; the option names follow each language's idiom (camelCase in JavaScript, snake\_case in Python), and the semantics and defaults are identical.

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

  client = Modulex(
      api_key="mx_live_your_api_key",       # required (arg or MODULEX_API_KEY)
      organization_id="org_your_org_id",    # optional default org context
      base_url="https://api.modulex.dev",   # optional, this is the default
      timeout=30.0,                         # optional, seconds (default 30.0)
      max_retries=3,                        # optional (default 3)
      default_headers={"X-Trace": "demo"},  # optional, merged into every request
  )
  ```

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

  const client = new Modulex({
    apiKey: "mx_live_your_api_key",          // required
    organizationId: "org_your_org_id",       // optional default org context
    baseUrl: "https://api.modulex.dev",      // optional, this is the default
    timeout: 30000,                          // optional, milliseconds (default 30000)
    maxRetries: 3,                           // optional (default 3)
    fetch: customFetch,                      // optional, custom fetch implementation
  });
  ```
</CodeGroup>

<ParamField path="apiKey / api_key" type="string" required>
  Your ModuleX API key with the `mx_live_` prefix. Sent as `Authorization: Bearer <key>`. Required in both SDKs. In Python it may instead be supplied through the `MODULEX_API_KEY` environment variable; in JavaScript there is no environment-variable fallback and it must be passed to the constructor (see [environment-variable fallback](#environment-variable-fallback) below).
</ParamField>

<ParamField path="organizationId / organization_id" type="string" default="undefined / None">
  The default organization context for every request, sent as the `X-Organization-ID` header. Optional; can be overridden per request. In Python it may also come from `MODULEX_ORGANIZATION_ID`. When no organization id is resolved, the SDK omits the header entirely.
</ParamField>

<ParamField path="baseUrl / base_url" type="string" default="https://api.modulex.dev">
  The REST API root. Do not append a version segment such as `/v1` or an `/api` prefix — ModuleX routers mount at the root with their own per-resource prefixes. Trailing slashes are stripped. In Python it may also come from `MODULEX_BASE_URL`. See [base URLs and versioning](/api-reference/environments).
</ParamField>

<ParamField path="timeout" type="number" default="30000 ms (JS) / 30.0 s (Python)">
  Request timeout. JavaScript expresses it in milliseconds (default `30000`); Python in seconds as a float (default `30.0`). There is no environment-variable fallback for this option in either SDK.
</ParamField>

<ParamField path="maxRetries / max_retries" type="number" default="3">
  Maximum automatic retries for transient failures. See [retry behavior](#retry-and-timeout-behavior). No environment-variable fallback in either SDK.
</ParamField>

<ParamField path="fetch" type="function" default="globalThis.fetch">
  JavaScript only. A custom `fetch` implementation. There is no equivalent custom-transport option in the Python client.
</ParamField>

<ParamField path="default_headers" type="object" default="empty">
  Python only. Extra headers merged into every request. These are spread before the auth and content-type headers, so they cannot override `Authorization` or `Content-Type`, but they can set or override others such as `User-Agent`.
</ParamField>

#### Environment-variable fallback

The two SDKs differ here, and it is a common source of confusion.

| Setting                     | JavaScript                          | Python                                                  |
| --------------------------- | ----------------------------------- | ------------------------------------------------------- |
| API key                     | constructor only; throws if missing | `api_key` arg **or** `MODULEX_API_KEY`                  |
| Base URL                    | constructor only                    | `base_url` arg **or** `MODULEX_BASE_URL` **or** default |
| Organization id             | constructor only                    | `organization_id` arg **or** `MODULEX_ORGANIZATION_ID`  |
| Timeout / retries / headers | constructor only                    | constructor only                                        |

The JavaScript README reads `process.env.MODULEX_API_KEY` in its examples, but that is caller code passing the value to the constructor — the JavaScript SDK itself performs no environment lookup. Do not rely on automatic `MODULEX_*` pickup in JavaScript. The per-SDK detail lives in [JavaScript SDK](/sdks/javascript) and [Python SDK](/sdks/python).

## Authentication and organization context

Every request both SDKs make carries the same two headers, matching the [REST authentication](/api-reference/authentication) and [auth model](/security/authentication) contract:

* `Authorization: Bearer mx_live_…` — your API key as a Bearer token. This is the correct header. There is no `X-Authorization` header anywhere in either SDK or the backend. (The backend also accepts an alternative `X-API-KEY` header, but neither SDK sends it.)
* `X-Organization-ID: <org id>` — added only when an organization id is resolved, scoping the request to one [organization](/concepts/organizations-roles).

The organization id resolves in this order: a per-request override, then the client-level default, then (in Python only) the `MODULEX_ORGANIZATION_ID` environment variable. If none resolve, the header is omitted; org-scoped endpoints will then return a `400` with the message `X-Organization-ID header is required`. See [org context](/security/org-context) for which endpoints require it.

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

  ```python Python theme={null}
  # Per-request override of the organization context
  workflows = await client.workflows.list(
      organization_id="org_other_organization_id",
  )
  ```

  ```javascript JavaScript theme={null}
  // Per-request override of the organization context
  const { workflows } = await client.workflows.list(
    {},
    { organizationId: "org_other_organization_id" },
  );
  ```
</CodeGroup>

<Warning>
  Composer, Assistant, and Knowledge operations require the `owner` or `admin` organization role. The `member` role has been retired and is no longer a current first-class role; callers without `owner`/`admin` receive a `403`. See [roles and permissions](/security/roles-permissions).
</Warning>

### Per-request options

Each JavaScript method accepts an optional trailing `RequestOptions` object; each Python method accepts equivalent keyword arguments. Both let you override the organization id, set extra query parameters, and control cancellation and timeout for a single call.

<ParamField path="organizationId / organization_id" type="string">
  Override the organization context for this one call.
</ParamField>

<ParamField path="signal" type="AbortSignal">
  JavaScript only. Cancel the in-flight request or stream. It is combined with the timeout signal, so an abort or a timeout both surface as a `TimeoutError`.
</ParamField>

<ParamField path="timeout" type="number">
  Override the client-level timeout for this call (milliseconds in JavaScript, seconds in Python).
</ParamField>

<ParamField path="params" type="object">
  JavaScript only. Extra query parameters; camelCase keys are converted to snake\_case on the URL (for example `pageSize` becomes `page_size`).
</ParamField>

## How SDK operations map to REST

Each SDK method corresponds to exactly one REST route. The SDK builds the URL as `base_url + path` (no version prefix), sets the auth and organization headers, sends the request, and returns the parsed response. The example below shows the same operation — running a workflow — three ways: the raw REST call, then the Python and JavaScript SDK equivalents that wrap it.

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST https://api.modulex.dev/workflows/run \
    -H "Authorization: Bearer mx_live_your_api_key" \
    -H "X-Organization-ID: org_your_organization_id" \
    -H "Content-Type: application/json" \
    -d '{
      "workflow_id": "wf_your_workflow_id",
      "input": { "messages": [{ "role": "user", "content": "Hello" }] },
      "stream": true
    }'
  ```

  ```python Python theme={null}
  run = await client.executions.run(
      workflow_id="wf_your_workflow_id",
      input={"messages": [{"role": "user", "content": "Hello"}]},
      stream=True,
  )
  print(run["run_id"], run["thread_id"])
  ```

  ```javascript JavaScript theme={null}
  const run = await client.executions.run({
    workflowId: "wf_your_workflow_id",
    input: { messages: [{ role: "user", content: "Hello" }] },
    stream: true,
  });
  console.log(run.run_id, run.thread_id);
  ```
</CodeGroup>

The method `executions.run` maps to `POST /workflows/run`. To follow the run as it streams, pass the returned `run_id` to `executions.listen`, which maps to the SSE route `GET /workflows/listen/{run_id}` — see [SSE run streaming](/realtime/sse-streaming) and [streaming and HITL in the SDKs](/sdks/streaming-hitl). The end-to-end recipe is in [run a workflow](/guides/run-a-workflow).

<Note>
  The legacy LLM-only mode of `POST /workflows/run` is deprecated and returns `410 Gone`; for general agentic chat use `assistant.chat` (`POST /assistant/chat`) instead. See the [Assistant](/concepts/assistant) and the [parity matrix](/sdks/parity).
</Note>

### Naming and the resource grouping

Both SDKs expose 17 resource groups (for example `client.workflows`, `client.executions`, `client.credentials`). Method names follow each language's convention: JavaScript uses camelCase (`setDefault`, `getState`) and Python uses snake\_case (`set_default`, `get_state`) for the same route. A handful of methods diverge by more than casing — for example `composer.focus` in JavaScript is `composer.set_focus` in Python. The complete route-to-method table, including every name divergence, lives in the [parity matrix](/sdks/parity).

<Expandable title="Resource groups and their SDK accessors">
  The two SDKs cover the same REST surface with one difference: the `subscriptions` resource exists in Python only.

  | REST area                      | JavaScript accessor    | Python accessor                 |
  | ------------------------------ | ---------------------- | ------------------------------- |
  | Auth & profile                 | `client.auth`          | `client.auth`                   |
  | API keys                       | `client.apiKeys`       | `client.api_keys`               |
  | Organizations                  | `client.organizations` | `client.organizations`          |
  | Workflows (CRUD + builder)     | `client.workflows`     | `client.workflows`              |
  | Run / resume / cancel / listen | `client.executions`    | `client.executions`             |
  | Durable run history            | `client.workflowRuns`  | folded into `client.executions` |
  | Deployments                    | `client.deployments`   | `client.deployments`            |
  | Chats                          | `client.chats`         | `client.chats`                  |
  | Credentials & OAuth2 / MCP     | `client.credentials`   | `client.credentials`            |
  | Integration catalog            | `client.integrations`  | `client.integrations`           |
  | Knowledge bases & search       | `client.knowledge`     | `client.knowledge`              |
  | Schedules                      | `client.schedules`     | `client.schedules`              |
  | AI Composer                    | `client.composer`      | `client.composer`               |
  | Assistant                      | `client.assistant`     | `client.assistant`              |
  | Dashboard & analytics          | `client.dashboard`     | `client.dashboard`              |
  | Notifications                  | `client.notifications` | `client.notifications`          |
  | System utilities               | `client.system`        | `client.system`                 |
  | Subscriptions & billing        | not available          | `client.subscriptions`          |

  Run history is grouped differently: JavaScript exposes a separate `client.workflowRuns` group (`list`, `get`), while Python folds the same routes into `client.executions` as `list_runs`, `iter_runs`, and `get_run`. Both call the same `GET /workflow-runs` routes.
</Expandable>

<Warning>
  The `subscriptions` resource is available in the Python SDK only; the JavaScript SDK has no `subscriptions` methods. Even in Python, only the read and link routes are wrapped (`organization_plans`, `organization_billing`, `checkout_link`, `customer_portal`); the wallet and plan-transition routes are not exposed by either SDK. For subscription and Stripe details see [subscriptions and Stripe](/billing/subscription-lifecycle) and the [parity matrix](/sdks/parity).
</Warning>

### Request and response casing

The SDKs differ in how they handle field casing on the wire, and this affects how you read responses.

* JavaScript: request bodies and query keys you pass in camelCase are converted to snake\_case before sending. Responses are not converted back — response fields stay snake\_case (for example `run_id`, `thread_id`, `created_at`). This camelCase-in, snake\_case-out asymmetry is intentional.
* Python: requests and responses are snake\_case end-to-end. Responses are Pydantic models that also support dict-style access, so both `resp.status` and `resp["status"]` work.

In the run example above, notice that even in JavaScript you read `run.run_id` (snake\_case) from the response, while the request used `workflowId` (camelCase).

## Retry and timeout behavior

Both SDKs retry transient failures automatically with exponential backoff and jitter, honoring a `Retry-After` header when present.

* Retryable statuses: `429`, `500`, `502`, `503`.
* Never retried: `400`, `401`, `403`, `404`, `409`, `422` — these are thrown immediately.
* Total attempts equal `maxRetries + 1`.

The SDKs differ on which methods are retried. JavaScript retries any verb on a retryable status (and network errors). Python retries only idempotent methods (`GET`/`HEAD`); `POST`, `PUT`, `PATCH`, and `DELETE` are never retried on an error status.

<Note>
  The Python SDK can send an `Idempotency-Key` header when you pass `idempotency_key=` to a mutating call, but the run endpoint assigns its own `run_id`, so it does not de-duplicate runs. See [errors and retries](/sdks/errors-retries).
</Note>

## Errors

Both SDKs map HTTP error statuses to typed exceptions that extend a base error class (`ModulexError` in both). Operations that pass through the billing admission gate — running workflows, Composer, Assistant, and managed knowledge — can return a flat `DenialEnvelope` with the shape `{code, layer, key, current, limit, reason}` as `402`, `403`, or `429`.

The two SDKs surface billing denials differently. Python maps `402` to `PaymentRequiredError` and raises billing subclasses keyed by the envelope `layer` — `QuotaExceededError` (403), `CreditExhaustedError` (402), and `WalletError` (402). JavaScript has no payment-specific error class: a `402` falls through to the base `ModulexError`. The full taxonomy, including the three REST error-envelope shapes, is documented in [errors and status codes](/api-reference/errors), [usage gating and limits](/billing/usage-gating), and [SDK errors and retries](/sdks/errors-retries).

<CodeGroup>
  ```python Python theme={null}
  from modulex import CreditExhaustedError, RateLimitError

  try:
      run = await client.executions.run(workflow_id="wf_your_workflow_id")
  except CreditExhaustedError as e:        # 402, layer="credit"
      print(f"Out of credits: {e.current}/{e.limit}")
  except RateLimitError as e:              # 429
      print(f"Rate limited; retry after {e.retry_after}s")
  ```

  ```javascript JavaScript theme={null}
  import { ModulexError, RateLimitError } from "modulex-js";

  try {
    const run = await client.executions.run({ workflowId: "wf_your_workflow_id" });
  } catch (e) {
    if (e instanceof RateLimitError) {
      console.error(`Rate limited; retry after ${e.retryAfter}s`);
    } else if (e instanceof ModulexError) {
      // 402 billing denials surface here (no payment-specific class in JS)
      console.error(e.code, e.reason);
    }
  }
  ```
</CodeGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="JavaScript SDK" icon="js" href="/sdks/javascript">
    Install, configure, and use the modulex-js client.
  </Card>

  <Card title="Python SDK" icon="python" href="/sdks/python">
    Install and configure the async modulex-python client.
  </Card>

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

  <Card title="SDK to API parity" icon="table" href="/sdks/parity">
    The full route-to-method map, with every gap and name divergence called out.
  </Card>
</CardGroup>
