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

# Base URLs, environments & versioning

> The ModuleX API base URL (https://api.modulex.dev), how to point the SDKs at local or staging environments, why there is no /v1 path segment, and how the API and SDKs are versioned.

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 REST API is served from a single production base URL. There is **no `/v1` (or `/api/v1`) path segment** — routers mount directly at their resource path, so a request URL is just the base URL joined to the route (for example `https://api.modulex.dev/workflows`). This page covers the base URL, how to point each surface at a different environment, the trailing-slash rule, and how the API and the SDKs are versioned.

For how to authenticate those requests, see [Authentication](/api-reference/authentication). For the request lifecycle and content types, see the [API overview](/api-reference/overview).

## Base URL

| Environment          | Base URL                  | Notes                                               |
| -------------------- | ------------------------- | --------------------------------------------------- |
| Production (default) | `https://api.modulex.dev` | The base URL every SDK uses unless you override it. |
| Local development    | `http://localhost:8000`   | The backend's documented local serving port.        |

Both the [JavaScript SDK](/sdks/javascript) and the [Python SDK](/sdks/python) default to `https://api.modulex.dev`, so you do not set the base URL for normal use. You only override it to target a local backend or a self-hosted deployment.

<Note>
  The production OAuth redirect target — where a browser is sent back after connecting a credential — is the ModuleX **app** at `https://app.modulex.dev`, not the API host. The API host `https://api.modulex.dev` only serves REST endpoints and the realtime streams. See [Credentials & OAuth2](/concepts/credentials-oauth).
</Note>

### How a request URL is built

The SDKs build every request URL as `baseUrl + path`, with no version segment inserted. A trailing slash on the base URL is stripped before the path is joined, so `https://api.modulex.dev/` and `https://api.modulex.dev` behave identically.

```text Request URL composition theme={null}
base URL          https://api.modulex.dev
+ route path      /workflows/run
= request URL     https://api.modulex.dev/workflows/run
```

## No `/v1` path segment

ModuleX does not put a version number in the URL path. Every resource router mounts directly at its prefix:

| Resource        | Full URL                                                                                     |
| --------------- | -------------------------------------------------------------------------------------------- |
| Workflows       | `https://api.modulex.dev/workflows`                                                          |
| Run a workflow  | `POST https://api.modulex.dev/workflows/run` (workflow id in the JSON body as `workflow_id`) |
| Knowledge bases | `https://api.modulex.dev/knowledge-bases`                                                    |
| Credentials     | `https://api.modulex.dev/credentials`                                                        |
| Integrations    | `https://api.modulex.dev/integrations`                                                       |
| Organizations   | `https://api.modulex.dev/organizations`                                                      |
| API keys        | `https://api.modulex.dev/api-keys`                                                           |

<Warning>
  Do not prefix routes with `/v1` or `/api/v1`. There is no such segment — a request to `https://api.modulex.dev/v1/workflows` does not resolve to the workflows router. Use `https://api.modulex.dev/workflows`.
</Warning>

## Trailing slashes

The API has **automatic trailing-slash redirects turned off**. A route is matched exactly as declared, so request the path without a trailing slash:

| Request           | Result                                                                               |
| ----------------- | ------------------------------------------------------------------------------------ |
| `GET /workflows`  | Matches the list-workflows route.                                                    |
| `GET /workflows/` | Does **not** redirect to `/workflows`; it is treated as a different, unmatched path. |

Always call the canonical path with no trailing slash. The SDKs already do this for you.

## Switching environments

You change the environment by changing the base URL. Authentication is unchanged across environments — every request still sends `Authorization: Bearer mx_live_…` and, for organization-scoped endpoints, `X-Organization-ID`. Use a key that belongs to the environment you are calling.

<CodeGroup>
  ```bash cURL theme={null}
  # Production (default)
  curl https://api.modulex.dev/workflows \
    -H "Authorization: Bearer mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF" \
    -H "X-Organization-ID: 9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34"

  # Local backend — same call, base URL swapped
  curl http://localhost:8000/workflows \
    -H "Authorization: Bearer mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF" \
    -H "X-Organization-ID: 9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34"
  ```

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

  # Production (default) — no base_url needed
  client = Modulex(
      api_key="mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF",
      organization_id="9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34",
  )

  # Local backend — override base_url
  local = Modulex(
      api_key="mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF",
      organization_id="9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34",
      base_url="http://localhost:8000",
  )
  ```

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

  // Production (default) — no baseUrl needed
  const client = new Modulex({
    apiKey: "mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF",
    organizationId: "9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34",
  });

  // Local backend — override baseUrl
  const local = new Modulex({
    apiKey: "mx_live_8sJ2kQ4mZ1pX7vR9tB3nW6yL0aD5cF",
    organizationId: "9d1f0c2e-4b7a-4c1e-8f3a-2b6d5e0a1c34",
    baseUrl: "http://localhost:8000",
  });
  ```
</CodeGroup>

### Configuring the base URL per SDK

The two SDKs differ in whether the base URL can come from an environment variable. This matters when you deploy the same code to different environments.

<ParamField path="base_url" type="string" default="https://api.modulex.dev">
  **Python SDK.** Set it explicitly on the constructor, or set the `MODULEX_BASE_URL` environment variable, or let it fall back to the default. The resolution order is **constructor argument → `MODULEX_BASE_URL` → default**. The same fallback exists for `MODULEX_API_KEY` and `MODULEX_ORGANIZATION_ID`.
</ParamField>

<ParamField path="baseUrl" type="string" default="https://api.modulex.dev">
  **JavaScript SDK.** Set it on the constructor (`baseUrl`) or omit it for the default. The JavaScript SDK has **no environment-variable fallback** — there is no `MODULEX_BASE_URL` lookup, and likewise no env fallback for the API key or organization ID. Read any environment variables in your own code and pass the value to the constructor.
</ParamField>

```javascript JavaScript — env-driven base URL (you read the env var) theme={null}
import { Modulex } from "modulex";

// The JS SDK does not read process.env itself — pass it through explicitly.
const client = new Modulex({
  apiKey: process.env.MODULEX_API_KEY ?? "",
  organizationId: process.env.MODULEX_ORGANIZATION_ID ?? "",
  baseUrl: process.env.MODULEX_BASE_URL ?? "https://api.modulex.dev",
});
```

<MediaEmbed id="MX-MEDIA-1070" type="image" caption={"A side-by-side diagram of base-URL resolution for the Python and JavaScript SDKs."} />

## Versioning

ModuleX versions the API and the SDKs separately. Neither uses a version segment in the URL path.

### API versioning

The API is **unversioned in the URL** — there is no `/v1` path segment and no API-version request header to send. The backend application advertises an internal version string of `0.1.2` (visible at the service root `GET /` and the health endpoint `GET /system/health`), but you do not pass a version on a request, and you cannot pin to a specific API version from the client side.

Because there is no version negotiation, ModuleX evolves the API in a backward-compatible way wherever possible. Track changes that affect you through the [changelog](/reference/changelog), and use the [SDK ⇄ API parity matrix](/sdks/parity) to confirm which routes a given SDK release covers.

### SDK versioning (semantic versioning)

The official SDKs follow semantic versioning and ship at version **1.0.0**:

| SDK                     | Package   | Version | Page                               |
| ----------------------- | --------- | ------- | ---------------------------------- |
| JavaScript / TypeScript | `modulex` | `1.0.0` | [JavaScript SDK](/sdks/javascript) |
| Python                  | `modulex` | `1.0.0` | [Python SDK](/sdks/python)         |

Pin the SDK version in your dependency manifest so a major release does not change behavior under you. Breaking changes land in a new major version; consult the changelog before upgrading across a major boundary. Notable changes already shipped in the 1.0 line include the retirement of the organization `member` role (invites accept only `admin`; see [Roles & permissions](/security/roles-permissions)), realtime SSE frames discriminating on `type` rather than `event`, and the removal of client-side billing helpers from the JavaScript SDK.

<Note>
  The Python SDK sends a `User-Agent` of `modulex-python/<version>` on every request; the JavaScript SDK does not set a `User-Agent`. Neither identifies an API version, because the API is unversioned.
</Note>

## Interactive API documentation

The running backend also serves machine- and human-readable API references at the base URL:

| Path                                    | Surface                                                                                              |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `https://api.modulex.dev/docs`          | Swagger UI (interactive).                                                                            |
| `https://api.modulex.dev/redoc`         | ReDoc reference.                                                                                     |
| `https://api.modulex.dev/system/health` | Public liveness check returning `{ "status": "healthy", "service": "ModuleX", "version": "0.1.2" }`. |

For the curated, language-tabbed reference and a live "Try it" playground inside these docs, use the [API overview](/api-reference/overview) and the endpoint reference. To check live availability, see [System status & health](/reference/status).

## What does not change per environment

Switching environments changes only the base URL. The following are the same across production and a local or self-hosted backend:

<Steps>
  <Step title="Authentication scheme">
    Every request sends `Authorization: Bearer mx_live_…`, and organization-scoped requests also send `X-Organization-ID`. See [Authentication](/api-reference/authentication).
  </Step>

  <Step title="Route paths">
    The same paths with no `/v1` segment, called without a trailing slash.
  </Step>

  <Step title="Error envelopes">
    The same three error shapes, including the billing `DenialEnvelope` returned as 402 / 403 / 429 on metered surfaces. See [Errors & status codes](/api-reference/errors).
  </Step>

  <Step title="Rate limiting">
    The same per-key, per-user, and per-organization limits and `429` responses. See [Rate limiting](/api-reference/rate-limiting).
  </Step>
</Steps>

## Next steps

<CardGroup cols={2}>
  <Card title="Make your first API call" icon="terminal" href="/get-started/first-api-call">
    Authenticate and run a workflow with cURL, Python, and JavaScript.
  </Card>

  <Card title="Authentication" icon="key" href="/api-reference/authentication">
    The `Authorization: Bearer` header and `X-Organization-ID` org context.
  </Card>

  <Card title="API overview" icon="book-open" href="/api-reference/overview">
    The request lifecycle, content types, and how each operation is shown three ways.
  </Card>

  <Card title="SDK ⇄ API parity matrix" icon="table" href="/sdks/parity">
    Which REST routes each SDK release maps to, with gaps called out.
  </Card>
</CardGroup>
