# Authentication & Tenancy This page covers the two things every embed call depends on: how your customers map to Monad **teams**, and how your backend turns a long-lived API key into the short-lived **session token** the browser uses. Understand the tenancy model first — the team id shows up in almost every API call. ## Tenancy and teams ### Vocabulary | Term | What it means | | --- | --- | | **Team (organization)** | A Monad sub-org, nested underneath your (the vendor's) root org, that owns a customer's connectors and pipelines. Identified by an `organizationId`. | | **Tenant** | Your customer. Your backend maps a tenant to its Monad team. | | **Session token** | A short-lived (about 30 minutes), team-scoped credential minted from your API key. The browser passes it into the iframe. | ### The recommended model: one team per account Give each account on your side its own Monad team. "Account" here means whatever your isolation boundary is — usually one of your customers. Several users inside the same customer still share that one customer's team. You hold a single Monad API key. Under it you create one team per account. Your backend owns the tenant-to-team mapping, which in the simplest case is a column on your accounts table. The browser only ever names a tenant; your backend resolves it to the Monad team id and never sends that id to the client. :::warning Resolve, don't trust Don't accept an org id from the browser as authority. Resolve it server-side from the tenant. The same rule, applied to pipeline and connector ids, comes back as "resolve-then-act" in [Connector Lifecycle](/sdks/embed/lifecycle). ::: ### Why one team per account The team is Monad's isolation boundary between your accounts, and the team id is load-bearing: - The session token is scoped to one team. The API rejects any call outside its `org` claim, so a token minted for one account can't touch another's data. - Every catalog and pipeline call is org-scoped: `/v1/{org}/inputs`, `/v2/{org}/pipelines/`, and so on. Listing connectors returns everything in that team and nothing outside it. One team per account means each customer's connectors, pipelines, and secrets are isolated by the API itself, and the short-lived token you hand the browser can only ever reach that one customer's resources. Wherever you see `org` or `organizationId` in these docs, read it as "the team this tenant maps to." ### The risk of sharing a team across accounts You *can* put multiple accounts on one team, but that erases the boundary the model depends on: - **No isolation.** A token minted for account A can read and act on account B's connectors and pipelines — to the API they're the same org. - **Listing leaks.** `GET /v1/{org}/{kind}s` returns every connector in the team, exposing one account's names and ids to another. - **Mutations can hit the wrong account.** Build, pause, and delete act on ids in the shared team. Resolve-then-act helps, but can't restore a boundary a separate team would give you. - **Wider blast radius.** One leaked token or bad deploy affects every account on the team, not one. - **Messy offboarding.** Deleting an account means hand-picking its resources out of a shared team instead of just deleting the team. Sharing is only fine when the accounts aren't separate security principals (say, environments of one internal app). Distinct customers get distinct teams. ### Provisioning teams Create a team per account with `POST /v3/{yourOrgId}/organizations` (body `{ name, friendly_name }`), and store the returned id against your tenant when you onboard them. If you're embedding for a single account, this collapses to one team you resolve to server-side — no mapping table needed. ## Authentication Authentication is a chain: **API key** on your server → **session token** in the browser → into the iframe. ### The API key A long-lived JWT you issue in Monad (Settings → API Keys, the `eyJ…` value). It authenticates backend calls as `Authorization: ApiKey ` and stays on your server, for the reason in the [architecture overview](/sdks/embed/index#architecture). See the [API Keys guide](/guides/api-keys) for issuing and rotating keys. ### Minting a session token Your backend trades the key for a token scoped to one team, then hands it to the browser. It's a plain `POST /v3/sessions`: ```ts const r = await fetch('https://app.monad.com/api/v3/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `ApiKey ${process.env.MONAD_API_KEY}`, }, body: JSON.stringify({ organization_id: org, ttl_seconds: 1800 }), // 30 min }); const session = await r.json(); // { session_token, expires_at } ``` Response: ```json { "session_token": "eyJhbGc…", "expires_at": "2026-07-07T18:30:00Z" } ``` ### Passing it to the iframe The frontend fetches a token from **your** endpoint, not Monad directly, and passes it to `createConnectorFrame`. Watch the casing: the mint response is snake_case (`session_token`) but `createConnectorFrame` wants camelCase (`sessionToken`). Remap at the call site: ```ts const frame = createConnectorFrame({ container: '#monad-mount', sessionToken: s.session_token, // remap snake_case → camelCase organizationId: s.organizationId, kind: 'input', typeId: 'aws-cloudtrail', }); ``` ### Scope, lifetime, and reuse - **Scope is one team.** The API rejects any call outside the token's `org` claim. - **Lifetime is whatever `ttl_seconds` you set.** Keep it short; about 30 minutes is typical. - **A token is scoped to a team and a time window, not to a single iframe.** You can reuse one token across several mounts while it's valid. Mint per tenant, not per frame. - **The form reads the token once at mount.** An expired token means minting a fresh one and remounting. Re-mint on expiry or on a `401`. ## Next - [Connector Lifecycle](/sdks/embed/lifecycle) — build pipelines and manage integrations. - [Best Practices](/sdks/embed/best-practices) — default inputs/outputs and deployment. ## Support For additional support contact support@monad.com