Skip to main content
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 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, streaming & HITL, errors & retries, and the SDK to API parity matrix.

Package facts

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

Install

Install modulex-js from npm with your package manager of choice.
The package ships both an ESM build and a CommonJS build, so both import styles resolve to the same client.

Initialize the client

Construct the client with new Modulex(config). The constructor takes a single ModulexConfig 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 and Auth model: JWT vs API key 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.
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.

No environment-variable fallback in JavaScript

This is the most important difference between the JavaScript and Python SDKs, and a common source of confusion.
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.
If you omit apiKey (or pass an empty string), the constructor throws synchronously, before any request is made:
Error message
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 and the parity matrix 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:
Read env vars yourself

Configuration options

ModulexConfig is the single object you pass to new Modulex(config). Only apiKey is required.
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).
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. Defaults to undefined — when no organization id resolves, the X-Organization-ID header is omitted entirely.
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.
number
default:"30000"
Per-request timeout in milliseconds, applied with AbortSignal.timeout. Overridable per call through RequestOptions.timeout.
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.
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.
The full set of defaults the SDK applies during resolveConfig:
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.

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.
The JavaScript SDK does not include a subscriptions resource. Subscription and billing endpoints are only available in the Python SDK. The full method-by-method breakdown of which SDK covers which route is in the parity matrix.

Headers the SDK injects

The SDK sets a small, fixed set of headers. You do not build these yourself.
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.
header
Sent only when an organization id resolves (see organization id precedence). When neither a per-request nor a client-level organizationId is set, this header is omitted. Note the capital ID.
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.
header
Added for SSE streaming requests as Accept: text/event-stream (with Cache-Control: no-cache on GET streams). Covered in streaming & HITL.
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.
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.
string
Override the X-Organization-ID header for this one call. Takes precedence over the client-level organizationId.
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.
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.
number
A per-request timeout in milliseconds that overrides the client-level timeout for this call only.

Organization id precedence

The organization context for a call resolves in this order:
1

Per-request option

options.organizationId, if provided on the call.
2

Client-level default

The organizationId you passed to the constructor.
3

No header

If neither is set, the SDK omits the X-Organization-ID header. Organization-scoped endpoints then return a 400.
The example below sets a client-level default and overrides it for one call:
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.

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.

Requests: camelCase is converted to snake_case

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.

Responses: snake_case is returned unchanged

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.
A round trip showing both directions:
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.

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 and Errors & status codes.

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.
The same signal cancels long-lived SSE streams (such as executions.listen). Streaming consumption is covered in streaming & HITL.

Next steps

SDKs overview

See how the JavaScript and Python SDKs map onto every REST operation.

Streaming & HITL

Consume SSE run streams and answer human-in-the-loop prompts.

Errors & retries

Full error classes, the retry policy, and idempotency behavior.

Parity matrix

The route-by-route map of JavaScript and Python methods, with gaps called out.