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

# Usage gating & limits

> How the ModuleX usage gate admits or denies managed work before it runs: the four denial layers (credit, quota, rate, wallet), the flat DenialEnvelope returned as 402/403/429, the run/Composer/Assistant/managed-knowledge surfaces it covers, and why plain CRUD is never gated.

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>;
};

Every piece of **managed work** in ModuleX passes through an admission **usage gate** before it runs. The gate resolves your [organization's](/concepts/organizations-roles) plan entitlements, reserves the [credit](/billing/credits) the work will cost, and checks your rate and quota limits — all *before* any database row is written or any model is called. If the gate declines, the request fails with a structured denial and **no work is performed and nothing is charged**.

This page is the reference for that gate: what it checks, the exact response it returns when it declines, which surfaces it covers, and the surfaces it deliberately leaves alone.

<Note>
  **The gate fails closed.** When the gate cannot confirm you have budget — including when its backing cache is unavailable — it denies the request rather than letting it through unmetered. You are never billed for work the gate did not admit.
</Note>

## What the gate is

The gate is a synchronous **reject-before-write** check that runs at the start of every managed run or turn. It does three things in order, then either admits the work or raises a denial:

<Steps>
  <Step title="Resolve entitlements">
    The gate loads your organization's active subscription period — the plan's monthly credit allowance, rate limits, quotas, and wallet flags. A **suspended** organization (an unpaid or past-grace subscription) is denied here and does not fall back to the free tier. See [Subscriptions & Stripe](/billing/subscription-lifecycle) for how suspension happens.
  </Step>

  <Step title="Reserve credit">
    The gate hard-reserves the credit the work will cost against your current billing period (for paid plans) or your per-user free pool (on Free). The reservation is atomic, so two concurrent runs cannot both spend the last credit. If the plan allowance is exhausted, the reservation cascades to your prepaid [wallet](/billing/wallet) — but only if overage is enabled and the balance covers it.
  </Step>

  <Step title="Consume the rate class">
    For runs, the gate also consumes one unit of your plan's per-period run-rate limit. If that limit is hit, the gate releases the credit reservation it just made and denies with a rate error.
  </Step>
</Steps>

Only after all three pass does the work begin. When it finishes, the reserved credit is charged and the reservation is settled; if the work errors out early, the reservation is released so the held budget self-heals. The full lifecycle — reserve, charge, settle, release — is covered in [Credits & metering](/billing/credits).

<MediaEmbed id="MX-MEDIA-1270" type="image" caption={"A flowchart of one managed request passing through the usage gate, showing the three checks and the four possible denials."} />

## The denial response: `DenialEnvelope`

When the gate declines, it returns a single, flat JSON shape called the **`DenialEnvelope`** — the same shape for every denial layer. It is **not** wrapped in a `detail` key (unlike the standard `{"detail": "…"}` errors the rest of the API returns). Front-end and SDK code branches on the top-level `code` and `layer` fields.

```json Example: credits exhausted (402) theme={null}
{
  "code": "credit_plan_exhausted",
  "layer": "credit",
  "key": "org_2a1f9c4e7b8d",
  "current": null,
  "limit": 5000.0,
  "reason": "credit_plan_exhausted"
}
```

### Fields

<ResponseField name="code" type="string" required>
  The machine-stable denial code. Branch on this. One of `credit_plan_exhausted`, `wallet_overage_disabled`, `wallet_insufficient`, `upgrade_payment_failed`, `quota_exceeded`, or `rate_limit_exceeded`.
</ResponseField>

<ResponseField name="layer" type="string" required>
  Which gate layer denied the request — one of `credit`, `wallet`, `quota`, or `rate`. The layer fixes the HTTP status: `credit` and `wallet` return **402**, `quota` returns **403**, and `rate` returns **429**.
</ResponseField>

<ResponseField name="key" type="string | null">
  The specific limit or counter key involved — for example the organization id for a credit denial, `sync_exec` for a run-rate denial, or a quota key such as `max_knowledge_bases`. May be `null`.
</ResponseField>

<ResponseField name="current" type="number | null">
  The observed usage or count at the moment of denial. May be `null` when not applicable (for example, the credit layer often reports `null` here).
</ResponseField>

<ResponseField name="limit" type="number | null">
  The limit that was hit. May be `null` when the limit is not applicable or not exposed.
</ResponseField>

<ResponseField name="reason" type="string | null">
  A short reason token, often identical to `code` (for example `overage_disabled`, `insufficient_balance`).
</ResponseField>

<Warning>
  The `DenialEnvelope` is **flat** — its fields are at the top level of the response body, with no `detail` wrapper. A 402/403/429 that arrives wrapped as `{"detail": …}` is **not** a `DenialEnvelope`; it comes from a non-gated route (see [Plain CRUD is never gated](#plain-crud-is-never-gated)). For the complete catalog of every error-envelope shape ModuleX returns and how each SDK maps them to a typed error class, see [Errors & status codes](/api-reference/errors).
</Warning>

## The four denial layers

Each denial belongs to one of four layers. The layer determines the HTTP status, and each layer has one or more codes.

| `code`                    | `layer`  | HTTP | When it happens                                                                                                                                                                                                                                                                                       |
| ------------------------- | -------- | :--: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `credit_plan_exhausted`   | `credit` |  402 | The plan's monthly credit allowance is used up and no wallet overage is available — or the subscription is suspended.                                                                                                                                                                                 |
| `wallet_overage_disabled` | `wallet` |  402 | The allowance is exhausted and overage spending is turned off for the organization.                                                                                                                                                                                                                   |
| `wallet_insufficient`     | `wallet` |  402 | Overage is on, but the prepaid [wallet](/billing/wallet) balance is too low to cover the work.                                                                                                                                                                                                        |
| `upgrade_payment_failed`  | `credit` |  402 | An immediate plan-upgrade proration charge failed; the plan change was rolled back and you stay on the old plan.                                                                                                                                                                                      |
| `quota_exceeded`          | `quota`  |  403 | Defined but **not currently emitted by the gate** — no reserve path raises it today. A real countable-entitlement breach (for example, the maximum number of [knowledge bases](/platform/knowledge/managed)) instead returns a plain `{"detail": …}` 403 from the `POST /knowledge-bases` CRUD route. |
| `rate_limit_exceeded`     | `rate`   |  429 | The per-period run-rate limit for your plan was reached on a gated surface.                                                                                                                                                                                                                           |

<Tabs>
  <Tab title="Credit (402)">
    Returned when the plan allowance is gone and there is no wallet to fall back on, or when the subscription is suspended for non-payment. Resolve it by topping up the [wallet](/billing/wallet) (with overage enabled), upgrading your [plan](/billing/plans), or waiting for the monthly allowance to reset.

    ```json 402 credit_plan_exhausted theme={null}
    {
      "code": "credit_plan_exhausted",
      "layer": "credit",
      "key": "org_2a1f9c4e7b8d",
      "current": null,
      "limit": 5000.0,
      "reason": "credit_plan_exhausted"
    }
    ```
  </Tab>

  <Tab title="Wallet (402)">
    Returned when the plan allowance is exhausted and the cascade to the prepaid wallet cannot complete: either overage is switched off (`wallet_overage_disabled`) or the balance is too low (`wallet_insufficient`). Enable overage and top up in [Wallet & top-ups](/billing/wallet).

    ```json 402 wallet_overage_disabled theme={null}
    {
      "code": "wallet_overage_disabled",
      "layer": "wallet",
      "key": "org_2a1f9c4e7b8d",
      "current": null,
      "limit": null,
      "reason": "overage_disabled"
    }
    ```
  </Tab>

  <Tab title="Quota (403)">
    The `quota`-layer `DenialEnvelope` is **defined but not currently emitted by the usage gate** — the gate's reserve step only ever raises `credit`/`wallet` 402s and the `rate` 429, so no path produces this flat envelope today. The shape below is documented so SDK code can branch on it if the gate begins emitting it, but a real knowledge-base or storage limit breach today does **not** look like this.

    What you actually get when you hit a KB/storage ceiling is a plain `{"detail": "…"}` 403 from the CRUD route `POST /knowledge-bases` (raised as `KnowledgeBaseQuotaExceededError → HTTPException(403)`), not the flat envelope. See [Plain CRUD is never gated](#plain-crud-is-never-gated).

    ```json 403 quota DenialEnvelope (defined, not currently emitted by the gate) theme={null}
    {
      "code": "quota_exceeded",
      "layer": "quota",
      "key": "max_knowledge_bases",
      "current": 10,
      "limit": 10,
      "reason": "quota_exceeded"
    }
    ```

    ```json 403 from POST /knowledge-bases (what a real KB-limit breach returns today) theme={null}
    {
      "detail": "Knowledge base limit reached for your plan"
    }
    ```
  </Tab>

  <Tab title="Rate (429)">
    Returned when the per-period run-rate limit is reached. A rate denial also carries rate-limit headers (see below). Back off and retry after the indicated delay.

    ```json 429 rate_limit_exceeded theme={null}
    {
      "code": "rate_limit_exceeded",
      "layer": "rate",
      "key": "sync_exec",
      "current": 150,
      "limit": 150,
      "reason": "rate_limit_exceeded"
    }
    ```
  </Tab>
</Tabs>

### Rate-limit headers on a 429

A `rate`-layer denial is accompanied by the standard rate-limit headers. Each numeric header is **omitted** when its value is not known, so a client may see only some of them. `Retry-After` is always present.

<ResponseField name="Retry-After" type="integer (seconds)">
  How long to wait before retrying. Defaults to `60` when no more specific value is available.
</ResponseField>

<ResponseField name="X-RateLimit-Limit" type="integer">
  The run-rate limit for your plan's run-rate class. Omitted if not set.
</ResponseField>

<ResponseField name="X-RateLimit-Remaining" type="integer">
  Remaining units in the current window (typically `0` at the moment of denial). Omitted if not set.
</ResponseField>

<ResponseField name="X-RateLimit-Reset" type="integer (Unix epoch seconds)">
  When the window resets. Omitted if not set.
</ResponseField>

<Note>
  The gate's run-rate `rate_limit_exceeded` (a flat `DenialEnvelope`) is distinct from the **API-key/user request rate limit**, which also returns 429 but with a `{"detail": "…"}` string body and the same headers. Both are real and both can occur. For the full picture of every 429 path, see [Rate limiting](/api-reference/rate-limiting).
</Note>

## Which surfaces are gated

The usage gate runs on ModuleX's **managed-usage surfaces** — the places where ModuleX does paid work on your behalf. On these surfaces a 402/403/429 can be a flat `DenialEnvelope`.

<CardGroup cols={2}>
  <Card title="Workflow runs" icon="diagram-project" href="/workflow-builder/execution/api-endpoint">
    `POST /workflows/run` with the workflow id in the JSON body (`workflow_id`). Each run reserves one run credit and consumes the run-rate class before it executes.
  </Card>

  <Card title="AI Composer" icon="wand-magic-sparkles" href="/concepts/ai-composer">
    The [Composer](/workflow-builder/composer) chat and resume endpoints. Each turn is admitted and metered before the agent edits your workflow.
  </Card>

  <Card title="Assistant" icon="robot" href="/concepts/assistant">
    The [Assistant](/assistant/overview) chat and resume endpoints. Each turn is admitted before the agent reasons or calls a tool.
  </Card>

  <Card title="Managed knowledge" icon="book-open" href="/platform/knowledge/managed">
    Retrieval and document ingest on ModuleX-managed (`modulexdb`) knowledge bases. BYOK vector stores are not metered and not gated.
  </Card>
</CardGroup>

### Per-surface behavior

<AccordionGroup>
  <Accordion title="Workflow runs" icon="diagram-project">
    A managed run reserves one run credit (`RUN_CREDIT = 1`) and consumes the **`sync_exec`** run-rate class. If the rate class is exhausted, the credit reservation is released first, then the run is denied with `rate_limit_exceeded` (429). A credit or wallet shortfall denies with the matching 402 before the workflow starts. The run endpoint requires an owner or admin caller. See [Run via API](/workflow-builder/execution/api-endpoint).
  </Accordion>

  <Accordion title="Composer turns" icon="wand-magic-sparkles">
    Each Composer message is one turn. The gate admits the turn (reserving one credit) and consumes the run-rate class before the agent runs; resuming a paused turn re-enters through the resume endpoint and is not double-charged. Composer requires an owner or admin caller. See [AI Composer in the builder](/workflow-builder/composer).
  </Accordion>

  <Accordion title="Assistant turns" icon="robot">
    Each Assistant message is one turn, admitted and rate-checked exactly like a Composer turn, even though the Assistant has no workflow tools. The Assistant requires an owner or admin caller. Permissions and limits are detailed in [Permissions & limits](/assistant/permissions-and-limits).
  </Accordion>

  <Accordion title="Managed-knowledge retrieval and ingest" icon="book-open">
    On a ModuleX-managed knowledge base, each retrieval reserves one credit (`RETRIEVAL_BASE = 1`) and each document ingest reserves one credit (`FILE_INGEST_BASE = 1`) before the work begins, both against the `api` rate class. A bring-your-own vector store (Qdrant, Pinecone, MongoDB Atlas, Weaviate) is not managed usage, so it is neither metered nor gated. See [Managed knowledge](/platform/knowledge/managed).
  </Accordion>
</AccordionGroup>

### Plain CRUD is never gated

Everyday actions that do not run managed work are **not** gated and never return a `DenialEnvelope`. Listing, reading, creating, updating, and deleting resources, and changing organization settings, all bypass the gate entirely. A 402/403/429 from one of these routes is a standard `{"detail": …}` error, never the flat envelope.

<Warning>
  Routes such as `GET`/`POST`/`PATCH`/`DELETE /workflows` and `/workflows/{workflow_id}`, `GET`/`POST /knowledge-bases`, `/credentials`, `/api-keys`, `/schedules`, and `GET /organizations` or `GET /integrations` do not call the usage gate. If you get a 403 on one of these, it is an authorization or quota error in `{"detail": …}` form, not a billing denial. Do not parse a flat `DenialEnvelope` from a CRUD response.
</Warning>

## Handling a denial in code

Run a workflow and handle a billing denial across cURL, Python, and JavaScript. Authenticate with your API key as a bearer token plus the organization header, exactly as on every ModuleX request — see [Authentication](/api-reference/authentication).

<CodeGroup>
  ```bash cURL theme={null}
  # A denied run returns the flat DenialEnvelope (no "detail" wrapper).
  curl -sS -X POST 'https://api.modulex.dev/workflows/run' \
    -H 'Authorization: Bearer mx_live_8Kf2pQ7nR4tV9wXz' \
    -H 'X-Organization-ID: org_2a1f9c4e7b8d' \
    -H 'Content-Type: application/json' \
    -d '{
      "workflow_id": "wf_3d8b1a2c",
      "input": { "topic": "quarterly report" },
      "stream": false
    }'

  # Example 402 response body when the plan allowance is exhausted:
  # {
  #   "code": "credit_plan_exhausted",
  #   "layer": "credit",
  #   "key": "org_2a1f9c4e7b8d",
  #   "current": null,
  #   "limit": 5000.0,
  #   "reason": "credit_plan_exhausted"
  # }
  ```

  ```python Python theme={null}
  from modulex import Modulex
  from modulex import (
      CreditExhaustedError,
      WalletError,
      QuotaExceededError,
      RateLimitError,
  )

  async with Modulex(
      api_key="mx_live_8Kf2pQ7nR4tV9wXz",
      organization_id="org_2a1f9c4e7b8d",
  ) as client:
      try:
          run = await client.executions.run(
              workflow_id="wf_3d8b1a2c",
              input={"topic": "quarterly report"},
              stream=False,
          )
          print(run.run_id, run.status)
      except CreditExhaustedError as exc:        # 402, layer="credit"
          print("Out of credits:", exc.code, exc.limit)
      except WalletError as exc:                 # 402, layer="wallet"
          print("Wallet problem:", exc.code, exc.reason)
      except QuotaExceededError as exc:          # 403, layer="quota"
          print("Quota hit:", exc.key, exc.current, "/", exc.limit)
      except RateLimitError as exc:              # 429, header path
          print("Rate limited, retry after", exc.retry_after, "s")
  ```

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

  const client = new Modulex({
    apiKey: 'mx_live_8Kf2pQ7nR4tV9wXz',
    organizationId: 'org_2a1f9c4e7b8d',
  });

  try {
    const run = await client.executions.run({
      workflowId: 'wf_3d8b1a2c',
      input: { topic: 'quarterly report' },
      stream: false,
    });
    console.log(run.run_id, run.status);
  } catch (err) {
    if (err instanceof RateLimitError) {
      console.log('Rate limited, retry after', err.retryAfter, 's');
    } else if (err instanceof ModulexError && err.layer) {
      // Billing denials (402/403) carry .code, .layer, and .reason.
      console.log('Denied:', err.layer, err.code, err.reason);
    } else {
      throw err;
    }
  }
  ```
</CodeGroup>

<Note>
  The two SDKs model denials differently. The **Python** SDK has a `BillingError` family — `CreditExhaustedError`, `WalletError`, `QuotaExceededError`, and a base for `rate`-layer envelopes — so you can catch each layer by type. The **JavaScript** SDK has no dedicated billing subclasses: a 402 or a `rate`-layer envelope surfaces as the base `ModulexError` with `.code`, `.layer`, and `.reason` populated, while a header-path 429 is a `RateLimitError`. The full status-to-class mapping for both SDKs is in [Errors & status codes](/api-reference/errors) and [Errors & retries](/sdks/errors-retries).
</Note>

## What is not billed or gated

The gate only meters **managed** usage — work ModuleX runs through its own provisioned models, tools, and vector stores. The following are not gated and never cost credits:

* **Bring-your-own-key (BYOK) usage.** When you connect your own [LLM provider](/integrations/llm-providers/overview) or [knowledge provider](/integrations/knowledge-providers/overview), that usage is billed directly by the provider and recorded for analytics only — no credit is charged and the gate does not deny it.
* **Plain CRUD and org-settings routes**, as covered above.
* **Reading runs, history, and dashboards.**

For exactly what a credit is and how managed model, tool, and retrieval usage is converted into credits, see [Credits & metering](/billing/credits).

## Related pages

<CardGroup cols={2}>
  <Card title="Errors & status codes" icon="triangle-exclamation" href="/api-reference/errors">
    Every error-envelope shape, the full HTTP status taxonomy, and how the SDKs map each status to a typed error class.
  </Card>

  <Card title="Credits & metering" icon="coins" href="/billing/credits">
    What a credit is, the reserve-charge-settle lifecycle, and exactly what consumes credits.
  </Card>

  <Card title="Rate limiting" icon="gauge-high" href="/api-reference/rate-limiting">
    Per-key and per-user rate limits and all three live 429 paths.
  </Card>

  <Card title="Wallet & top-ups" icon="wallet" href="/billing/wallet">
    Enable overage, top up the prepaid wallet, and set up auto top-up.
  </Card>

  <Card title="Plans & pricing" icon="layer-group" href="/billing/plans">
    Allowances, quotas, and rate limits for Free, Pro, Max, and Enterprise.
  </Card>

  <Card title="Credits & the billing model" icon="circle-info" href="/concepts/credits-billing">
    The concept behind metering and where the gate applies.
  </Card>
</CardGroup>
