Skip to main content
A credential is a stored, encrypted authentication record that links one organization to one integration — a tool, an LLM provider, or a knowledge provider. Credentials never live in your workflow definitions; they are resolved at run time, decrypted in memory, and used to authenticate the outbound call ModuleX makes on your behalf. This page is the reference for how credentials are modelled, how the OAuth2 authorization-code flow works (with PKCE on by default), how secrets are encrypted at rest, and how ModuleX picks which credential to use when a tool or model runs. For the connect-it walkthrough see Authentication & credentials; for day-to-day management see Managing credentials.
Every credentials endpoint requires owner or admin role on the organization, and every request must carry both Authorization: Bearer mx_live_… and the X-Organization-ID header. The organization is taken from that header, never from the request body. See Authentication and Roles & permissions.

The credential model

Each credential record belongs to exactly one organization and one integration, and it stores its secrets in encrypted columns. The non-sensitive fields below are what every API and SDK response returns; the secret material (auth_data, oauth_config) is never returned in clear text.
string
The credential’s UUID. This is the handle you pass to every per-credential operation.
string
The integration this credential authenticates (for example github, openai, slack).
string | null
The integration category: tool, llm_provider, or knowledge_provider.
string
A human-readable label. Defaults to the integration’s display name when you do not set one.
string
The authentication mechanism. One of oauth2, api_key, bearer_token, modulex_key, custom, or internal. (The backend also still accepts the legacy value bearer. The internal type is system-managed and is excluded from list output.)
boolean
Whether this is the default credential for its integration in the organization. Only one credential per integration can be the default.
string | null
ISO-8601 timestamp of creation.
string | null
ISO-8601 timestamp of the last update.
string | null
ISO-8601 timestamp of the last time the credential was resolved for a run, or null if never used.
string | null
ISO-8601 expiry timestamp. For OAuth2 credentials this is the access-token expiry derived from the provider’s expires_in.
object | null
Arbitrary metadata. For MCP server credentials this holds the discovered tool catalog. Note the field is credentials_metadata (snake_case) on the wire.
Wire casing. The credentials API does not camelCase its responses. Fields such as credentials_metadata, auth_type, display_name, and created_by_email are snake_case on the wire. The SDKs accept camelCase parameters (for example integrationName, makeDefault) and translate them for you, but they return the snake_case response shapes unchanged.

The six authentication types

auth_type records how a credential authenticates. When you create a credential, ModuleX auto-detects the type from the body you send (see Create a credential).
modulex_key is metered; your own keys are not. A modulex_key credential routes through ModuleX-provisioned provider keys and consumes credits on every use, gated by your plan. Credentials you bring yourself (BYOK) — api_key, bearer_token, oauth2, custom — are billed by the provider directly and carry no ModuleX credit gate. See Credits & metering.

OAuth2 authorization-code flow (with PKCE)

For integrations that support OAuth2, ModuleX runs the standard authorization-code flow with PKCE (Proof Key for Code Exchange, S256) on by default. The flow has two API steps you drive — initiate and the callback — and the provider’s browser redirect connects them. A one-time state token (held in a short-lived server-side store with a 5-minute TTL) ties the two halves together.
1

Initiate the flow

Call POST /credentials/oauth2/initiate with the integration name and a redirect_uri. ModuleX generates the PKCE code_verifier/code_challenge, stores the flow state in a short-lived server-side store (5-minute TTL), and returns an authorization_url plus a state token.
2

Send the user to the provider

Redirect the user’s browser to the returned authorization_url. The user authenticates with the provider and grants the requested scopes.
3

The provider redirects to the callback

The provider redirects the browser to GET /credentials/oauth2/callback with a code and the state. This endpoint is anonymous — it cannot carry your app token, so trust comes from the one-time state it reads (and deletes) from the short-lived server-side store.
4

ModuleX exchanges the code and stores the credential

The callback exchanges the authorization code for tokens, encrypts them, creates an oauth2 credential, and 302-redirects the browser to your frontend landing page with the result appended as query parameters — never as JSON.

Initiate the OAuth2 flow

