Skip to main content
ModuleX is multi-tenant. Identity and organization are two separate axes on every request: your credential says who you are, and the X-Organization-ID header says which organization the request runs against. This page is the reference for how that header is resolved, validated, and enforced — including org-scoped API keys and the realtime handshake equivalent. For the credentials themselves (API keys versus the Clerk session token), see Auth model: JWT vs API key. For the conceptual model of organizations and membership, see Organizations, roles & membership.

The two axes

A single credential can belong to a user who is a member of several organizations. The credential never picks the organization for you — you select it per request. Most resources are organization-scoped: workflows, runs, knowledge bases, credentials, integrations, the AI Composer, the Assistant, schedules, and organization settings. Identity-only endpoints — for example GET /auth/me and GET /auth/me/organizations — do not require the org header.
The header name is X-Organization-ID with a capital ID. HTTP header lookup is case-insensitive on the wire, but quote this exact casing in code and docs. Response bodies use snake_case (organization_ids, current_organization_id, primary_organization_id).

Sending the header

The header value is the organization’s id (a UUID). Add it to any org-scoped request alongside your credential:
Org-scoped request
Find the organizations you belong to — and their ids — with GET /auth/me/organizations. The response carries an organizations array and a total count; use each organization’s id as the header value.

How the header is resolved and enforced

On every org-scoped route the backend runs the same membership check before your handler executes. It reads the header, validates the credential’s org scope, confirms membership, sets the org context on the request, and applies an org-keyed rate limit. The order matters because it determines which error you get.
1

Read the header

The backend reads X-Organization-ID from the request. If it is missing, the request is rejected with 400 and detail X-Organization-ID header is required — before any membership or billing logic runs.
2

Check the user is active

An inactive user is rejected with 403 Inactive user.
3

Enforce API-key org scope

If the credential is an API key that was scoped to a specific organization at creation, the header must name that same organization. A mismatch returns 403 API key is scoped to a different organization. Keys with no scope skip this check.
4

Confirm membership and resolve the role

The backend looks up your role in the named organization. If you are not a member, it returns 403 User is not a member of organization {id}. On success it sets current_organization_id and current_organization_role on the request context (plus a back-compat organization_id alias).
5

Apply the org-class rate limit

An organization-keyed api-class rate limit is checked. It covers both Clerk-JWT and API-key traffic uniformly and fails open on any internal error. Exceeding it returns 429 — see Rate limiting.

Error responses

All org-context failures use the standard FastAPI envelope, {"detail": "<message>"}, except the org rate-limit denial, whose detail is a structured object. See Errors & status codes for the full envelope reference.
400 — missing org header
403 — not a member
A missing org header is a 400 (the header is required and absent), not a 403 (membership/permission). A 403 means the header was present but you may not act in that organization. Reserve 404 for the resource not existing within the org you named.
The flat billing-gate DenialEnvelope ({code, layer, key, current, limit, reason}, returned as 402 / 403 / 429) is a separate mechanism that applies only to run and managed-usage surfaces — workflow runs, the Composer, the Assistant, and managed knowledge — never to plain CRUD or org-settings routes. Org-context failures above are not billing denials. See Usage gating & limits.

Per-request override

The organization is not pinned to your client. You choose it per request, with a clear precedence: a value passed on the call wins over the client default. Both SDKs resolve the per-request value first and fall back to the client default. When neither is set, the header is omitted entirely — which is correct for identity-only endpoints and a 400 on org-scoped ones.
Environment-variable fallback for the org id differs between the SDKs. The Python SDK reads MODULEX_ORGANIZATION_ID (and MODULEX_API_KEY / MODULEX_BASE_URL) when the matching argument is omitted. The JavaScript SDK has no such fallback — pass organizationId to the constructor or per call. Any process.env.* in a JS example is your own caller code, not SDK behavior.

Header org scope vs. body organization_id

Two different things share the word “organization”, and conflating them is the most common org-context mistake.

X-Organization-ID (header)

The organization you are acting in for this request. Resolved and enforced per the steps above. Applies to nearly every org-scoped endpoint.

organization_id (request body)

On a few endpoints (for example, creating an org-scoped API key), a body field that sets which organization owns the new resource. It is independent of the header.
“Which org owns this new resource” (organization_id in the body) is not the same as “which org am I acting in” (X-Organization-ID in the header). They can name different organizations on the same request.

Org-scoped API keys

An API key is owned by a user and can optionally be scoped to one organization when you create it. Scope is set with the organization_id field in the create body and is independent of the X-Organization-ID header you send on later requests.
string | null
default:"null"
The organization to scope the key to. The caller must be an active member of that organization. When null (the default), the key works across all of your organizations, and you choose the organization per request with X-Organization-ID.
Create an org-scoped key
How the scope behaves at request time:
Scoping a key does not make the header optional. Even a single-org key must send X-Organization-ID on org-scoped routes; the scope adds an equality check, it does not supply a default. Pair a scoped key with a tight rate_limit_per_minute so a leaked automation key has limited blast radius.
The create response returns the scope it was given:
string | null
The organization the key is scoped to, or null for all of your organizations.
For the full key lifecycle — format, creation, limits, and revocation — see Authentication.

Roles within an organization

Membership alone is enough for most reads and writes, but some surfaces require a privileged role. The live organization roles are owner and admin.
The member role is retired and is not a current first-class role. Document and design against owner / admin only. Legacy member rows may still exist and the role name can still appear in some list filters and in the realtime read-gate, but the REST edge rejects it.
The AI Composer, the Assistant, and the organization-settings and member-management endpoints require owner or admin. A membership-only call to one of these returns 403. See Roles & permissions for the per-endpoint matrix and Organizations, roles & membership for the concept.

Org context in realtime

The Socket.io collaboration server carries organization scope as a handshake field, not an HTTP header. The handshake auth object must include both token (a Clerk JWT) and organizationId; either one missing rejects the entire connection.
Socket.io handshake
On a successful handshake the socket joins the org:<organizationId> room and scopes all collaboration traffic to that organization.
Realtime membership is cached server-side with a short TTL, so a role or membership change can take up to a few minutes to take effect on an open socket. The REST surface re-checks membership on every request and has no such lag.
See Socket.io collaboration events for the event reference and Realtime overview & event taxonomy for both realtime planes.

Practical guidance

  • Always send the header on org-scoped routes. When in doubt, send it — only identity endpoints (/auth/me, /auth/me/organizations) are safe without it.
  • Discover ids dynamically. Resolve organization ids with GET /auth/me/organizations rather than hard-coding them, so the code keeps working as a user joins or leaves organizations.
  • Match the key scope to the deployment. Use an unscoped key for tooling that spans organizations, and a scoped key for automation that should only ever touch one organization.
  • Distinguish the two organization_ids. The header sets the acting org; a body organization_id sets resource ownership.
  • Plan for the realtime lag. When you change someone’s membership or role, expect REST to reflect it immediately and open realtime sockets to catch up within a few minutes.

Next steps

Organizations, roles & membership

The conceptual model behind org context.

Authentication

Credentials, key format, and the full org-scope flow.

Roles & permissions

Which actions require owner or admin.

Auth model: JWT vs API key

How the two credential paths differ.