Skip to main content
A credential is a stored, encrypted auth record that links your organization to an integration (a tool, an LLM provider, or a knowledge provider). Each credential has a credential_id, an auth_type, an owning organization, and an optional display_name. This page covers the full lifecycle: create and connect, inspect and test, set a default, rotate, and revoke — both in the app and via the API. For the auth-type variants themselves (API key, OAuth2 with PKCE, bearer, and the other schemas) and the OAuth2 connect flow in depth, see Authentication & credentials and Credentials & OAuth2. For request lifecycle and base URLs, see the API overview.
Credentials are organization-scoped. The organization is always taken from the X-Organization-ID header (and the authenticated caller’s active org) — never from the request body. A credential created under one organization is invisible to every other organization. See Org context & X-Organization-ID.

Who can manage credentials

Every credential endpoint requires an owner or admin role on the organization (the backend dependency is organization_admin_required). The member role is retired and is not a current first-class role. See Roles & permissions.
OutcomeHTTP statusWhen
Missing X-Organization-ID400No org context on the request
Not a member of the org403Caller is not in the organization
Member but not owner/admin403Role check fails
Inactive user403The user account is not active
Rate limited429The org-class api rate limit (fail-open), with X-RateLimit-* and Retry-After headers
The credential CRUD and OAuth endpoints have no per-call credit gate — they return the standard {"detail": ...} error envelope, not the flat DenialEnvelope. The credit gate runs later, at execution time, only for ModuleX-managed (modulex_key) credentials. There is no 402 on these routes. For the gated surfaces and the DenialEnvelope shape, see Errors & status codes and Usage gating & limits.

Authenticate every request

All examples use the same auth as the rest of the API: an Authorization: Bearer token (a mx_live_* API key, or a Clerk JWT from the app) plus the X-Organization-ID header. The SDKs send both for you once configured. See Authentication.
curl -s https://api.modulex.dev/credentials \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"

Create and connect a credential

In the app

  1. Open Settings → Credentials (or Browse to start from the integration catalog), then choose the integration you want to connect.
  2. Pick the auth type the integration supports — oauth2, api_key, bearer_token, modulex_key, or a custom schema with fields from the integration’s manifest.
  3. For API key, bearer, and custom types, fill in the secret fields and select Test to validate before saving (see Test a credential). For OAuth2 you are redirected to the provider’s consent screen, then back to ModuleX.
  4. Optionally set a display name and toggle Make default so this credential is used automatically for the integration.
  5. Save. The secret is encrypted at rest and only a masked form is ever shown again.
🎬 MEDIA PLACEHOLDER · MX-MEDIA-4030 · [APP_VIDEO] [APP_VIDEO_DESCRIPTION]: Connecting an integration credential from Settings → Credentials, including the test-before-save step and the OAuth2 redirect. [APP_VIDEO_DETAILS]: Record in the live app: open Settings → Credentials, pick an API-key integration (e.g. Tavily), enter a key, run Test (show the success toast), set a display name, toggle Make default, save. Then show an OAuth2 integration (e.g. GitHub) redirecting to consent and returning with a created credential. 30-45s, light mode, 16:9, highlight the Test and Make default controls.

Via the API

