A credential is organization-scoped, never user-scoped. Every credential
belongs to one organization and is encrypted, resolved, and billed against that
org. All credential endpoints require the owner or admin role
(
organization_admin_required); the member role is retired. The org is selected
by the X-Organization-ID header on every request — see
Org context & X-Organization-ID.The six auth schema variants
An integration manifest exposes one or more auth schemas underauth_schemas. The list is a discriminated union keyed on auth_type, so each
entry is exactly one of six variant classes. A single integration may ship several
(GitHub ships both OAuth2 and a bearer-token personal-access-token option; Exa ships
both an API key and a ModuleX-managed key).
There is one casing edge case to know about. The integration manifest schema
defines six
auth_type values. The backend credential table’s CHECK
constraint allows seven — it additionally accepts a legacy bearer value with
no corresponding manifest variant. New integrations must emit only the six schema
variants above; bearer is a legacy database value, not a manifest option.Shared fields on every variant
Every auth schema, regardless ofauth_type, inherits the same base fields.
string
required
Human-readable label shown in the connect UI (e.g.
"OAuth2 Authentication").string
required
One-line explanation of the method shown next to the label.
string[] | null
default:"null"
An ordered list of steps shown to the person connecting the integration (e.g. how
to create a personal access token).
EnvVar[]
default:"[]"
Operator- or user-supplied secrets and settings for this method. See
EnvVar below.TestEndpoint | null
default:"null"
An optional HTTP call the runtime makes to validate a configured credential. Some
modulex_key public-API integrations ship none, in which case credential testing
is skipped. See TestEndpoint.OAuth2 variant (oauth2)
OAuth2AuthSchema adds a single extra field, oauth_config, that describes the
authorization-code flow.
Bearer-token variant (bearer_token)
BearerTokenAuthSchema adds no extra fields. You supply a long-lived token (e.g. a
GitHub personal access token). The runtime sends it as
Authorization: Bearer <token> to the service. On the wire the stored
auth_type is bearer_token.
API-key variant (api_key)
ApiKeyAuthSchema adds no extra fields. You supply a provider-issued API key. How
the key is presented (header name, query parameter, etc.) is defined by the
integration’s test_endpoint and its tool functions, not by a fixed convention.
ModuleX-managed-key variant (modulex_key)
ModulexKeyAuthSchema adds no extra fields, and you supply nothing — ModuleX
provisions a managed key from a pooled key store. Usage of a modulex_key
credential is metered in credits and passes through the billing gate at run
time (see Resolution at run time
below and Credits & metering). This is the difference that
matters: a modulex_key credential always carries a usage/credit gate; a
credential you supply yourself never does.
Custom variant (custom)
CustomAuthSchema adds no extra fields at the schema level. The integration
defines the fields it needs through setup_environment_variables, and the connect
UI renders one input per declared EnvVar. PostgreSQL (host, port, database, user,
password) is a typical example.
EnvVar — operator- or user-supplied values
Each auth schema can declare a list of EnvVar entries under
setup_environment_variables. An EnvVar is a single secret or setting, and two
of its flags — only_for_custom and inject_into_auth_data — decide where the
value comes from and whether a tool function can read it.
string
required
Env-var-style key (e.g.
GITHUB_OAUTH2_CLIENT_ID).string
required
Label shown in the UI.
string
required
Help text shown under the field.
boolean
default:"true"
Whether the value must be supplied. Note this defaults to
true — the opposite of
ParameterDef.required, which defaults to false.boolean
default:"false"
Whether the value is a secret. Sensitive values are masked in the UI.
boolean
default:"false"
true marks the value as a server-level secret: the ModuleX-managed app
resolves it from the server environment, while a bring-your-own-app user supplies
their own. Examples: a GitHub OAuth client id/secret, a Google Ads
developer_token.boolean
default:"false"
true guarantees the value is present in auth_data at action-execution time so a
tool function can read it (the key is prefix-stripped and lowercased). false
(the default) means the value is used only for OAuth provider config and the test
endpoint, and never reaches a tool call.string | null
default:"null"
Placeholder hint shown in the input (e.g.
"ghp_xxxx...").string | null
default:"null"
Link to where the user obtains the value.
TestEndpoint — credential validation
When an auth schema declares a test_endpoint, ModuleX can validate a credential
by making one HTTP call before relying on it. Placeholders in the URL, headers,
params, or body — such as {access_token}, {token}, {api_key},
{bearer_token}, or any auth_data/EnvVar key — are substituted by the runtime.
When you test a credential, the response reports test_method, which is one of
api_call (an actual request was made), basic, or none (the integration ships
no test_endpoint, so testing is skipped and the credential is treated as valid).
See Managing credentials for the full
test-and-save flow.
How an integration reads the credential
A tool function receives its credential through one of two run-time conventions, chosen by the auth type:- Token-based (
oauth2,bearer_token) — the function signature leads withauth_type: str, auth_data: dict[str, Any], then its action parameters. It builds request headers from(auth_type, auth_data). - Key-based (
api_key,modulex_key) — the function takes the action parameters plusapi_key: strdirectly (often sent asx-api-key: {api_key}), and checks the key is non-empty before calling out.
@tool function contract; for the full decorator order, output
models, and worked examples, see
The @tool function contract and
Manifest & schema contract.
The OAuth2 connect flow (PKCE)
OAuth2 integrations are connected through an authorization-code flow with PKCE. You never paste a token; ModuleX initiates the flow, the provider redirects the browser back, and ModuleX exchanges the code and stores the resulting credential.Step 1 — initiate
POST /credentials/oauth2/initiate starts the flow. The backend loads the
integration’s oauth2 schema, builds the authorization URL (generating a PKCE
code_verifier and S256 code_challenge), stores the flow state in Redis with a
5-minute TTL, and returns the URL for the browser to open.
string
required
The integration to connect. Must expose an
oauth2 auth schema, or the request
fails with 400.boolean
default:"true"
true uses the ModuleX-registered OAuth app for the provider (the managed path).
false uses your own OAuth app and requires custom_oauth_config.object
Required only when
use_modulex_oauth is false. Must contain client_id and
client_secret for your own OAuth app. The auth_url/token_url come from the
integration schema.string
required
The callback URL the provider redirects to after consent. This is the ModuleX
backend callback (e.g.
https://api.modulex.dev/credentials/oauth2/callback).string
Space-separated scopes. Defaults to the provider/schema scopes joined by spaces.
string
A label for the credential that gets created.
boolean
default:"false"
Whether the new credential becomes the default for its integration.
object
Per-user
EnvVar values (string-to-string). Only values whose EnvVar has
inject_into_auth_data: true and only_for_custom: false are folded into the
persisted auth_data.string
Composer auto-resume linkage. Must be supplied together with
composer_request_id and composer_llm_config; supplying some but not all returns
400.string
Composer auto-resume linkage — see
composer_chat_id.object
Composer auto-resume linkage — see
composer_chat_id.string
The provider authorize URL to open in the browser. Includes the
state and PKCE
code_challenge.string
A one-time CSRF/correlation token. ModuleX stores the flow state in Redis keyed by
this value with a 5-minute TTL.
Both official SDKs expose this initiate call —
credentials.initiateOAuth2(params)
(JavaScript) and credentials.initiate_oauth2(integration_name, *, redirect_uri, ...)
(Python) — to start the flow and return the authorization_url. They cannot
complete it for you: finishing consent requires opening that authorization_url in
a browser so the provider redirect hits the backend callback. In the app, the
connect UI performs the same request and opens the consent page for you. See
Managing credentials.Step 2 — provider consent and callback
The browser opensauthorization_url, the user approves the requested scopes, and
the provider redirects back to the redirect_uri, which is the backend callback:
GET /credentials/oauth2/callback?code=...&state=...
This endpoint is the only credential route that is anonymous — the browser
arriving from the provider carries no ModuleX session. Trust comes entirely from
the one-time Redis state value. The callback:
1
Validate state
Reads and deletes the Redis
state (one-time read). If it is missing — expired
past five minutes or already used — the flow ends in an error redirect with
error_code=invalid_state.2
Exchange the code
Posts the authorization code (and PKCE
code_verifier) to the provider’s
token_url, presenting client credentials per the schema’s token_auth_method
(body or basic). Failure ends in error_code=token_exchange_failed.3
Build and store the credential
Assembles
auth_data (access_token, token_type defaulting to bearer, an
optional refresh_token, and expires_at derived from expires_in), folds in any
injected EnvVar values, encrypts it, and creates the credential. The credential
is created with no created_by because the call is anonymous.4
Redirect back to the app
Always returns a 302 to the frontend landing page — never JSON. On success the
URL carries
?status=success&integration=<name>&credential_id=<uuid>; on failure
?status=error&integration=<name>&error_code=<code>&message=<urlencoded>.error_code values include oauth_denied,
oauth_provider_error, missing_params, invalid_state,
token_exchange_failed, credential_creation_failed, invalid_credentials, and
internal_error.
Token refresh — and why you reconnect instead
ModuleX refreshes OAuth2 access tokens automatically at run time. When a credential is resolved for a tool call and itsexpires_at is within five minutes
of expiring, the resolver uses the stored refresh_token and decrypted
oauth_config to obtain a new access token, re-encrypts the credential, and
continues. You do not trigger this; it happens inside resolution.
There is also a behavioral gap to be aware of in automatic refresh. The automatic
in-resolution refresh always presents client credentials in the request body and
ignores the schema’s
token_auth_method. A provider that requires basic-auth at
the token endpoint (such as Notion) would fail an automatic refresh with
invalid_client; reconnecting re-issues a working credential. This is logged as a
parity gap.How credentials are stored (encrypted at rest)
Credentials are never stored in plaintext. Each credential’s secret material is encrypted before it is written and decrypted only in memory at the moment of use.- Per-credential isolation. Each credential is sealed with its own key, bound to your organization and that specific credential, so a stored secret cannot be unlocked outside the organization it belongs to.
- Masked on read-back. When a credential is read, secrets are returned masked,
never in clear: an
oauth2credential reports the literal labelOAuth2, anapi_keyreports a masked key, and a bearer token reports a masked token.
The
modulex_key (ModuleX-managed) credential stores only a UUID in its
encrypted blob, not a real provider key. The UUID points into a system-managed key
pool; the real key is fetched from the pool at resolution time and is never
persisted on your credential. See the resolution rules below.How credentials are resolved at run time
When a tool or LLM node executes, ModuleX resolves a credential in two phases: pick one, then prepare it for execution (decrypt, refresh, and — for managed keys — gate on credits).Phase A — pick a credential
1
Explicit credential_id wins
If the caller passes a
credential_id, that exact credential is used. It must
belong to the org and match the integration; otherwise resolution fails with “no
credential found.”2
Otherwise, the default
If no id is given, the credential marked
is_default for the
(organization, integration_name, integration_type) is used.3
Otherwise, most-recent valid
If there is no default, ModuleX falls back to any valid credential, ordered to
prefer a user credential over a
modulex_key, then most recent first.4
Otherwise, fail
If nothing valid is found, resolution fails with “no credential found.”
credential_id > is_default row > most-recent
valid user credential > most-recent valid modulex_key.
Phase B — prepare for execution
The prepare step differs by whether the chosen credential is a managed key or one you supplied.- User credential (oauth2 / api_key / bearer_token / custom)
- ModuleX-managed key (modulex_key)
- Decrypt
auth_data. - If
auth_typeisoauth2, refresh the access token when it is within five minutes of expiring (re-encrypt, update, commit, and clear the Redis cache for the credential). This needs arefresh_tokenand the decryptedoauth_config. - Return the decrypted
auth_datafor the tool function to use.
Errors
Credential CRUD and OAuth endpoints raise the standard FastAPIHTTPException envelope — {"detail": <string>} — for everything except the
org-level rate-limit deny, whose detail is a dict. These routes do not emit
the flat DenialEnvelope; the credit gate fires inside credential resolution at
run time, not on these CRUD routes. For the full taxonomy of all three error
envelope shapes and which surface emits each, see
Errors & status codes.
There is no
402 on these credential routes. A 402 from credit exhaustion
appears on the run / composer / assistant / managed-knowledge surfaces when a
modulex_key credential is resolved during execution — that is where the credit
gate is enforced. See Usage gating & limits.Next steps
Credentials & OAuth2
The conceptual model and a guided walkthrough of connecting an OAuth integration.
Managing credentials
Create, set defaults for, test, and delete credentials in the app and over the API.
Manifest & schema contract
The full pydantic contract behind the six auth schema variants.
Data security & encryption
How ModuleX encrypts credentials and manages keys.