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-timestate 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.
200 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
Authorizationheader is required, because the browser arriving from the provider has no app token. Trust comes from the one-timestatevalue, which is read and deleted from the short-lived server-side store on first use. - It always responds with a
302redirect to your frontend landing page (defaulthttps://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>
- Success:
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.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
oauth2credential shows the literal"OAuth2"; anapi_keyshows a masked value likesk-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:Your own credentials (oauth2 / api_key / bearer_token / custom)
Your own credentials (oauth2 / api_key / bearer_token / custom)
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.ModuleX-managed keys (modulex_key)
ModuleX-managed keys (modulex_key)
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,
1–500.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_config → oauth2.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-temporaryvalidates auth data before you save it. It takesintegration_name,auth_type, andauth_data.POST /credentials/{id}/testvalidates a saved credential. An expired credential returnsis_valid: falsewith “Credential has expired”.
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-defaultmakes a credential the default for its integration and unsets the previous default.PUT /credentials/{id}updates onlydisplay_nameandmetadata. 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 returns204 No Content.
MCP server credentials
A credential can also point at an external Model Context Protocol (MCP) server. Creating one connects to the server (overstreamable_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-toolsreturns the discovered tool list and a total count.POST /credentials/{id}/refresh-discoveryre-discovers tools and reports what was added or removed. It is valid only formcp_servercredentials; calling it on any other credential returns400.
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.