POST /credentials creates a credential. The body is read raw and the credential type is auto-detected from auth_data / auth_type / integration_name — you do not send a type field. Returns 201 Created with a CredentialResponse.
integration_name
string
required
The integration to attach the credential to (for example slack, openai, github). Must be a live integration. For an external MCP server, prefer the dedicated MCP create path described in Authentication & credentials.
auth_data
object
The secret payload, encrypted at rest. The presence of specific keys selects the credential type:
auth_type
string
Explicit auth type. Required for modulex_key (ModuleX-managed pooled key — send no auth_data) and custom. For api_key, bearer_token, and oauth2, the type is inferred from auth_data and you may omit it. Accepted values: oauth2, api_key, bearer_token, modulex_key, custom. (The backend also tolerates a legacy bearer.)
oauth_config
object
OAuth2 client configuration (client_id, client_secret, token_url, and related fields) sent only when creating an oauth2 credential directly with an access_token. Most OAuth2 credentials are created by the connect flow on the Authentication & credentials page rather than this field.
display_name
string
A human-friendly label, for example Production Slack. Defaults to a generated name.
metadata
object
Optional free-form metadata stored alongside the credential.
make_default
boolean
default:"false"
When true, this credential becomes the default for its integration, unsetting any prior default.
expires_at
string
Optional ISO-8601 expiry timestamp (SDK only — accepted by the SDK create methods).
curl -X POST https://api.modulex.dev/credentials \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" \
  -H "Content-Type: application/json" \
  -d '{
    "integration_name": "slack",
    "display_name": "Production Slack",
    "make_default": true,
    "auth_data": { "api_key": "xoxb-your-slack-token" }
  }'
credential_id
string
The unique identifier for the credential. Use it for every subsequent operation.
integration_name
string
The integration this credential belongs to.
integration_type
string
One of tool, llm_provider, knowledge_provider.
display_name
string
The credential’s label.
auth_type
string
The detected auth type: oauth2, api_key, bearer_token, modulex_key, or custom.
is_default
boolean
Whether this credential is the default for its integration.
created_at
string
ISO-8601 creation timestamp.
updated_at
string
ISO-8601 last-update timestamp.
last_used_at
string
ISO-8601 timestamp of last use, or null if never used.
expires_at
string
ISO-8601 expiry, or null if the credential does not expire.
Errors: 400 validation (CredentialValidationError / CredentialServiceError, including invalid auth_data), 401 / 403 auth, 429 rate limit, 500 on unexpected failure.

List and inspect credentials

List

GET /credentials returns credentials grouped by integration. Supplying integration_name switches to a flat list for that one integration.
integration_name
string
Filter to a single integration and return a flat list instead of the grouped shape.
auth_type
string
Filter by auth type (oauth2, api_key, bearer_token, modulex_key, custom).
limit
integer
default:"100"
Page size. Range 1500.
offset
integer
default:"0"
Number of items to skip. Minimum 0.
curl -s "https://api.modulex.dev/credentials?auth_type=oauth2&limit=50" \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"
The grouped response keys each integration by name; each group lists its credentials with the per-credential fields above plus the integration logo, total_count, and the set of auth_types present:
Grouped response
{
  "integrations": {
    "github": {
      "integration_name": "github",
      "integration_type": "tool",
      "logo": "https://.../github.svg",
      "credentials": [
        {
          "credential_id": "b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef",
          "integration_name": "github",
          "integration_type": "tool",
          "display_name": "Production GitHub",
          "auth_type": "oauth2",
          "is_default": true,
          "created_at": "2026-01-19T12:00:00",
          "updated_at": "2026-01-19T12:00:00",
          "last_used_at": null,
          "expires_at": "2026-02-19T12:00:00",
          "credentials_metadata": null
        }
      ],
      "total_count": 1,
      "auth_types": ["oauth2"]
    }
  },
  "total_credentials": 1,
  "total_integrations": 1,
  "filters": { "auth_type": null }
}
Response fields are snake_case on the wire (for example credentials_metadata, auth_type, display_name). The SDKs convert to their own conventions (integrationName in JS, snake_case in Python).

Get one credential

GET /credentials/{credential_id} returns one credential with masked secrets. Pass include_masked=true to also receive a per-field map of masked values. The secret itself is never returned — only labels and masked fragments (for example xoxb***-end).
include_masked
boolean
default:"false"
When true, add a dict of per-field masked auth values to the response.
curl -s "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef" \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"
The detail response adds organization_id, created_by, created_by_email, and auth_data_masked (a label for OAuth2/managed keys, or a masked secret for API-key and bearer credentials). Errors: 404 not found, 403 access denied, 400 service error.

Test a credential