Start an OAuth2 authorization-code flow and get back the URL to send the user to.
string
required
The integration to authorize. It must declare an oauth2 auth schema, or the call returns 400 “does not support OAuth2”.
string
required
The callback URL the provider redirects back to. Use the ModuleX callback, https://api.modulex.dev/credentials/oauth2/callback.
boolean
default:"true"
When true, the flow uses ModuleX’s managed OAuth application for the integration. When false, you must supply custom_oauth_config.
object
Your own OAuth app configuration. Required when use_modulex_oauth is false; must include client_id and client_secret. The auth_url/token_url are taken from the integration’s schema.
string
Space-separated scopes to request. Defaults to the provider’s or schema’s configured scopes.
string
Label for the credential that will be created on success.
boolean
default:"false"
Whether to set the resulting credential as the default for its integration.
object
Per-user setup environment variables to fold into the persisted auth_data, keyed by raw env-var name. Only keys the integration is allowed to read are retained.
string
AI Composer chat to atomically resume once the callback completes. Must be supplied together with composer_request_id and composer_llm_config — partial linkage returns 400.
string
The Composer interrupt request id, validated on the callback. All-or-none with composer_chat_id and composer_llm_config.
object
LLM configuration used to rebuild the chat model on Composer resume. All-or-none with the two fields above.
Response200 OK:
string
The provider URL to redirect the user to.
string
The opaque one-time CSRF/state token correlating this flow. Valid for 5 minutes.

The callback (browser redirect target)

