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

# Assistant permissions, rate limits & credit cost

> Exactly who can use the ModuleX Assistant (owner or admin only — the member role is retired), the credit cost per turn, the per-plan rate limits, and the 402/403/429 denial envelopes the Assistant returns when a limit is hit.

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

This page is the technical reference for the access rules and the limits that bound the [Assistant](/concepts/assistant). It covers who is allowed to call the Assistant, the credit cost charged for each turn, the per-plan rate limits, and every denial response you can receive. For what happens inside a turn, see [the Assistant's agentic loop](/assistant/how-it-works); for streaming the result, see [streaming responses](/assistant/streaming).

Two distinct controls govern every Assistant request, and they are checked in order on every turn:

<CardGroup cols={2}>
  <Card title="Permission" icon="shield-check" href="#who-can-use-the-assistant">
    A role check. The caller must be an `owner` or `admin` in the target organization. A request from any other role is rejected before the turn starts.
  </Card>

  <Card title="Limits" icon="gauge" href="#limits-overview">
    A usage check. The turn must pass the credit and rate gate for the organization's plan. A turn that exceeds a limit is denied before any work is billed.
  </Card>
</CardGroup>

## Who can use the Assistant

Every Assistant endpoint depends on `organization_admin_required`. To call the Assistant you must satisfy all three of the following:

<Steps>
  <Step title="Be authenticated">
    Send a valid bearer token in the `Authorization` header — either a Clerk session JWT (the app) or an `mx_live_*` API key (programmatic). API keys may also be sent in the `X-API-KEY` header. See [authentication](/api-reference/authentication).
  </Step>

  <Step title="Name the organization">
    Send the `X-Organization-ID` header to scope the request to one organization. Without it the request cannot be resolved to an org and is rejected. See [org context](/security/org-context).
  </Step>

  <Step title="Hold the owner or admin role in that organization">
    Your role in the named organization must be `owner` or `admin`. Any other role returns **403**.
  </Step>
</Steps>

### Roles that can and cannot call the Assistant

The live ModuleX organization role model has exactly two roles. The `member` role is **retired** (removed as a first-class role on 2026-06-20) and cannot be used to access the Assistant.

| Role     | Can use the Assistant | Notes                                                                                                                                |
| -------- | :-------------------: | ------------------------------------------------------------------------------------------------------------------------------------ |
| `owner`  |          Yes          | Assignable only at organization creation.                                                                                            |
| `admin`  |          Yes          | The default role for invited users; the role new invitations grant.                                                                  |
| `member` |           No          | **Retired.** Not a current role. Old rows or legacy filters may still surface the string, but the Assistant rejects it with **403**. |

<Warning>
  The `member` role is retired. The Assistant, the [AI Composer](/concepts/ai-composer), and managed [knowledge](/concepts/knowledge-rag) operations all require `owner` or `admin`. An older comment in the source described the Assistant as available to "every org member" — that comment is inaccurate. As built today, a caller without the `owner` or `admin` role cannot use the Assistant. For the full role model see [roles and permissions](/security/roles-permissions) and [organizations, roles and membership](/concepts/organizations-roles).
</Warning>

### Permission denial response

A caller who is authenticated but lacks the `owner` or `admin` role receives a standard FastAPI error envelope — the `{detail}` shape, not the billing `DenialEnvelope`:

```json 403 — role not sufficient theme={null}
{
  "detail": "Admin or owner role required in organization <organization_id>"
}
```

<Note>
  A **403** here means the role check failed and uses the `{detail}` envelope. A **403** from the usage gate (a quota denial) is a different shape — the flat `DenialEnvelope`. The two are distinguished by their body: a role failure has a top-level `detail` string; a quota denial has top-level `code` and `layer` fields. See [errors and status codes](/api-reference/errors).
</Note>

### Per-endpoint authorization

Every Assistant endpoint enforces the same role gate plus a resource-ownership check. Continuing, listening to, resuming, or deleting a chat that belongs to a different organization returns **404** (ownership is hidden as not-found rather than revealed as 403).

<ResponseField name="Authorization model" type="applies to all endpoints">
  <Expandable title="Endpoints and their checks">
    <ResponseField name="POST /assistant/chat" type="owner/admin">
      Start or continue a turn. Adds concurrency guards: a chat with a pending human-in-the-loop question returns **409**, and a chat with a run already in flight returns **409**.
    </ResponseField>

    <ResponseField name="Fetch a chat" type="owner/admin">
      `GET /assistant/chat/{chat_id}` — fetch a chat and its messages. **404** if the chat is missing or owned by another organization.
    </ResponseField>

    <ResponseField name="Open the SSE stream" type="owner/admin">
      `GET /assistant/chat/{chat_id}/listen/{run_id}` — bind the `run_id` to this chat and stream events. A mismatched or cross-tenant `run_id` returns **404** ("Run not found").
    </ResponseField>

    <ResponseField name="Resume a turn" type="owner/admin">
      `POST /assistant/chat/{chat_id}/resume` — answer a pending question. Additionally, only the user who triggered the question may answer it — anyone else gets **403**.
    </ResponseField>

    <ResponseField name="List your chats" type="owner/admin, user-scoped">
      `GET /assistant/chats` — list your own Assistant chats. Filtered to the caller (`creator_id`), so it returns only chats you created, not every chat in the org.
    </ResponseField>

    <ResponseField name="Read run status" type="owner/admin">
      `GET /assistant/chat/{chat_id}/status` — read run status. **404** if missing.
    </ResponseField>

    <ResponseField name="Cancel a run" type="owner/admin">
      `POST /assistant/chat/{chat_id}/cancel` — cancel a running execution. **400** if there is no active execution.
    </ResponseField>

    <ResponseField name="Delete a chat" type="owner/admin">
      `DELETE /assistant/chat/{chat_id}` — soft-delete (or hard-delete with `permanent=true`). **404** if missing.
    </ResponseField>
  </Expandable>
</ResponseField>

<MediaEmbed id="MX-MEDIA-3280" type="screenshot" caption={"The organization members settings page showing the owner/admin role assignment that grants Assistant access."} />

## Limits overview

Once the role check passes, every turn runs through the billing admission gate **before any database row is written or any model call is made**. This gate is live on the Assistant surface (alongside the [AI Composer](/concepts/ai-composer), [workflow runs](/workflow-builder/execution/api-endpoint), and managed [knowledge](/platform/knowledge/managed)). It enforces three things in one synchronous step:

<CardGroup cols={3}>
  <Card title="Credit balance" icon="coins">
    The organization must have credit allowance or wallet funds for the turn's run charge.
  </Card>

  <Card title="Rate" icon="timer">
    The turn must fit within the plan's `sync_exec` runs-per-minute limit.
  </Card>

  <Card title="Quota" icon="lock">
    The organization must not be suspended and must be within plan quotas.
  </Card>
</CardGroup>

A turn that fails any check is denied with a `402`, `403`, or `429` carrying the flat `DenialEnvelope`. Because the gate runs before any write, a denied turn creates no chat message, no run, and no charge.

## Credit cost per turn

The Assistant charges **exactly one run credit per turn**. A turn is one user message and the agent run it triggers — regardless of how many model calls or tool calls the agent makes inside that turn.

<ResponseField name="Run charge" type="1 credit per turn">
  The flat per-turn charge, written once as a `run` usage record.

  <Expandable title="How the charge works">
    <ResponseField name="amount" type="1 credit">
      `RUN_CREDIT = 1`. One credit equals `$0.01` (100 credits = `$1.00`). The charge is the same for every turn no matter how long the agentic loop runs.
    </ResponseField>

    <ResponseField name="when" type="once per turn, on success">
      Charged after the admission gate admits the turn, keyed by the turn key `{chat.id}:{run_id}`. The charge is idempotent on that key, so it cannot be double-counted.
    </ResponseField>

    <ResponseField name="resume" type="not re-charged">
      Answering a [human-in-the-loop](/assistant/human-in-the-loop) question re-enters the same turn through `POST /assistant/chat/{chat_id}/resume`, which mints a new `run_id` but does **not** levy a second run credit — the resume path is marked as already-charged.
    </ResponseField>
  </Expandable>
</ResponseField>

Two other charges can accrue during a turn, billed separately from the flat run credit. These are the cost of the work the agent actually does:

<ResponseField name="Token usage (managed models)" type="metered separately">
  When the turn uses a ModuleX-managed model, language-model tokens are metered on input and output token counts and recorded in the executor's `finally` block — billed regardless of whether the turn succeeds, fails, or is cancelled. Bring-your-own-key model usage is **not** credited; it is analytics-only and billed by your upstream provider. See [credits and metering](/billing/credits) and [model selection](/assistant/models-and-settings).
</ResponseField>

<ResponseField name="Tool and retrieval charges" type="metered separately">
  Each managed [integration tool](/assistant/using-tools) the Assistant executes carries a flat tool base charge (`TOOL_BASE = 1` credit, anchored to `$0.01`, multiplied by any pool multiplier). Each managed [knowledge](/concepts/knowledge-rag) search carries a retrieval charge (`RETRIEVAL_BASE = 1` credit). These accrue on top of the per-turn run credit.
</ResponseField>

<Note>
  The single run credit is the Assistant's flat per-turn fee. Token, tool, and retrieval charges are the variable cost of the actions inside the turn and are metered independently. For the full credit lifecycle (reserve, charge, settle) and the credit-to-dollar conversion, see [credits and metering](/billing/credits).
</Note>

## Rate limits

Every Assistant turn counts against the organization plan's `sync_exec` rate class — the synchronous-execution runs-per-minute limit. Assistant turns count under `sync_exec` unconditionally. When the rate is exceeded the gate releases the credit reservation and returns **429**.

### Per-plan limits

The values below come from the authoritative plan configuration. The `sync_exec` limit is the one that bounds the Assistant. Where a value is `null`, that limit is unlimited (the check is skipped).

| Plan       | `sync_exec` (Assistant turns / min) | `api` (requests / min) | Monthly credit allowance | Knowledge bases |
| ---------- | :---------------------------------: | :--------------------: | :----------------------: | :-------------: |
| Free       |                  10                 |           10           |      300 (one-time)      |        10       |
| Pro        |                 150                 |           100          |       5,000 / month      |        3        |
| Max        |                 500                 |           500          |      20,000 / month      |        50       |
| Enterprise |              unlimited              |        unlimited       |          custom          |    unlimited    |

<Note>
  The `sync_exec` limit governs Assistant turns (and other synchronous executions). The separate `api` limit governs raw request throughput per key or user. A turn can be denied by either: too many turns per minute returns a `sync_exec` rate denial, while too many HTTP requests per minute returns an API rate denial. For the canonical, all-surface rate-limit reference see [rate limiting](/api-reference/rate-limiting) and the plan matrix in [usage gating and limits](/billing/usage-gating).
</Note>

<Warning>
  The credit allowances and rate limits above follow the runtime plan configuration, which is the authoritative source for limits. A stale schema docstring in the codebase lists different numbers (for example Pro 10,000 / Max 50,000 credits); those are out of date. Use the values in the table. The marketing site and the runtime config agree on these credit and rate numbers; they disagree only on the **annual price** of paid plans — see [plans and pricing](/billing/plans) for both figures.
</Warning>

## Denial responses

When a limit is hit, the turn is rejected with the flat `DenialEnvelope` — a different shape from the role-failure `{detail}` envelope above. The envelope's `layer` field determines the HTTP status.

<ResponseField name="DenialEnvelope" type="object">
  The billing/usage denial body. Flat — no `detail` wrapper.

  <Expandable title="Fields">
    <ResponseField name="code" type="string">
      The machine-readable denial code, for example `credit_plan_exhausted`, `rate_limit_exceeded`, or `quota_exceeded`.
    </ResponseField>

    <ResponseField name="layer" type="string">
      The gate layer that denied the turn: `credit`, `wallet`, `quota`, or `rate`. This maps to the HTTP status.
    </ResponseField>

    <ResponseField name="key" type="string | null">
      The bucket or rate-class key the denial applies to (for example `sync_exec` for a rate denial, or the organization id for a credit denial). May be `null`.
    </ResponseField>

    <ResponseField name="current" type="number | null">
      The current measured value at denial time (for example the current request count for a rate limit). May be `null`.
    </ResponseField>

    <ResponseField name="limit" type="number | null">
      The limit that was exceeded (for example the plan's `sync_exec` value, or the monthly credit allowance). May be `null`.
    </ResponseField>

    <ResponseField name="reason" type="string">
      A short reason string, usually equal to or paired with `code`.
    </ResponseField>
  </Expandable>
</ResponseField>

The status code depends on which layer denied the turn:

| Layer    | HTTP status | Code                                              | When it happens                                                                                                                                                                                                                                                                                                                                               |
| -------- | :---------: | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rate`   |   **429**   | `rate_limit_exceeded`                             | The plan's `sync_exec` runs-per-minute limit was exceeded. Includes `Retry-After`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers.                                                                                                                                                                                             |
| `quota`  |   **403**   | `quota_exceeded`                                  | The 403/quota denial shape. This layer is defined but is not actively emitted on the Assistant surface — no Assistant code path raises it today. Documented here so clients can recognize the `quota` envelope shape if it ever appears.                                                                                                                      |
| `credit` |   **402**   | `credit_plan_exhausted`, `upgrade_payment_failed` | The monthly plan credit allowance is used up and no wallet overage applies, or the organization is suspended after dunning (suspension raises `credit_exhausted` → `credit_plan_exhausted`). `upgrade_payment_failed` is a `credit`-layer **402** raised only from the plan-upgrade endpoint when a proration charge fails — not from the Assistant run gate. |
| `wallet` |   **402**   | `wallet_overage_disabled`, `wallet_insufficient`  | Plan credits are exhausted and the wallet cannot cover the turn (overage off, or balance too low).                                                                                                                                                                                                                                                            |

```json 402 — credit allowance exhausted theme={null}
{
  "code": "credit_plan_exhausted",
  "layer": "credit",
  "key": "<organization_id>",
  "current": null,
  "limit": 5000.0,
  "reason": "credit_plan_exhausted"
}
```

```json 429 — sync_exec rate exceeded (with Retry-After and X-RateLimit-* headers) theme={null}
{
  "code": "rate_limit_exceeded",
  "layer": "rate",
  "key": "sync_exec",
  "current": 150,
  "limit": 150,
  "reason": "rate_limit_exceeded"
}
```

<Warning>
  The Assistant endpoints can return **both** envelope shapes. A validation or authorization failure (a missing field, a wrong role, a not-found chat) returns `{detail: ...}`. A credit, wallet, quota, or rate denial returns the flat `DenialEnvelope`. A client must branch on the response body to tell a role-based **403** apart from a quota-based **403**. The full taxonomy of all three error-envelope shapes lives on [errors and status codes](/api-reference/errors).
</Warning>

## Other limits on a turn

Beyond credit and rate gating, a few structural limits bound an Assistant turn:

<ResponseField name="One pending question per chat" type="409 while pending">
  A chat may hold only one pending [human-in-the-loop](/assistant/human-in-the-loop) question at a time. Starting a new turn while a question is pending returns **409** ("This chat has a pending question; answer it first").
</ResponseField>

<ResponseField name="One run in flight per chat" type="409 while running">
  A chat may have only one run in flight. Starting a new turn while a run is in progress returns **409** ("A run is already in progress on this chat; wait for it to finish or cancel it"). Cancel the in-flight run with `POST /assistant/chat/{chat_id}/cancel`.
</ResponseField>

<ResponseField name="Text-only input" type="400 on non-string">
  The `message` field must be a JSON string. An array or object returns **400** ("Message must be a string"). Multimodal input is not supported on this surface.
</ResponseField>

<ResponseField name="Pending-question lifetime" type="7 days">
  A pending HITL question is held for 7 days. After that the pending sentinel expires; see [human-in-the-loop](/assistant/human-in-the-loop) for resume and cancel semantics.
</ResponseField>

<ResponseField name="Stream history window" type="1 hour">
  A run's SSE history is retained for 1 hour, so you can reconnect and replay events within that window. After it expires only live events on an active run are available. See [streaming responses](/assistant/streaming).
</ResponseField>

## Worked example — handling a denied turn

This example starts an Assistant turn and shows how to detect and react to each denial class. Every request authenticates with `Authorization: Bearer mx_live_…` plus `X-Organization-ID` (see [authentication](/api-reference/authentication)). The caller must hold the `owner` or `admin` role.

<CodeGroup>
  ```bash cURL theme={null}
  # Start a turn. Any of these statuses can come back:
  #   200 success | 403 {detail} role failure | 402/403/429 DenialEnvelope | 409 busy
  curl -i -X POST https://api.modulex.dev/assistant/chat \
    -H "Authorization: Bearer mx_live_xxxxxxxxxxxxxxxxxxxx" \
    -H "X-Organization-ID: org_3f9a12c4d5e6" \
    -H "Content-Type: application/json" \
    -d '{
      "message": "Summarize my three most recent GitHub issues",
      "llm": {"integration_name":"openai","provider_id":"openai","model_id":"gpt-4o"}
    }'

  # On 429 the response carries Retry-After and X-RateLimit-* headers:
  #   HTTP/1.1 429 Too Many Requests
  #   Retry-After: 60
  #   X-RateLimit-Limit: 150
  #   X-RateLimit-Remaining: 0
  #   {"code":"rate_limit_exceeded","layer":"rate","key":"sync_exec",...}
  ```

  ```python Python theme={null}
  import httpx

  BASE = "https://api.modulex.dev"
  HEADERS = {
      "Authorization": "Bearer mx_live_xxxxxxxxxxxxxxxxxxxx",
      "X-Organization-ID": "org_3f9a12c4d5e6",
      "Content-Type": "application/json",
  }

  resp = httpx.post(
      f"{BASE}/assistant/chat",
      headers=HEADERS,
      json={
          "message": "Summarize my three most recent GitHub issues",
          "llm": {"integration_name": "openai", "provider_id": "openai", "model_id": "gpt-4o"},
      },
  )

  if resp.status_code == 200:
      run = resp.json()
      print("turn started:", run["run_id"], "stream:", run["stream_url"])
  elif resp.status_code in (402, 403, 429):
      body = resp.json()
      if "layer" in body:  # flat DenialEnvelope -> a usage/billing limit
          layer = body["layer"]
          if layer == "rate":
              wait = int(resp.headers.get("Retry-After", "60"))
              print(f"rate limited on {body['key']}; retry after {wait}s")
          elif layer in ("credit", "wallet"):
              # credit-layer 402 also covers org suspension after dunning
              print(f"out of credit ({body['code']}); top up the wallet or upgrade")
          elif layer == "quota":
              print(f"quota denial ({body['code']})")
      else:  # {"detail": ...} -> role / validation failure
          print("not permitted:", body["detail"])  # e.g. owner/admin required
  elif resp.status_code == 409:
      print("chat is busy: a run or question is already pending")
  ```

  ```javascript JavaScript theme={null}
  const BASE = "https://api.modulex.dev";
  const headers = {
    Authorization: "Bearer mx_live_xxxxxxxxxxxxxxxxxxxx",
    "X-Organization-ID": "org_3f9a12c4d5e6",
    "Content-Type": "application/json",
  };

  const resp = await fetch(`${BASE}/assistant/chat`, {
    method: "POST",
    headers,
    body: JSON.stringify({
      message: "Summarize my three most recent GitHub issues",
      llm: { integration_name: "openai", provider_id: "openai", model_id: "gpt-4o" },
    }),
  });

  const body = await resp.json();

  if (resp.status === 200) {
    console.log("turn started:", body.run_id, "stream:", body.stream_url);
  } else if ([402, 403, 429].includes(resp.status)) {
    if ("layer" in body) {
      // flat DenialEnvelope -> a usage/billing limit
      if (body.layer === "rate") {
        const wait = Number(resp.headers.get("Retry-After") ?? "60");
        console.log(`rate limited on ${body.key}; retry after ${wait}s`);
      } else if (body.layer === "credit" || body.layer === "wallet") {
        // credit-layer 402 also covers org suspension after dunning
        console.log(`out of credit (${body.code}); top up the wallet or upgrade`);
      } else if (body.layer === "quota") {
        console.log(`quota denial (${body.code})`);
      }
    } else {
      // {"detail": ...} -> role / validation failure
      console.log("not permitted:", body.detail); // e.g. owner/admin required
    }
  } else if (resp.status === 409) {
    console.log("chat is busy: a run or question is already pending");
  }
  ```
</CodeGroup>

<Note>
  The branch on `"layer" in body` is the reliable way to tell a usage denial (flat `DenialEnvelope`) from a role or validation failure (`{detail}`). Retry a **429** after the `Retry-After` interval; do not retry a **402** or a role **403** — those require a credit top-up, a plan upgrade, or a role change rather than a retry.
</Note>

## Related pages

<CardGroup cols={2}>
  <Card title="Roles and permissions" icon="users" href="/security/roles-permissions">
    The full organization role model and which actions require owner or admin.
  </Card>

  <Card title="Usage gating and limits" icon="gauge" href="/billing/usage-gating">
    The billing admission gate and its 402/403/429 denial responses across all surfaces.
  </Card>

  <Card title="Credits and metering" icon="coins" href="/billing/credits">
    What a credit is, the conversion to dollars, and exactly what consumes credits.
  </Card>

  <Card title="The Assistant's agentic loop" icon="repeat" href="/assistant/how-it-works">
    What happens inside a single turn, from start to termination.
  </Card>
</CardGroup>
