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

# Manifest & schema contract

> The complete Pydantic contract every ModuleX integration manifest must satisfy — IntegrationManifest, ActionDefinition, ParameterDef, the six auth schema variants, EnvVar, and TestEndpoint.

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 ModuleX integration declares itself through a single Pydantic contract. Your integration's `manifest.py` constructs one `IntegrationManifest` instance, and the ModuleX runtime imports it through the `modulex.tools` entry-point group to drive credential setup, validation, and tool discovery. This page is the exhaustive field reference for that contract.

The contract is defined in `schema.py` in the `modulex-integrations` package. Once you understand the manifest, read the [@tool function contract](/integrations/building/tool-contract) for the executable side, and [Build an integration](/integrations/building/overview) for the end-to-end workflow.

<Note>
  Every model in the schema sets `extra="forbid"`. Any field name that is not part of the contract — a typo, or a legacy field the new schema dropped — fails at **import time, not at runtime**. There is no silent acceptance of unknown keys.
</Note>

## How the contract is consumed

The runtime discovers integrations through the `modulex.tools` Python entry-point group. Each entry point names a `modulex_integrations.tools.<name>` package that exposes exactly two public names:

* `manifest` — one `IntegrationManifest` instance.
* `TOOLS` — a tuple of LangChain `StructuredTool` objects, one per action.

The backend reads both, then converts the manifest plus the `@tool` tuple into the action shape it serves to the model and the app. Two consequences shape the entire schema:

1. **`auth_schemas` is a discriminated union** keyed on `auth_type`. A single integration can expose more than one credential method — GitHub ships OAuth2 and a personal access token side by side.
2. **Action output schemas are intentionally absent from the manifest.** The return shape of every action is derived from its `@tool` function's return-type annotation (defined in `outputs.py`) via `Model.model_json_schema()` at startup — not declared in the manifest. See [Output models are not in the manifest](#output-models-are-not-in-the-manifest).

<MediaEmbed id="MX-MEDIA-4180" type="image" caption={"Diagram of how a manifest flows from package to runtime."} />

## `IntegrationManifest`

The top-level model your `manifest.py` produces. Every other model on this page is reachable from here.

<ParamField path="integration_type" type="Literal[&#x22;tool&#x22;]" default="tool">
  The only accepted value is `"tool"`. It distinguishes a tool integration from the LLM and knowledge providers, which live inside the ModuleX backend rather than in this package.
</ParamField>

<ParamField path="name" type="string" required>
  The integration's identifier. Must match the pattern `^[a-z][a-z0-9_]*$` — lowercase, snake\_case, starting with a letter. It must equal the entry-point key and the package directory name (for example `github`, `google_ads`).
</ParamField>

<ParamField path="display_name" type="string" required>
  The human-readable label shown in the app, for example `"GitHub"`.
</ParamField>

<ParamField path="description" type="string" required>
  A one-line description of what the integration does.
</ParamField>

<ParamField path="version" type="string" default="1.0.0">
  The per-integration manifest version. This is **not** the `modulex-integrations` package version.
</ParamField>

<ParamField path="author" type="string" default="ModuleX">
  The integration author.
</ParamField>

<ParamField path="logo" type="string | null" default="null">
  The logo reference. The convention is `"modulex:<name>-themed"`, for example `"modulex:github-themed"`.
</ParamField>

<ParamField path="app_url" type="string | null" default="null">
  The provider's homepage URL.
</ParamField>

<ParamField path="categories" type="string[]" default="[]">
  Free-form category strings used to group the integration in the catalog, for example `["Developer Tools & Infrastructure", "version-control", "productivity"]`.
</ParamField>