GET /credentials/oauth2/callback is where the provider sends the user’s browser. You do not call it directly and there is no SDK method for it — it is the redirect target you pass as redirect_uri.
  • It is anonymous: no Authorization header is required, because the browser arriving from the provider has no app token. Trust comes from the one-time state value, which is read and deleted from the short-lived server-side store on first use.
  • It always responds with a 302 redirect to your frontend landing page (default https://app.modulex.dev/oauth/callback), never with JSON — including on failure.
  • The result is appended as query parameters:
    • Success: ?status=success&integration=<name>&credential_id=<uuid>
    • Failure: ?status=error&integration=<name>&error_code=<code>&message=<urlencoded>
Token exchange handles non-standard providers. The code-for-token exchange sends client credentials either in the request body (client_secret_post, the default) or as HTTP Basic auth, depending on the integration’s token_auth_method. It also handles providers that return a 200 with a {"ok": false} body (Slack-style failures) and redacts token values from logs.

Refreshing OAuth2 tokens

ModuleX keeps OAuth2 access tokens fresh automatically at run time. When a credential is resolved for a tool or model call and its access token expires within the next 5 minutes, ModuleX refreshes it inline using the stored refresh token and re-encrypts the result before the call proceeds. You do not need to schedule or trigger this.
Do not rely on a manual or programmatic token refresh. A manual refresh endpoint and the matching refreshOAuth2 / refresh_oauth2 SDK methods exist in the surface, but the path is known-broken and is not a supported flow:
  • In the ModuleX app, the credential UI’s refresh action targets a frontend route that does not exist and 404s before reaching the backend.
  • The backend handler itself has defects that prevent it from completing reliably.
If a credential’s authorization has fully lapsed (for example the refresh token was revoked or expired) and automatic refresh cannot recover it, reconnect the integration by running the OAuth2 flow again (POST /credentials/oauth2/initiate). Re-authorizing replaces the stale tokens with a fresh credential. This is the only supported way to restore a lapsed OAuth2 connection. See Known limitations.
Automatic refresh has one parity gap. The inline run-time refresh always sends client credentials in the request body. A provider that requires HTTP Basic auth for refresh (for example Notion’s token_auth_method: "basic") would receive an invalid_client error on automatic refresh. For those providers, reconnect via the OAuth2 flow rather than depending on background refresh.

Encryption at rest

Credential secrets are encrypted before they are stored and decrypted only in memory, at the moment a tool or model needs them. 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. See Data security & encryption for the platform-wide picture.

Per-credential secrets

Your access tokens, API keys, and OAuth settings are encrypted with a key unique to that credential and organization — never stored in clear text.

OAuth app secrets

The secrets behind ModuleX’s managed OAuth apps are protected with their own separately-keyed encryption.
  • Per-credential isolation. Because each key is tied to one credential in one organization, a stored secret can never be reused or moved to another credential.
  • No secret read-back. Reading a credential returns masked placeholders only: an oauth2 credential shows the literal "OAuth2"; an api_key shows a masked value like sk-proj12…xyz; a bearer token shows a masked token. The clear-text secret is never returned by any endpoint.

Credential resolution at run time

When a tool or model executes, ModuleX must decide which credential to use and then prepare it for the outbound call. This happens in two stages.

Stage 1 — pick a credential

1

Explicit credential id

If the run specifies a credential_id, ModuleX uses it directly. The credential must match both the organization and the integration; otherwise resolution fails with “no credential found”.
2

The default credential

Otherwise, ModuleX looks for the credential marked is_default for that integration and type.
3

The most-recent valid fallback

If there is no default, ModuleX falls back to any valid credential, preferring your own credentials over modulex_key ones, then the most recently created. If nothing valid exists, resolution fails.
Precedence, in one line: explicit credential_id → the is_default credential → most-recent valid user credential → most-recent valid modulex_key.

Stage 2 — prepare for execution

The chosen credential is then prepared depending on its type:
ModuleX decrypts the auth_data. For oauth2 credentials it also checks the access-token expiry and refreshes inline if the token expires within 5 minutes (see Refreshing OAuth2 tokens). There is no credit check on this path — your own keys are billed by the provider, not by ModuleX.
The encrypted blob holds a UUID pointer, not the real API key. ModuleX decrypts the pointer, verifies it against the organization’s managed-key records, fetches the real key from the system key pool, and runs a credit-limit check before use. If the credit limit is exceeded — or there is no active subscription — preparation fails and the call does not proceed. Tool usage on this path is metered after execution.
Where the billing gate lives. The credentials CRUD and OAuth endpoints on this page do not themselves run a per-call credit gate — they return the standard {"detail": …} error shape. The credit gate fires during resolution of modulex_key credentials, which happens on the run / composer / assistant / managed-knowledge surfaces. Those surfaces return the flat DenialEnvelope (402/403/429) when usage is denied. See Usage gating & limits and Errors & status codes.

The credentials API

All paths below are literally /credentials/... (the router is mounted with no version prefix). Every route requires owner/admin and the auth headers shown above. Each operation maps to a JavaScript and Python SDK method; see the SDK ⇄ API parity matrix.

List credentials

GET /credentials returns credentials grouped by integration. Supplying integration_name switches the response to a flat list for that one integration.
string
Return a flat list for this integration instead of the grouped shape.
string
Filter by auth type (for example oauth2, api_key).
integer
default:"100"
Page size, 1500.
integer
default:"0"
Number of records to skip. Must be ≥ 0.

Create a credential

POST /credentials (returns 201). The credential type is auto-detected from the body — you do not pass a route-level type. Match the trigger column from the authentication types table.
string
required
The integration to create the credential for (for example openai).
object
The secret material. Its shape selects the type: {"api_key": "…"}api_key; {"token": "…"} or {"bearer_token": "…"}bearer_token; {"access_token": "…"} + a top-level oauth_configoauth2.
string
Set explicitly to modulex_key or custom to select those types. For other types the value is inferred from auth_data.
object
OAuth2 configuration (token_url, client_id, client_secret, …). Required to create an oauth2 credential directly. Stored encrypted.
string
Human-readable label.
boolean
default:"false"
Set this credential as the default for its integration.
string
ISO-8601 datetime after which the credential is treated as expired.
For OAuth2, prefer the initiate flow over creating an oauth2 credential directly — it runs PKCE and the code exchange for you.

Test a credential

Two operations validate credentials against the integration’s configured test endpoint:
  • POST /credentials/test-temporary validates auth data before you save it. It takes integration_name, auth_type, and auth_data.
  • POST /credentials/{id}/test validates a saved credential. An expired credential returns is_valid: false with “Credential has expired”.
A test response reports is_valid, a human-readable message, tested_at, and a test_method of api_call, basic, or none. Integrations without a configured test endpoint report test_method: "none" and is_valid: true with a “no test endpoint” message rather than failing.

Set default, update, and delete

  • POST /credentials/{id}/set-default makes a credential the default for its integration and unsets the previous default.
  • PUT /credentials/{id} updates only display_name and metadata. Secrets are immutable through this route — to change a secret, reconnect (OAuth2) or create a new credential and delete the old one. There is no rotate endpoint.
  • DELETE /credentials/{id} permanently deletes a credential and returns 204 No Content.

MCP server credentials

A credential can also point at an external Model Context Protocol (MCP) server. Creating one connects to the server (over streamable_http by default), discovers its tools, and stores the catalog in credentials_metadata. MCP server credentials are persisted with integration_name: "mcp_server" and auth_type: "custom".
string
required
The MCP server’s URL.
object
Headers to send when connecting, for example {"Authorization": "Bearer mcp-token"}.
string
Label for the credential.
boolean
default:"false"
Set as default.
  • GET /credentials/{id}/mcp-tools returns the discovered tool list and a total count.
  • POST /credentials/{id}/refresh-discovery re-discovers tools and reports what was added or removed. It is valid only for mcp_server credentials; calling it on any other credential returns 400.

Errors

Credentials endpoints return the standard {"detail": "<string or object>"} HTTPException envelope. There is no 402 on these routes — credit denial happens during run-time resolution, not on these CRUD calls (see Where the billing gate lives). The status codes you will see across this subsystem: See Errors & status codes for the full error model across surfaces and SDK errors & retries for how the SDKs surface them.

Where to go next

Authentication & credentials

The connect-it walkthrough and the six integration auth schema variants.

Managing credentials

Create, scope, and rotate credentials in the app and via the API.

Data security & encryption

How ModuleX encrypts credentials and manages keys platform-wide.

Known limitations

Documented gaps, including the broken manual OAuth2 refresh path.