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

# API overview & request lifecycle

> How the ModuleX REST API works: the api.modulex.dev base URL, the unversioned (no /v1) routing scheme, the authenticate, run, stream request lifecycle, content types, and the one operation, three ways model with a live playground.

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 the programmatic surface behind every product feature: workflows, runs, the [AI Composer](/concepts/ai-composer), the [Assistant](/concepts/assistant), [knowledge bases](/concepts/knowledge-rag), [credentials](/concepts/credentials-oauth), and [integrations](/integrations/overview). The same API powers the web app, the official [SDKs](/sdks/overview), and your own code.

This page explains the parts every request shares: the base URL, the routing scheme, the request lifecycle, content types, and how each operation is documented exactly once across cURL, Python, and JavaScript with a live playground.

<CardGroup cols={2}>
  <Card title="Authenticate" icon="key" href="/api-reference/authentication">
    Every request carries `Authorization: Bearer mx_live_…` and `X-Organization-ID`.
  </Card>

  <Card title="Pick an SDK" icon="code" href="/sdks/overview">
    Official JavaScript and Python clients wrap every operation on this page.
  </Card>

  <Card title="Handle errors" icon="triangle-alert" href="/api-reference/errors">
    Three error-envelope shapes, including the billing `DenialEnvelope`.
  </Card>

  <Card title="Stream results" icon="radio" href="/realtime/sse-streaming">
    Long-running runs stream over Server-Sent Events, not a polling loop.
  </Card>
</CardGroup>

## Base URL

All API requests go to a single host. There is no separate path prefix for the API: routers mount directly at their resource path.

```text Base URL theme={null}
https://api.modulex.dev
```

Both official SDKs default to this host, so you only set it when you point at a non-production environment:

* JavaScript: `DEFAULT_BASE_URL = 'https://api.modulex.dev'`, overridable with the `baseUrl` client option.
* Python: `DEFAULT_BASE_URL = "https://api.modulex.dev"`, overridable with the `base_url` argument or the `MODULEX_BASE_URL` environment variable.

A request URL is simply the base URL joined to the operation path. For example, running a workflow is `POST https://api.modulex.dev/workflows/run`.

<Note>
  Trailing-slash redirects are disabled (`redirect_slashes=False`). Call `/workflows`, not `/workflows/` — a trailing slash is not auto-corrected and will not match the route. The SDKs build paths correctly for you.
</Note>

For the full list of environments, hostnames, and the versioning policy, see [Base URLs & versioning](/api-reference/environments) and [Base URLs, environments & versioning](/get-started/environments).

## No `/v1` in the path

ModuleX does **not** put a version segment in the URL. There is no `/v1`, `/api`, or `/api/v1` prefix anywhere in the routing scheme — paths are exactly their resource name.

<CodeGroup>
  ```text Correct theme={null}
  POST https://api.modulex.dev/workflows/run
  GET  https://api.modulex.dev/workflows
  GET  https://api.modulex.dev/knowledge-bases
  ```

  ```text Incorrect — these routes do not exist theme={null}
  POST https://api.modulex.dev/v1/workflows/run
  GET  https://api.modulex.dev/api/v1/workflows
  ```
</CodeGroup>

If you hand-build URLs (rather than using an SDK), drop any `/v1` you may have copied from another API. A versioned path returns `404 Not Found` because the route is not registered.

Breaking changes are communicated through the [changelog](/reference/changelog) and SDK release notes rather than through a URL version. See [Base URLs & versioning](/api-reference/environments) for how ModuleX evolves the API without a path version.

## Request lifecycle

Every authenticated call follows the same path. Authentication and organization context are resolved as route dependencies — there is no separate request-id or auth middleware to account for.

<Steps>
  <Step title="You send a request">
    Set the method and resource path, attach the two required headers ([`Authorization: Bearer`](/api-reference/authentication) and [`X-Organization-ID`](/security/org-context)), and send a JSON body for write operations.
  </Step>

  <Step title="Credentials are verified">
    A `Bearer` token that starts with `mx_live_` is treated as an [API key](/security/authentication); any other Bearer token is treated as a Clerk session JWT (used by the app). A missing or invalid token returns `401`.
  </Step>

  <Step title="Organization context is applied">
    For org-scoped operations, ModuleX reads `X-Organization-ID` and checks your membership and role. A missing header returns `400`; a non-member or a role too low for the operation returns `403`. See [Roles & permissions](/security/roles-permissions).
  </Step>

  <Step title="Rate limits and the billing gate run">
    API-key traffic is rate limited per key and per user, and org traffic is rate limited per organization. Operations that consume managed usage also pass the [usage gate](/billing/usage-gating) before any work begins. Either check can stop the request with a `429`, `403`, or `402`.
  </Step>

  <Step title="The operation runs and responds">
    The handler executes and returns a JSON body. For most operations that is the final result. For runs and agent turns, the response returns immediately with run metadata and the result streams separately over [SSE](/realtime/sse-streaming).
  </Step>
</Steps>

<MediaEmbed id="MX-MEDIA-1180" type="image" caption={"Request lifecycle diagram for a ModuleX API call."} />