<ParamField path="actions" type="ActionDefinition[]" default="[]">
  The list of callable actions the integration exposes. See [`ActionDefinition`](#actiondefinition).
</ParamField>

<ParamField path="auth_schemas" type="AuthSchema[]" default="[]">
  The credential methods the integration supports, as a discriminated union on `auth_type`. See [Auth schemas](#auth-schemas-the-discriminated-union). A single integration may list more than one variant.
</ParamField>

<Warning>
  There is **no** `output_schema`, `docs_url`, `features`, `rate_limits`, `pricing`, or `metadata` field on `IntegrationManifest`. The earlier `<name>_integration.json` format carried several informational fields the Pydantic schema deliberately drops; because `extra="forbid"` rejects them, they must be removed when you migrate a manifest and kept only in the integration's README.
</Warning>

## `ActionDefinition`

One callable action exposed by the integration. The return shape is **not** declared here — it is derived from the matching `@tool` function's return annotation in `tools.py` and `outputs.py`.

<ParamField path="name" type="string" required>
  The action name. Must match the pattern `^[a-z][a-z0-9_]*$` and must equal a `@tool` function name in the package's `TOOLS` tuple.
</ParamField>

<ParamField path="description" type="string" required>
  A one-line description of what the action does.
</ParamField>

<ParamField path="parameters" type="dict[str, ParameterDef]" default="{}">
  The action's parameters, keyed by parameter name. See [`ParameterDef`](#parameterdef).
</ParamField>

### `ParameterDef`

A single action parameter.

<ParamField path="type" type="ParameterType" required>
  The parameter type. One of `"string"`, `"integer"`, `"number"`, `"boolean"`, `"array"`, or `"object"`.
</ParamField>

<ParamField path="description" type="string" required>
  A one-line description of the parameter.
</ParamField>

<ParamField path="default" type="Any" default="null">
  The default value when the parameter is omitted.
</ParamField>

<ParamField path="required" type="boolean" default="false">
  Whether the parameter must be supplied. Defaults to `false`.

  Note the contrast with [`EnvVar.required`](#envvar), which defaults to `true`.
</ParamField>

Here is a single action with two parameters:

```python actions example theme={null}
ActionDefinition(
    name="list_repositories",
    description="List repositories for the authenticated user or organization",
    parameters={
        "visibility": ParameterDef(
            type="string",
            description="Filter by visibility: all, public, private",
            default="all",
        ),
        "per_page": ParameterDef(
            type="integer",
            description="Results per page",
            default=30,
        ),
    },
)
```

<Warning>
  The manifest `parameters` are **not** the authoritative list of an action's callable arguments — the `@tool` function's `args_schema` is. Several shipped actions accept arguments (for example GitHub `create_issue` accepts `assignees`) that the function forwards to the upstream API but the manifest does not list. Treat manifest `parameters` as descriptive catalog metadata; the [@tool function contract](/integrations/building/tool-contract) defines what the action actually accepts.
</Warning>

## Auth schemas: the discriminated union

`AuthSchema` is a Pydantic discriminated union over six variant classes, keyed on the `auth_type` literal. List one or more variants on `IntegrationManifest.auth_schemas`. GitHub, for example, ships `OAuth2AuthSchema` plus `BearerTokenAuthSchema`; Exa ships `ApiKeyAuthSchema` plus `ModulexKeyAuthSchema`.

### Shared base fields

Every variant inherits these fields from `_AuthSchemaBase`:

<ParamField path="display_name" type="string" required>
  The credential method's label in the app, for example `"OAuth2 Authentication"` or `"Personal Access Token"`.
</ParamField>

<ParamField path="description" type="string" required>
  A one-line description of the credential method.
</ParamField>

<ParamField path="setup_instructions" type="string[] | null" default="null">
  An ordered list of human setup steps shown in the credential UI.
</ParamField>

<ParamField path="setup_environment_variables" type="EnvVar[]" default="[]">
  The secrets and settings the operator must provide for this method. See [`EnvVar`](#envvar).
</ParamField>

<ParamField path="test_endpoint" type="TestEndpoint | null" default="null">
  The HTTP call the runtime makes to validate a configured credential. **Optional** — some `modulex_key` "public API" integrations (for example `instacart`, `hackernews`) have no credential to validate and ship no test endpoint; the runtime skips credential testing in that case. See [`TestEndpoint`](#testendpoint).
</ParamField>

### The six variants

Each variant adds only its `auth_type` discriminator. `OAuth2AuthSchema` additionally requires an `oauth_config`.

| Class                   | `auth_type`      | Extra field                 | Used by (verified samples)                                             |
| ----------------------- | ---------------- | --------------------------- | ---------------------------------------------------------------------- |
| `OAuth2AuthSchema`      | `"oauth2"`       | `oauth_config: OAuthConfig` | `github`, `google_calendar`, `netlify`                                 |
| `BearerTokenAuthSchema` | `"bearer_token"` | —                           | `github` (personal access token)                                       |
| `ApiKeyAuthSchema`      | `"api_key"`      | —                           | `exa`, `convertapi`, `hunter`, `freshdesk`                             |
| `ModulexKeyAuthSchema`  | `"modulex_key"`  | —                           | `exa` (managed key), `hackernews`, `instacart`, `firecrawl`, `jina_ai` |
| `CustomAuthSchema`      | `"custom"`       | —                           | `postgresql`, `woocommerce`, `coinbase`, `mintlify`                    |
| `InternalAuthSchema`    | `"internal"`     | —                           | None in the catalog — reserved variant                                 |

<Warning>
  `InternalAuthSchema` is **defined but unused.** No shipped integration instantiates it. It remains a valid wire `auth_type` on the ModuleX side (the backend credential constraint accepts `'internal'`), so treat it as a reserved or forward-looking variant rather than an option to author against today.

  Separately, the backend credential constraint accepts a **seventh** value, the legacy `'bearer'`, which has no corresponding schema variant. New integrations must emit only the six variants above; `'bearer'` exists only as a legacy database value.
</Warning>

### `OAuthConfig`

Required on `OAuth2AuthSchema` only. Carries the OAuth 2.0 authorize and token endpoints plus per-provider flags.

<ParamField path="auth_url" type="string" required>
  The provider's authorization URL.
</ParamField>

<ParamField path="token_url" type="string" required>
  The provider's token-exchange URL.
</ParamField>

<ParamField path="scopes" type="string[]" default="[]">
  The OAuth scopes to request, for example `["repo", "user", "read:org", "workflow"]`.
</ParamField>

<ParamField path="token_auth_method" type="Literal[&#x22;body&#x22;, &#x22;basic&#x22;]" default="body">
  How client credentials are presented at the token exchange — in the request body, or as HTTP Basic auth.
</ParamField>

<ParamField path="access_type" type="string | null" default="null">
  An extra authorize-URL parameter for providers that require an explicit opt-in to refresh tokens. Google needs `"offline"` to issue a `refresh_token`.
</ParamField>

<ParamField path="prompt" type="string | null" default="null">
  An extra authorize-URL parameter. Setting `"consent"` forces the provider to re-issue a refresh token on every reconnect rather than only on the user's first authorization.
</ParamField>

<ParamField path="use_pkce" type="boolean" default="true">
  Whether the provider supports PKCE (RFC 7636) on the authorization-code flow. Defaults to `true`. Set it to `false` for providers that reject an unexpected `code_verifier` with `invalid_grant` — Netlify is the shipped example. The ModuleX runtime reads this flag from the manifest when it builds the authorization URL (`use_pkce=oauth_config.get("use_pkce", True)`), so a manifest opt-out is honored end to end.
</ParamField>

<Info>
  A source-research note flagged `use_pkce` as possibly not yet honored by the runtime ("the runtime currently hardcodes PKCE on"). Verified against the current backend: `credentials.py` reads `oauth_config.get("use_pkce", True)` when generating the authorization URL, so the manifest flag **is** honored. This page reflects the current behavior.
</Info>

Here is a complete OAuth2 auth schema:

```python OAuth2AuthSchema example theme={null}
OAuth2AuthSchema(
    display_name="OAuth2 Authentication",
    description="Connect using GitHub OAuth (recommended for most use cases)",
    setup_environment_variables=[
        EnvVar(
            name="GITHUB_OAUTH2_CLIENT_ID",
            display_name="Client ID",
            description="GitHub OAuth App Client ID",
            required=True,
            sensitive=False,
            only_for_custom=True,
            sample_format="Iv1.xxxxxxxxxxxxxxxx",
            about_url="https://github.com/settings/applications/new",
        ),
        # ... CLIENT_SECRET with sensitive=True ...
    ],
    oauth_config=OAuthConfig(
        auth_url="https://github.com/login/oauth/authorize",
        token_url="https://github.com/login/oauth/access_token",
        scopes=["repo", "user", "read:org", "workflow"],
        token_auth_method="body",
    ),
    test_endpoint=TestEndpoint(
        url="https://api.github.com/user",
        method="GET",
        headers={
            "Authorization": "Bearer {access_token}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
        },
        success_indicators=SuccessIndicators(
            status_codes=[200],
            response_fields=["login", "id"],
        ),
        cost_level="free",
        description="Validates the OAuth token by fetching authenticated user info",
    ),
)
```

For how ModuleX stores and resolves the resulting credentials, see [Credentials & OAuth2](/concepts/credentials-oauth) and [Authentication & credentials](/integrations/authentication).

## `EnvVar`

A configurable secret or setting the operator must provide, listed under an auth schema's `setup_environment_variables`.

<ParamField path="name" type="string" required>
  The environment-variable-style key, for example `GITHUB_OAUTH2_CLIENT_ID`.
</ParamField>

<ParamField path="display_name" type="string" required>
  The label shown in the credential UI.
</ParamField>

<ParamField path="description" type="string" required>
  A one-line description of the value.
</ParamField>

<ParamField path="required" type="boolean" default="true">
  Whether the value must be supplied. Defaults to `true` — the opposite of [`ParameterDef.required`](#parameterdef), which defaults to `false`.
</ParamField>

<ParamField path="sensitive" type="boolean" default="false">
  Whether the value is a secret. When `true`, it is masked in the UI (use it for client secrets and tokens).
</ParamField>

<ParamField path="only_for_custom" type="boolean" default="false">
  Whether the value is a server-level secret. When `true`, the managed ModuleX app resolves it from the server environment, while a bring-your-own-app operator supplies their own (for example a GitHub OAuth client ID and secret, or a Google Ads `developer_token`).
</ParamField>

<ParamField path="inject_into_auth_data" type="boolean" default="false">
  Whether the runtime guarantees this value is present in the credential's `auth_data` at action-execution time, so a `tools.py` function can read it. The value is read by its normalized (prefix-stripped, lowercased) key.

  When `false` (the default), the `EnvVar` is used only for OAuth provider configuration and the credential test endpoint, never for action calls.
</ParamField>

<ParamField path="sample_format" type="string | null" default="null">
  A placeholder hint shown in the UI, for example `"ghp_xxxx..."`.
</ParamField>

<ParamField path="about_url" type="string | null" default="null">
  A link to where the operator obtains the value.
</ParamField>

### `only_for_custom` × `inject_into_auth_data`

These two flags together determine where a value comes from and whether it reaches your `tools.py` function:

<Expandable title="The three combinations that matter">
  * **`inject_into_auth_data=false`** (default) — the value is OAuth or test-endpoint only. It never reaches `tools.py`.
  * **`inject_into_auth_data=true` + `only_for_custom=false`** — per-credential **user input**. The runtime persists the user-entered value into `auth_data` at credential creation (for example a Google Merchant Center `merchant_id`).
  * **`inject_into_auth_data=true` + `only_for_custom=true`** — a **server-level secret**. The managed app injects it from the server environment at credential-resolution time, while a bring-your-own-app operator supplies their own (for example a Google Ads `developer_token`).
</Expandable>

## `TestEndpoint`

The HTTP call the runtime makes to validate a configured credential. URL, header, body, and query-parameter values may contain placeholders such as `{access_token}`, `{token}`, `{api_key}`, `{bearer_token}`, or any `auth_data` or `EnvVar` key, which the runtime substitutes with the resolved credential value before making the call.

<ParamField path="url" type="string" required>
  The endpoint URL. May embed query placeholders, for example ConvertAPI's `?Secret={api_key}`.
</ParamField>

<ParamField path="method" type="Literal[&#x22;GET&#x22;, &#x22;POST&#x22;, &#x22;PUT&#x22;, &#x22;DELETE&#x22;, &#x22;PATCH&#x22;]" default="GET">
  The HTTP method for the validation call.
</ParamField>

<ParamField path="headers" type="dict[str, str]" default="{}">
  Request headers, for example `{"Authorization": "Bearer {access_token}"}`.
</ParamField>

<ParamField path="params" type="dict[str, str]" default="{}">
  The URL query string, used by integrations that pass their credential as a query parameter.
</ParamField>

<ParamField path="body" type="dict[str, Any] | null" default="null">
  A JSON payload sent on non-GET methods, for example Exa's `POST /search` validation call.
</ParamField>

<ParamField path="auth" type="BasicAuthSpec | null" default="null">
  An optional declarative directive for HTTP Basic auth synthesis. When set, the runtime builds the `Authorization: Basic <base64(u:p)>` header itself and ignores any `Authorization` entry in `headers`. See [`BasicAuthSpec`](#basicauthspec).
</ParamField>

<ParamField path="success_indicators" type="SuccessIndicators" required>
  How the runtime decides the validation call succeeded. See [`SuccessIndicators`](#successindicators).
</ParamField>

<ParamField path="cost_level" type="string" default="free">
  A free-form hint about the cost of the validation call. Observed values include `"free"` and `"minimal"`; the schema does not constrain the set.
</ParamField>

<ParamField path="description" type="string | null" default="null">
  A one-line description of what the validation call checks.
</ParamField>

### `SuccessIndicators`

How to tell a credential test endpoint succeeded.

<ResponseField name="status_codes" type="int[]" required>
  The HTTP status codes treated as success, for example `[200]`.
</ResponseField>

<ResponseField name="response_fields" type="string[] | null" default="null">
  Keys expected to be present in the response body, for example `["login", "id"]`.
</ResponseField>

### `BasicAuthSpec`

Declarative Basic auth for test endpoints where the Base64 cannot be precomputed because one or both halves are user secrets. The runtime resolves each placeholder against `auth_data`; if a placeholder name is **not** a key in `auth_data`, it is treated as a **literal** string (Mailgun, for example, uses `username_placeholder="api"`).

<ParamField path="type" type="Literal[&#x22;basic&#x22;]" default="basic">
  The auth synthesis type. The only value is `"basic"`.
</ParamField>

<ParamField path="username_placeholder" type="string" required>
  The placeholder or literal used as the Basic auth username.
</ParamField>

<ParamField path="password_placeholder" type="string" required>
  The placeholder or literal used as the Basic auth password.
</ParamField>

Here is a Basic-auth test endpoint:

```python TestEndpoint with BasicAuthSpec theme={null}
TestEndpoint(
    url="https://api.twilio.com/2010-04-01/Accounts/{TWILIO_ACCOUNT_SID}.json",
    method="GET",
    auth=BasicAuthSpec(
        username_placeholder="TWILIO_ACCOUNT_SID",
        password_placeholder="TWILIO_AUTH_TOKEN",
    ),
    success_indicators=SuccessIndicators(
        status_codes=[200],
        response_fields=["sid"],
    ),
    cost_level="free",
    description="Validates the Account SID and Auth Token by fetching the account.",
)
```

## Output models are not in the manifest

Neither `ActionDefinition` nor `IntegrationManifest` carries an output schema. The return shape of every action is derived at backend startup from its `@tool` function's return-type annotation: the runtime resolves the annotation, and if the return type has a `model_json_schema()` method, it calls it to produce the model-facing output schema.

This is why your action's typed Pydantic return model lives in `outputs.py`, and why the `@serialize_pydantic_return` wrapper must preserve the annotation while coercing the runtime value to a dict. Reading a manifest alone will never reveal an action's output shape — see the [@tool function contract](/integrations/building/tool-contract) for the full output-model and decorator rules.

## Full manifest example

This abridged GitHub manifest shows the whole contract assembled: two auth schemas (OAuth2 and a personal access token) and a set of actions.

<CodeGroup>
  ```python manifest.py theme={null}
  from modulex_integrations.schema import (
      ActionDefinition,
      BearerTokenAuthSchema,
      EnvVar,
      IntegrationManifest,
      OAuth2AuthSchema,
      OAuthConfig,
      ParameterDef,
      SuccessIndicators,
      TestEndpoint,
  )

  manifest = IntegrationManifest(
      name="github",
      display_name="GitHub",
      description="GitHub repository and code management platform",
      version="1.0.0",
      author="ModuleX",
      logo="modulex:github-themed",
      app_url="https://github.com",
      categories=["Developer Tools & Infrastructure", "version-control", "productivity"],
      actions=[
          ActionDefinition(
              name="create_repository",
              description="Create a new repository",
              parameters={
                  "name": ParameterDef(
                      type="string", description="Repository name", required=True
                  ),
                  "description": ParameterDef(
                      type="string", description="Repository description"
                  ),
                  "private": ParameterDef(
                      type="boolean",
                      description="Whether the repository is private",
                      default=False,
                  ),
              },
          ),
          # ... more actions ...
      ],
      auth_schemas=[
          OAuth2AuthSchema(
              display_name="OAuth2 Authentication",
              description="Connect using GitHub OAuth (recommended for most use cases)",
              setup_environment_variables=[
                  EnvVar(
                      name="GITHUB_OAUTH2_CLIENT_ID",
                      display_name="Client ID",
                      description="GitHub OAuth App Client ID",
                      required=True,
                      sensitive=False,
                      only_for_custom=True,
                      sample_format="Iv1.xxxxxxxxxxxxxxxx",
                      about_url="https://github.com/settings/applications/new",
                  ),
                  # ... CLIENT_SECRET with sensitive=True ...
              ],
              oauth_config=OAuthConfig(
                  auth_url="https://github.com/login/oauth/authorize",
                  token_url="https://github.com/login/oauth/access_token",
                  scopes=["repo", "user", "read:org", "workflow"],
                  token_auth_method="body",
              ),
              test_endpoint=TestEndpoint(
                  url="https://api.github.com/user",
                  method="GET",
                  headers={
                      "Authorization": "Bearer {access_token}",
                      "Accept": "application/vnd.github+json",
                      "X-GitHub-Api-Version": "2022-11-28",
                  },
                  success_indicators=SuccessIndicators(
                      status_codes=[200], response_fields=["login", "id"]
                  ),
                  cost_level="free",
                  description="Validates OAuth token by fetching authenticated user info",
              ),
          ),
          BearerTokenAuthSchema(
              display_name="Personal Access Token",
              description="Use your GitHub Personal Access Token (Classic or Fine-grained)",
              setup_instructions=[
                  "Go to GitHub Settings then Developer settings then Personal access tokens",
                  "Generate a token with the repo and user scopes",
                  "Paste the token below",
              ],
              setup_environment_variables=[
                  EnvVar(
                      name="GITHUB_PERSONAL_TOKEN",
                      display_name="Personal Access Token",
                      description="GitHub Personal Access Token",
                      required=True,
                      sensitive=True,
                  ),
              ],
              test_endpoint=TestEndpoint(
                  url="https://api.github.com/user",
                  headers={"Authorization": "Bearer {bearer_token}"},
                  success_indicators=SuccessIndicators(
                      status_codes=[200], response_fields=["login", "id"]
                  ),
              ),
          ),
      ],
  )
  ```
</CodeGroup>

<MediaEmbed id="MX-MEDIA-4181" type="screenshot" caption={"The credential setup UI rendered from a manifest's auth schemas."} />

## Validation rules to remember

<AccordionGroup>
  <Accordion title="Unknown fields fail at import time">
    Every model sets `extra="forbid"`. A misspelled field name or a leftover legacy field stops the package from importing — you find out immediately, not when a credential is created.
  </Accordion>

  <Accordion title="name patterns are enforced">
    Both `IntegrationManifest.name` and `ActionDefinition.name` must match `^[a-z][a-z0-9_]*$`. The integration name must also equal the entry-point key and the package directory name; each action name must equal a `@tool` function name in `TOOLS`.
  </Accordion>

  <Accordion title="required defaults differ between ParameterDef and EnvVar">
    `ParameterDef.required` defaults to `false`; `EnvVar.required` defaults to `true`. Set them explicitly when the default is not what you want.
  </Accordion>

  <Accordion title="test_endpoint is optional">
    A `modulex_key` "public API" integration with no credential to validate can omit `test_endpoint` entirely; the runtime then skips credential testing.
  </Accordion>

  <Accordion title="The manifest is not the callable surface">
    Manifest `parameters` are catalog metadata. The `@tool` function's `args_schema` is the authoritative list of arguments an action accepts, and output shapes come from the function's return annotation, not the manifest.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="@tool function contract" icon="code" href="/integrations/building/tool-contract">
    The decorator order, auth parameter patterns, and Pydantic output models that pair with this manifest.
  </Card>

  <Card title="Build an integration" icon="puzzle-piece" href="/integrations/building/overview">
    The end-to-end workflow for authoring and registering your own integration.
  </Card>

  <Card title="Authentication & credentials" icon="key" href="/integrations/authentication">
    How the six auth schema variants map onto credential setup and OAuth2 (PKCE).
  </Card>

  <Card title="Glossary" icon="book" href="/reference/glossary">
    Canonical definitions for every manifest term used on this page.
  </Card>
</CardGroup>
