This matrix covers method-to-route coverage and cross-SDK naming and behavior divergence. Per-route status codes and error envelopes live in Errors and status codes; SSE frame shapes live in SSE run streaming; credit gating lives in Usage gating and limits.
How to read this page
The SDKs are thin clients over the backend REST surface. A method callsbase_url + path and returns the response. Three rules make the mapping predictable, and three structural facts make it diverge.
The predictable rules:
- Method names follow the route, with casing per language. JavaScript uses camelCase verbs (
setDefault,getState); Python uses snake_case (set_default,get_state). The naming divergences section lists the handful of cases where the names differ by more than casing. - Auth is identical in both SDKs. Every request sends
Authorization: Bearer mx_live_…plusX-Organization-IDwhen an organization is resolved. See Authentication. There is noX-Authorizationheader anywhere — neither SDK sends it and the backend does not read it. - One backend route can map to differently-grouped SDK methods. For example,
GET /workflow-runsisworkflowRuns.listin JavaScript butexecutions.list_runsin Python.
subscriptions resource exists only in Python; and environment-variable fallback exists only in Python.
The three structural divergences
These three facts are load-bearing for any side-by-side SDK code. Read them before the matrix.Responses: JS returns raw snake_case, Python returns typed models
Responses: JS returns raw snake_case, Python returns typed models
The JavaScript SDK takes camelCase parameters and converts them to snake_case before sending (There are two casing traps inside the JS request body itself: DSL config fields are already snake_case in TypeScript (
convertKeysToSnakeCase), but it does not convert responses back — it returns the parsed JSON verbatim, so response fields stay snake_case (run_id, created_at). A JS call is effectively response.json() as T: there is no runtime model, no validation, and no field renaming on the way out. This is the JS-only “raw response” behavior — camelCase in, snake_case out, asymmetric by design.The Python SDK is snake_case end to end. Requests are built as snake_case dicts verbatim, and responses are parsed into Pydantic models (configured extra="allow", with a dict-compatible shim). Field names are stable in both directions.JavaScript
Python
integration_name, system_prompt), so the converter is a no-op on them, and UpdateChatParams.is_private is snake_case in the TS parameter surface — both break the otherwise-camelCase JS convention.subscriptions resource: Python-only
subscriptions resource: Python-only
modulex-python exposes a full client.subscriptions resource (four methods). modulex-js has no subscriptions resource and no subscriptions methods — there is no subscriptions.ts in the JS SDK at all. This is a real parity gap, not a naming difference. See the subscriptions table and Subscriptions and Stripe.The JavaScript SDK surfaces billing only indirectly: organizations.invitePreview returns a prorated seat cost, and the dashboard analytics payloads carry subscription and current_month_credit_usage fields. If you need plan, billing, checkout, or customer-portal calls from JavaScript, call the REST routes directly with fetch.Environment-variable fallback: Python YES, JS NO
Environment-variable fallback: Python YES, JS NO
The Python client reads The JavaScript README shows
MODULEX_API_KEY, MODULEX_BASE_URL, and MODULEX_ORGANIZATION_ID from the environment when the corresponding argument is omitted. The JavaScript client reads none of them — apiKey must be passed to the constructor or it throws, and baseUrl / organizationId are constructor-only.JavaScript
Python
process.env.MODULEX_API_KEY in example code, but that is caller code, not SDK behavior. timeout, max_retries, and default_headers have no environment fallback in either SDK.Header divergences (the small ones)
The auth and org headers are identical across both SDKs. Only two header behaviors differ:Resource-group coverage
Both SDKs advertise 17 resource groups, but the sets differ by one: JavaScript lackssubscriptions. Two more groups are structured differently:
Per-resource method counts:
Two count differences are tally artifacts, not coverage gaps. The credentials sets are equivalent — Python counts its sync SSE factory (
bulk_modulex_keys_stream) separately, so it reads “15 + 1” against JS’s “17”. The organizations sets cover the same 11 routes; the JS note tallies 12. Treat both rows as full parity by route. The open question on the organizations tally is tracked in Open questions.The route-to-method matrix
Legend: a check means the SDK has a method for that route; a cross means it does not. Paths are backend-relative — the SDK callsbase_url + path. Where a method name differs by more than casing, the cell shows the exact name.
auth
POST /auth/organizations/leave requires X-Organization-ID.
workflows (CRUD and builder)
workflows.create, update, and delete require an owner or admin role. DELETE /workflows/{id} is a hard delete. listAll / list_all are auto-paginators over the same GET /workflows route, not separate endpoints.
executions (run, state, resume, cancel, listen)
These routes live under the/workflows router; on the SDK side they are executions.*.
The legacy LLM-only mode of
POST /workflows/run returns 410 Gone; use assistant.chat for agentic chat. getState / get_state return 404 (not 403) when you do not own the thread.
workflow-runs (durable history)
This is the clearest grouping divergence: JavaScript exposes a separate
workflowRuns resource; Python folds run history into executions. {runPk} is the run record’s id (returned by list/get), not the execution run_id. iter_runs is a Python-only offset auto-paginator over GET /workflow-runs.
deployments
List items omit
workflow_id and description. DeactivateDeploymentResponse.previous_live_deployment_id is optional in both SDKs (omitted on a no-op).
schedules
POST /schedules requires a live deployment. The admin tick route (include_in_schema=False) has no SDK method in either SDK. The exact response keys of retryRun / retry_run are an open question.
composer
POST /composer/chat returns 409 if a HITL prompt is pending, and 402 / 403 / 429 from the billing gate. resume mints a new run_id; its llm parameter is optional at the type level but required in production (the backend returns 400 without it). The focus / set_focus name difference is intentional.
assistant
POST /assistant/chat returns 409 on a pending HITL prompt or a run already in progress, plus 402 / 403 / 429 from the billing gate. Unlike composer, assistant.resume’s llm parameter is required at the type level in both SDKs. There is no assistant rename route on the backend (PATCH /assistant/chat/{id} / .../title does not exist) — this is an unbuilt route, not an SDK gap, so no method references it. See Assistant permissions and limits.
chats
chats.list groups by folder with no pagination. chats.messages uses offset/limit, and its body omits total / has_next despite the docstring. chats.delete is a soft delete with no permanent flag (unlike composer/assistant delete). There is no POST /chats route or method — chats are created implicitly by execution, so this is not a gap.
credentials
GET /credentials/oauth2/callback is a browser redirect target and has no SDK method by design. credentials.list returns a discriminated grouped/flat union in JavaScript but raw Any in Python (the backend types it Dict[str, Any]). The JS MCPServerCredentialResponse omits updated_at, integration_type, last_used_at, and expires_at versus the full CredentialResponse. bulkModulexKeys / bulk_modulex_keys_stream is a sync SSE factory on both sides.
integrations
GET /integrations/tools is admin-gated (organization_admin_required). Several detail methods carry a name difference (tool / tool_detail, and so on). See the Integrations overview.
knowledge-bases
Full 1:1 coverage (19 methods each). Note the two name differences beyond casing:
documents / list_documents and searchMultiple / multi_search. Managed (modulexdb) search and ingest are billed in credits — see Managed knowledge.
organizations
api-keys
organization_id is sent in the request body here, not via the X-Organization-ID header. apiKeys.revoke is unusual: the backend DELETE returns a JSON body with 200 (not 204), so both SDKs parse a result — unlike credentials/knowledge delete, which return 204.
dashboard
notifications
notifications.create maps to /notifications/organization, not /notifications. GET /notifications returns a null organization_id without a valid org context. POST /notifications/system is commented out backend-side, so neither SDK has a method — not a gap.
system
GET /system/health is a JavaScript gap: client.system is documented as “health and utility” but exposes no health() method. Python has system.health. timezones/search requires q to be at least 2 characters (else 422).
subscriptions (Python-only)
This is the largest parity gap. The four
subscriptions.* methods exist only in Python; the wallet and transition routes have no method in either SDK. To call any of these from JavaScript — or to call the wallet/transition routes from either SDK — issue the REST request directly:
Method-name divergences
For the same backend route, these names differ by more than casing. Everywhere else, the rule is JS camelCase ↔ Python snake_case of the same verb (setDefault ↔ set_default, getState ↔ get_state, retryRun ↔ retry_run).
Pagination and auto-paginators
Pagination style is per-resource, and the SDKs add convenience auto-paginators over the same routes — these are not separate endpoints. See Pagination.
The JavaScript SDK auto-paginates workflows only. Python adds
executions.iter_runs (offset over GET /workflow-runs) and workflows.list_all (page). Both auto-paginators are async iterators that call the underlying list route repeatedly.
Coverage gaps (explicit)
A complete accounting of where coverage is not 1:1.Routes with no method in either SDK
These are intentionally unexposed (webhook receivers, internal intake, ops diagnostics, super-admin console, and the wallet/transition billing routes).Routes present in one SDK only
Backend-route coverage
None. Every documented method in both SDKs maps to a real backend route. The only “method without a 1:1 typed route” cases are convenience auto-paginators (workflows.listAll / list_all, executions.iter_runs) that re-call an existing list route, and the bulk-keys SSE factory that maps to POST /credentials/bulk-modulex-keys/stream. If a future method ships without a route, it must be flagged here.
Behavior divergences for the same route
These are not coverage gaps, but they change how you write code against the same endpoint in each SDK.
The
402 / 410 typing difference matters for retry logic — the Python SDK gives you billing-specific exception classes to branch on, while JavaScript surfaces both as the base error. See SDK errors and retries and Errors and status codes.
Open questions
These items are unresolved against source and are documented rather than guessed:Related pages
SDKs overview
Install once, use across every operation.
JavaScript SDK
Install, configure, and the raw-response behavior.
Python SDK
Async client, env fallback, and the subscriptions resource.
Errors and retries
Error classes, retry policy, and the idempotency no-op.