ModuleX can validate a credential against the integration’s declared test endpoint.
  • POST /credentials/test-temporary validates an unsaved credential before you store it (used by the test-before-save step in the app). Body: integration_name, auth_type (api_key / bearer_token / oauth2), auth_data.
  • POST /credentials/{credential_id}/test validates a credential you have already saved. A credential past its expires_at returns is_valid: false with Credential has expired.
If the integration declares no test endpoint, the test returns is_valid: true with a test_method of none or basic and a “no test endpoint” message.
# Validate before saving
curl -X POST https://api.modulex.dev/credentials/test-temporary \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" \
  -H "Content-Type: application/json" \
  -d '{
    "integration_name": "tavily",
    "auth_type": "api_key",
    "auth_data": { "api_key": "tvly-your-key" }
  }'

# Test an existing credential
curl -X POST "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef/test" \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"
test-temporary response
{
  "is_valid": true,
  "message": "Credential is valid for tavily (test cost: minimal)",
  "tested_at": "2026-01-19T12:00:00",
  "test_method": "api_call",
  "integration_name": "tavily",
  "auth_type": "api_key",
  "test_endpoint": "https://api.tavily.com/search",
  "status_code": 200,
  "cost_level": "minimal"
}
is_valid
boolean
Whether the credential passed validation.
message
string
Human-readable result detail.
test_method
string
How validation ran: api_call, basic, or none.
tested_at
string
ISO-8601 timestamp of the test.
Errors: test-temporary wraps any failure as 500 (Failed to test credential: ...). The saved-credential test returns 404 / 403 for missing or forbidden credentials.

Set a default credential

When an integration has more than one credential, ModuleX resolves which one to use in this precedence order: an explicitly requested credential_id, then the credential marked default, then the most recent valid user credential, then the most recent valid ModuleX-managed (modulex_key) credential. POST /credentials/{credential_id}/set-default marks a credential as the default and unsets any prior default for the same integration.
curl -X POST "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef/set-default" \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"
You can also update a credential’s label or metadata in place with PUT /credentials/{credential_id} (body: display_name?, metadata? — secrets are not updatable here; to change a secret, rotate).

Rotate a credential

There is no dedicated rotate endpoint. Rotation in ModuleX is a deliberate three-step pattern: create the replacement, promote it to default, then revoke the old one. This keeps the integration usable throughout — the new credential is in place and default before the old secret is removed.
  1. Create a new credential for the same integration with the new secret (POST /credentials).
  2. Promote it with make_default: true on create, or POST /credentials/{new_id}/set-default afterward, so resolution prefers it immediately.
  3. Revoke the old credential with DELETE /credentials/{old_id} once the new one is verified.