### Authentication recap

Authentication is the same on every operation, so it is documented in full on one page. Each request needs two headers:

| Header              | Value                                  | Required                      |
| ------------------- | -------------------------------------- | ----------------------------- |
| `Authorization`     | `Bearer mx_live_…` (your API key)      | Yes                           |
| `X-Organization-ID` | The organization UUID the call acts on | Yes for org-scoped operations |

The backend also accepts the API key in an `X-API-KEY` header as an alternative to `Authorization: Bearer`, but the SDKs and all examples in these docs use `Authorization: Bearer`. The header is `X-Organization-ID` (capital `ID`); there is no `X-Authorization` header.

For the full authentication model — how to create and scope keys, the difference between API keys and Clerk JWTs, and every auth-related status code — see [Authentication](/api-reference/authentication) and [Auth model: JWT vs API key](/security/authentication).

## Content types and conventions

<ParamField path="Content-Type" type="string" default="application/json">
  Send `application/json` for request bodies. Document upload to a knowledge base is the exception: those endpoints accept `multipart/form-data`.
</ParamField>

<ParamField path="Accept" type="string" default="application/json">
  Responses are JSON by default. Streaming endpoints return `text/event-stream` ([Server-Sent Events](/realtime/sse-streaming)) instead.
</ParamField>

<ParamField path="Field casing" type="string">
  Request and response bodies use `snake_case` on the wire (for example `workflow_id`, `created_at`, `run_id`). The Python SDK uses `snake_case` end to end. The JavaScript SDK accepts `camelCase` in method parameters and converts to `snake_case` before sending, but **leaves responses in `snake_case`** — so a JS response field is `run_id`, not `runId`.
</ParamField>

<ParamField path="Timestamps" type="string">
  Timestamps are ISO 8601 strings in UTC, for example `2026-06-21T12:00:00Z`.
</ParamField>

<ParamField path="IDs" type="string">
  Resource identifiers are strings. Note that `run_id` carries distinct meanings across layers — see [Workflows & runs](/concepts/workflows-and-runs) for the three run-id identities.
</ParamField>

List endpoints are paginated. The style (page, offset, or cursor) depends on the resource, and the SDKs provide auto-paginating helpers. See [Pagination](/api-reference/pagination) for the details.

## One operation, three ways, plus a playground

Each operation is documented **once**. Instead of separate per-language pages, you get a single `CodeGroup` with three tabs — cURL, Python, JavaScript — that all perform the same call, plus an interactive playground for the underlying endpoint.

<CardGroup cols={3}>
  <Card title="cURL" icon="terminal">
    The raw HTTP request: exact method, path, headers, and JSON body.
  </Card>

  <Card title="Python" icon="python" href="/sdks/python">
    The async `modulex-python` client (v1.0.0).
  </Card>

  <Card title="JavaScript" icon="square-js" href="/sdks/javascript">
    The `modulex-js` client (v1.0.0).
  </Card>
</CardGroup>

The cURL tab is the ground truth: it shows the literal wire call, so it works in any language. The Python and JavaScript tabs show the official SDK method that wraps that same call. Where an operation has no SDK method (a parity gap), the docs say so and show cURL only — see the [SDK ⇄ API parity matrix](/sdks/parity) for the complete coverage map, including the `subscriptions` resource that exists only in the Python SDK.

### Example: run a workflow

This is what the three-way pattern looks like. Running a deployed workflow is a single `POST /workflows/run`. The synchronous response returns immediately with run metadata while the result streams over [SSE](/realtime/sse-streaming).

<CodeGroup>
  ```bash cURL theme={null}
  curl https://api.modulex.dev/workflows/run \
    -H "Authorization: Bearer mx_live_8s2Kd0pQ4rTv7Xw" \
    -H "X-Organization-ID: 5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f" \
    -H "Content-Type: application/json" \
    -d '{
      "workflow_id": "wf_3b1c9a2e",
      "input": { "topic": "Q3 launch summary" },
      "stream": true
    }'
  ```

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

  async def main():
      async with Modulex(
          api_key="mx_live_8s2Kd0pQ4rTv7Xw",
          organization_id="5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f",
      ) as client:
          run = await client.executions.run(
              workflow_id="wf_3b1c9a2e",
              input={"topic": "Q3 launch summary"},
              stream=True,
          )
          print(run.run_id, run.status)

  asyncio.run(main())
  ```

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

  const client = new Modulex({
    apiKey: 'mx_live_8s2Kd0pQ4rTv7Xw',
    organizationId: '5d9c2e7a-1f4b-4a6c-9e3d-0b8a1c2d3e4f',
  });

  const run = await client.executions.run({
    workflowId: 'wf_3b1c9a2e',
    input: { topic: 'Q3 launch summary' },
    stream: true,
  });

  // Responses stay snake_case even in the JS SDK.
  console.log(run.run_id, run.status);
  ```
</CodeGroup>

<ResponseField name="run_id" type="string">
  The per-execution identifier. Use it to stream events and to look up run status. A new `run_id` is minted on each run and on each resume of an interrupted run.
</ResponseField>