# 1. Create the replacement as the new default
NEW_ID=$(curl -s -X POST https://api.modulex.dev/credentials \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60" \
  -H "Content-Type: application/json" \
  -d '{"integration_name":"slack","display_name":"Production Slack (rotated)","make_default":true,"auth_data":{"api_key":"xoxb-new-token"}}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['credential_id'])")

# 2. (Optional) verify the new credential
curl -X POST "https://api.modulex.dev/credentials/$NEW_ID/test" \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"

# 3. Revoke the old credential
curl -X DELETE "https://api.modulex.dev/credentials/OLD_CREDENTIAL_ID" \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"
The manual OAuth2 token-refresh endpoint (POST /credentials/{credential_id}/oauth2/refresh) and the app’s refreshOAuth2 action are known to be broken and must not be relied on to rotate or refresh OAuth2 credentials. To refresh an expired OAuth2 connection, reconnect the integration through the OAuth connect flow (which creates a fresh credential), then revoke the stale one. ModuleX also refreshes OAuth2 tokens automatically at execution time when a token is within 5 minutes of expiry, so most refresh happens without any manual step. See Known limitations.

Revoke a credential

Revoking deletes the credential permanently. DELETE /credentials/{credential_id} returns 204 No Content; in the app, open the credential’s detail panel and choose Delete.
curl -X DELETE "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef" \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"
Deletion is permanent — there is no soft-delete or undo. If the deleted credential was the default for its integration, any node or agent that relied on default resolution will fall through to the next valid credential, or fail with no credential found if none remains. Rotate (create the replacement first) instead of deleting in place for production integrations.
Errors: 404 not found, 403 access denied, 400 service error.

Per-organization scope and isolation

Every credential belongs to exactly one organization, and the active organization is fixed by the X-Organization-ID header on each request — it is never read from the body. The practical consequences:
  • No cross-org access. A credential created in one organization cannot be listed, fetched, tested, or used from another. Switching organizations changes the credential set entirely.
  • Encryption is org-scoped. Each credential’s secret is encrypted with a key derived from the organization ID and the credential ID, so ciphertext cannot be reused across credentials or organizations. See Data security & encryption.
  • Owner/admin only. Because credentials are organization-wide, only owners and admins can create, change, or revoke them. See Roles & permissions.
  • Resolution stays in-org. When a tool or LLM node runs, ModuleX resolves the credential within the same organization using the precedence in Set a default credential.
🎬 MEDIA PLACEHOLDER · MX-MEDIA-4031 · [IMAGE] [IMAGE_DESCRIPTION]: Diagram showing two organizations, each with its own isolated set of integration credentials, with the X-Organization-ID header selecting the active org’s credential scope. [IMAGE_DETAILS]: Two side-by-side org boxes (Org A, Org B), each containing distinct credential records for the same integrations (e.g. Slack, GitHub). An incoming API request labeled X-Organization-ID: Org A routes only to Org A’s credentials; a dashed line to Org B is crossed out. Note “encrypted per (org, credential)”. Clean, light mode, 16:9.

Auditing credential changes

GET /credentials/{credential_id}/audit returns the change history for a credential from the unified audit log. Logged operations include CREDENTIAL_CREATED, CREDENTIAL_UPDATED, CREDENTIAL_DELETED, CREDENTIAL_ROTATED, CREDENTIAL_REVOKED, CREDENTIAL_ACTIVATED, CREDENTIAL_DEACTIVATED, and MCP_DISCOVERY_REFRESHED.
limit
integer
default:"100"
Page size. Range 1500.
offset
integer
default:"0"
Number of items to skip. Minimum 0.
curl -s "https://api.modulex.dev/credentials/b3f1c2d4-aa11-4e55-8c90-12ab34cd56ef/audit?limit=50" \
  -H "Authorization: Bearer mx_live_your_api_key" \
  -H "X-Organization-ID: 9a1f0c7e-2b44-4f1a-9c3d-7e5b2a1d8f60"
A usage-statistics endpoint (GET /credentials/{credential_id}/usage) and the audit response are documented in the API reference, but both have known field-shape issues today — treat their exact response fields as TBD until verified against a live response. See Known limitations.

Errors

Credential endpoints return the standard {"detail": <string>} envelope (the dict form only for the rate-limit 429). There is no DenialEnvelope and no 402 on these routes.
StatusMeaning
200Read, update, set-default, or test succeeded
201Credential created
204Credential deleted (revoked)
400Missing X-Organization-ID, invalid auth_data / auth_type, or service validation error
401Missing or invalid auth token
403Not a member, or not owner/admin
404Credential not found in this organization
429Org-class API rate limit (fail-open); see X-RateLimit-* and Retry-After headers
500Unexpected error (and the test-temporary failure wrapper)
For the full error model across all surfaces — including the three error-envelope shapes and the billing DenialEnvelope on gated endpoints — see Errors & status codes.

API overview

Request lifecycle, base URLs, and how every operation is shown three ways.

Authentication & credentials

The auth-type variants and the OAuth2 (PKCE) connect flow.

Credentials & OAuth2

How ModuleX stores and resolves credentials, conceptually.

Roles & permissions

Which actions require an owner or admin role.