<ResponseField name="status" type="string">
  The status of the synchronous portion. For a streaming run this is `"running"`; terminal statuses (`completed`, `failed`, `cancelled`, `interrupted`) are observed over the [SSE stream](/realtime/sse-streaming) or the run-history endpoints, not here.
</ResponseField>

<Expandable title="Choosing what to run">
  The run body accepts exactly one workflow source:

  <ParamField body="workflow_id" type="string">
    Run a previously saved workflow. Requires an active [deployment](/workflow-builder/execution/deploy); otherwise the call returns `400`. Mutually exclusive with `workflow`.
  </ParamField>

  <ParamField body="workflow" type="object">
    An inline workflow definition to run without saving. Mutually exclusive with `workflow_id`. Pair with `attribution_workflow_id` to attribute the ad-hoc run to a saved workflow's run history.
  </ParamField>

  <ParamField body="input" type="object">
    State input values passed to the workflow's entry node.
  </ParamField>

  <ParamField body="config" type="object">
    Runtime overrides for this execution. Recognized keys include `thread_id`, `recursion_limit`, and `batch_interval_ms`.
  </ParamField>

  <ParamField body="stream" type="boolean" default="true">
    Whether to open an SSE stream for real-time events.
  </ParamField>

  <ParamField body="ephemeral" type="boolean" default="false">
    If `true`, the run is not persisted and no thread is created.
  </ParamField>

  <ParamField body="is_private" type="boolean" default="false">
    If `true`, the run and its messages are visible only to the creator.
  </ParamField>

  The legacy direct-LLM mode of this endpoint was removed. A request that only supplies LLM config now returns `410 Gone` — use [the Assistant](/assistant/overview) (`POST /assistant/chat`) instead.
</Expandable>

For the complete end-to-end walkthrough — authenticate, run, and consume the stream in all three languages — see [Run a workflow (REST + SDK)](/guides/run-a-workflow). To make your very first call, start with [Make your first API call](/get-started/first-api-call).

### Live playground

Endpoint reference pages include a live "Try it" playground generated from the ModuleX OpenAPI specification. Enter your API key and organization ID once, fill in parameters, and send a real request from the browser to see the actual response. The playground exercises the same routes documented above — for example `POST /workflows/run`, `GET /workflows`, and `POST /knowledge-bases/{knowledge_id}/search`.

<MediaEmbed id="MX-MEDIA-1181" type="screenshot" caption={"The interactive API playground for an endpoint, mid-request."} />

## Errors

Operations return standard HTTP status codes. ModuleX has **three** error-envelope shapes, and you should branch on all of them. The shape depends on the surface that produced the error.

<ResponseField name="HTTPException — {detail: string}" type="object">
  The common FastAPI shape, used by plain CRUD and org-settings routes. The `detail` is a human-readable string, for example `{ "detail": "X-Organization-ID header is required" }`.
</ResponseField>

<ResponseField name="Structured detail — {detail: object}" type="object">
  A dict-valued `detail`, used by the organization rate-limit deny (`code: "rate_limited"`) and the wallet paid-gate `402`.
</ResponseField>

<ResponseField name="DenialEnvelope — flat object" type="object">
  The flat billing/credit/rate/quota envelope returned by the usage gate, with no `detail` wrapper: `{ code, layer, key, current, limit, reason }`.
</ResponseField>

The billing usage gate is **live**. Operations that consume managed usage — running workflows, the [Composer](/concepts/ai-composer), the [Assistant](/concepts/assistant), and [managed knowledge](/platform/knowledge/managed) — pass through the gate before any work begins. When you exceed a limit, the gate returns a flat `DenialEnvelope` whose `layer` field maps to the status code:

| `layer`  | HTTP status | Meaning                                                                      |
| -------- | ----------- | ---------------------------------------------------------------------------- |
| `rate`   | `429`       | Rate limit exceeded (also carries `X-RateLimit-*` and `Retry-After` headers) |
| `quota`  | `403`       | A plan quota was exceeded                                                    |
| `credit` | `402`       | Plan credit allowance exhausted, or an upgrade payment failed                |
| `wallet` | `402`       | Overage is disabled, or the prepaid wallet has insufficient balance          |

These billing envelopes appear only on the managed-usage surfaces above. Plain CRUD and org-settings routes return the `{detail}` `HTTPException` shape instead. For the complete status-code taxonomy, every error `code`, and which surface emits each shape, see [Errors & status codes](/api-reference/errors), [Rate limiting](/api-reference/rate-limiting), and [Usage gating & limits](/billing/usage-gating).

## Where to go next

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/api-reference/authentication">
    Create keys, scope them to an organization, and authenticate every call.
  </Card>

  <Card title="SDKs overview" icon="boxes-stacked" href="/sdks/overview">
    Install the JavaScript or Python client and use every operation from code.
  </Card>

  <Card title="Run a workflow" icon="play" href="/guides/run-a-workflow">
    A full REST and SDK walkthrough, from key to streamed result.
  </Card>

  <Card title="Errors & status codes" icon="triangle-alert" href="/api-reference/errors">
    The three envelope shapes and the billing `DenialEnvelope` in detail.
  </Card>
</CardGroup>
