This is the full developer documentation for Scalekit --- # DOCUMENT BOUNDARY --- # Scalekit Docs > Scalekit AgentKit gives AI agents per-user OAuth, a token vault and 30,000+ ready-to-call tools. Scalekit also adds SSO, SCIM and login to SaaS apps. # Let your AI agent act in your users' apps AgentKit runs each user's OAuth flow, stores their tokens in a vault and executes tool calls as that user in apps like Gmail, Slack and Salesforce, from the SDK, the REST API or a Virtual MCP server. [Start the quickstart](/agentkit/quickstart/)[What is AgentKit](/agentkit/overview/) * Python ```python # "gmail" is your connection name in the dashboard link = actions.get_authorization_link( connection_name="gmail", identifier="user_123", ).link # The user approves once. Scalekit keeps the tokens. result = actions.execute_tool( tool_name="gmail_fetch_mails", tool_input={"max_results": 5}, connection_name="gmail", identifier="user_123", ) print(result.data) ``` * Node.js ```ts // 'gmail' is your connection name in the dashboard const { link } = await actions.getAuthorizationLink({ connectionName: 'gmail', identifier: 'user_123', }); // The user approves once. Scalekit keeps the tokens. const result = await actions.executeTool({ toolName: 'gmail_fetch_mails', toolInput: { max_results: 5 }, connector: 'gmail', identifier: 'user_123', }); console.log(result.data); ``` * cURL ```sh # $TOKEN: an access token from /oauth/token API="$SCALEKIT_ENVIRONMENT_URL/api/v1" curl -sS "$API/connected_accounts/magic_link" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"connector": "gmail", "identifier": "user_123"}' # The user approves once. Scalekit keeps the tokens. curl -sS "$API/execute_tool" \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"tool_name": "gmail_fetch_mails", "connector": "gmail", "identifier": "user_123", "params": {"max_results": 5}}' ``` ## Try it Run the CLI ```sh npx @scalekit-inc/cli setup ``` * Claude Code * Cursor * GitHub Copilot * Codex Quickstart with your coding agent Copy one prompt. Your agent installs the SDK, authorizes a user and makes a tool call. Build with Scalekit.Load https\://docs.scalekit.com/llms.txt before writing any Scalekit code. The installed authstack skill guides APIs, SDK calls, and connection names; where it and these docs differ, follow the docs. Credentials live in the environment, never in source.Copy ## Find a connector 33,000+ ready-to-call tools Search connectors and tools Search by app or task, like "send a Slack message" Search * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/gmail.svg)Gmail](/agentkit/connectors/gmail/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/slack.svg)Slack](/agentkit/connectors/slack/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/sales_force.svg)Salesforce](/agentkit/connectors/salesforce/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/hub_spot.svg)HubSpot](/agentkit/connectors/hubspot/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/google_drive.svg)Google Drive](/agentkit/connectors/googledrive/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/notion.svg)Notion](/agentkit/connectors/notion/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/jira.svg)Jira](/agentkit/connectors/jira/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/github.png)GitHub](/agentkit/connectors/github/) * [![](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/outlook.svg)Outlook](/agentkit/connectors/outlook/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/linear.svg)Linear](/agentkit/connectors/linear/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/zendesk.svg)Zendesk](/agentkit/connectors/zendesk/) * [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/snowflake.svg)Snowflake](/agentkit/connectors/snowflake/) * [All 614 →](/agentkit/connectors/) ## Know what you need? Every AgentKit section, from connecting users to going to production, is mapped on one page. [See all AgentKit sections →](/agentkit/overview/#all-agentkit-sections) ## Adding login, SSO, SCIM or MCP Auth to your SaaS app? That's Auth for SaaS (SaaSKit), Scalekit's auth for B2B apps. [Auth for SaaS quickstart](/authenticate/fsa/quickstart/) For AI agents Every page is available as markdown: add `.md` to its URL. Start from [/llms.txt](/llms.txt) or [/agentkit/llms.txt](/agentkit/llms.txt). Pricing Free to start. Current plans are on [scalekit.com/pricing](https://www.scalekit.com/pricing). --- # DOCUMENT BOUNDARY --- # What is Agent Gateway > Agent Gateway gives your team one MCP URL for Claude, ChatGPT, Cursor and other AI tools, with each person signed in and access set per connection. **Agent Gateway gives your team’s AI tools governed access to the apps your company uses.** You enable it in the Scalekit dashboard and get one MCP endpoint URL. Each person adds that URL to their AI client, such as Claude, ChatGPT, Cursor or VS Code, and signs in with their own account. The tools they see come from the connections you add, filtered by the groups they belong to. Available on request Agent Gateway is available on request. [Contact us](/support/contact-us/) to enable it for your workspace. Agent Gateway is for the admins who give their own team access to AI tools, such as IT or security teams. Everything is set up in the dashboard, with no code. Building an agent into your own product for your customers? See [AgentKit](/agentkit/overview/). ## Choose AgentKit or Agent Gateway [Section titled “Choose AgentKit or Agent Gateway”](#choose-agentkit-or-agent-gateway) Both products use the same [connectors](/agentkit/connectors/). They differ in who builds the agent and who uses it. | | AgentKit | Agent Gateway | | --------------------- | ---------------------------------------------------------------- | --------------------------------------------------------------- | | You want to | Let **your product’s** agent act in **your customers’** apps | Give **your team’s** AI tools access to **your company’s** apps | | Who sets it up | Developers, in code | Admins, in the dashboard | | Who signs in | Your customers, through your app | Your team, with their own accounts | | The agent | One you build and host | Existing AI clients, such as Claude, ChatGPT, Cursor or VS Code | | How tools are reached | SDK, REST API or a [Virtual MCP server](/agentkit/mcp/overview/) | One MCP endpoint URL for the whole team | | Access control | Your code chooses the tools for each call | Groups and per-connection policies | **Use AgentKit** when you’re building a product, such as an inbox assistant or a sales copilot, and your users connect their accounts inside your app. Start with the [AgentKit quickstart](/agentkit/quickstart/). **Use Agent Gateway** when people on your team already use AI tools and need them to read and act in company apps, with you deciding who can use which tools. Protecting an MCP server you built yourself? That’s [MCP Auth](/authenticate/mcp/quickstart/). ## How it works [Section titled “How it works”](#how-it-works) 1. **You enable the Gateway** in an environment. Scalekit creates its MCP endpoint and adds you as its first user. 2. **You add connections**, such as Gmail, Outlook, Slack or GitHub. These are the same [connections](/agentkit/concepts/) AgentKit uses, set up once for the environment. 3. **Each person connects their AI client** to the MCP endpoint URL and signs in. The client asks for their consent the first time it acts for them. 4. **Each person connects their own app accounts.** The first time a tool needs an app the person hasn’t connected, the AI client gives them a link to connect it. 5. **Groups decide which tools each person sees.** On each connection you choose which groups can use it, and whether they get every tool or only some. Tool calls through the Gateway appear under **Overview** and **Logs** in the dashboard. ## What you set up in the dashboard [Section titled “What you set up in the dashboard”](#what-you-set-up-in-the-dashboard) | Task | Where | Guide | | ---------------------------------------- | -------------------------------------------------- | ----------------------------------------------------------------- | | Enable the Gateway and get its URL | **Getting Started** | [Quickstart](/agentkit/agent-gateway/quickstart/) | | Connect Claude, ChatGPT, Cursor and more | The URL from **Settings** | [Connect AI clients](/agentkit/agent-gateway/connect-ai-clients/) | | Add users, SSO and SCIM | **Identity** | [Users and access](/agentkit/agent-gateway/users-and-access/) | | Decide who can use which tools | **Groups**, and each connection’s **Policies** tab | [Access policies](/agentkit/agent-gateway/access-policies/) | | Review plans and limits | **Billing** | [Plans and limits](/agentkit/agent-gateway/plans-and-limits/) | ## Next [Section titled “Next”](#next) Enable the Gateway and connect your first AI client in the [Quickstart](/agentkit/agent-gateway/quickstart/). --- # DOCUMENT BOUNDARY --- # When to add a connector > Add your own connector when an app isn't in the AgentKit catalog. Scalekit still handles user authorization, stored credentials and the calls your agent makes. Add your own connector when the API or MCP server you need is not available in Scalekit’s built-in catalog — custom connectors support any SaaS API, partner system, internal API, or remote MCP server while keeping authentication, authorization, and secure API access in Scalekit. Once the connector is created, you use the same flow as other connectors: create a connection, create or fetch a connected account, authorize the user, and perform tool calling. Custom connectors appear alongside built-in connectors when you create a connection in Scalekit: ![Custom connector shown alongside built-in connectors in the connector selection view](/.netlify/images?url=_astro%2Fcustom-provider-in-catalog.BEwx1iKj.png\&w=2596\&h=1138\&dpl=6ae471fc143c9a4c27550463) ## Why add your own connector [Section titled “Why add your own connector”](#why-add-your-own-connector) Adding your own connector lets you: * Extend beyond the built-in connector catalog without inventing a separate auth stack * Bring unsupported SaaS APIs, partner systems, internal APIs, and remote MCP servers into the same secure access model * Reuse connections, connected accounts, and user authorization instead of building one-off auth plumbing * Keep credential handling, authorization, and governed API access centralized in Scalekit * Move from connector definition to live upstream calls through the API proxy (REST) or tool calling (MCP) using the same runtime model as other integrations ## How adding your own connector works [Section titled “How adding your own connector works”](#how-adding-your-own-connector-works) Adding your own connector uses the same model as built-in connectors: 1. Create a connector definition 2. Create a connection in Scalekit Dashboard 3. Create a connected account and authorize the user 4. Call tools — via the API proxy (`actions.request()`) for REST API connectors, or via MCP tool calling for MCP connectors Creating the connector definition tells Scalekit how to authenticate to the upstream API or MCP server. After that, connections, connected accounts, user authorization, and the call runtime work the same way as they do for built-in connectors. ## Next [Section titled “Next”](#next) [Create a connector](/agentkit/bring-your-own-connector/create-connector/)Define how Scalekit authenticates to the app. [Call your connector](/agentkit/bring-your-own-connector/making-tool-calls/)Make API or MCP tool calls through your connector. --- # DOCUMENT BOUNDARY --- # How Virtual MCP servers work > A Virtual MCP server exposes the tools you choose, for one user at a time, to any MCP client. How servers, session tokens and tool calls fit together. A **Virtual MCP server** is a URL that exposes tools you choose, from one or more connections, to any MCP client. You create one server per agent role, such as a support assistant, and all your users share it. Before each run, you mint a **session token** for one user. The agent connects to the server URL with that token and can call only the tools you chose, as that user. Use a Virtual MCP server when your agent already speaks MCP. If you build the agent loop yourself, you can call tools from your code instead: see [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/). Every AgentKit connection also has its own MCP server, with all of that connection’s tools and nothing to create. [Use a connection’s MCP server](/agentkit/mcp/connection-mcp-servers/) compares the two. Session tokens work the same way on both. ## Why use one [Section titled “Why use one”](#why-use-one) * **The agent sees only the tools it needs.** A Gmail connection has dozens of tools. A summarizer needs one. The server exposes only the tools you list, so the agent can’t call the rest. * **Smaller prompts.** Every tool on an MCP server goes into the model’s context. Fewer tools means fewer tokens on every call. * **One definition, isolated users.** All users share the server’s URL. Each run’s session token is bound to one user and that user’s connected accounts, so one user’s run can’t act with another user’s credentials. * **Read-only runs from the same server.** Mint a [read-only token](/agentkit/mcp/read-only-tokens/) and the agent gets only the tools that don’t change anything, without a second server. ## How it works [Section titled “How it works”](#how-it-works) | When | You | What you get | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | | Once per agent role | [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) with the connections and tools the agent needs | A server ID and a fixed `mcp_server_url` | | Before each run | [Check the user’s connected accounts](/agentkit/reference/virtual-mcp-servers/check-connected-accounts-for-a-server/) are `ACTIVE`, then [mint a session token](/agentkit/mcp/session-tokens/) | A bearer token for one user and one server | | During the run | Connect any MCP client to the URL with `Authorization: Bearer ` | The server’s tools, called as that user | ![Create a Virtual MCP server once per agent role, mint a session token for each user's run, and let the MCP client call tools through Scalekit as that user](/d2/docs/agentkit/mcp/overview-0.svg) In the API and SDKs, a Virtual MCP server is called an **MCP configuration**, for example `create_config` in Python and `createConfig` in Node.js. The [Virtual MCP servers API reference](/agentkit/reference/virtual-mcp-servers/) lists every operation. ## Session tokens [Section titled “Session tokens”](#session-tokens) A session token authorizes one user on one server: * It lasts one hour unless you set `expiry`, which must be between 60 seconds and 24 hours. * It can’t be refreshed or extended. Mint a new one instead. Minting a new token doesn’t revoke earlier ones. * It only works on the server it was minted for. Any other server URL returns `401`. * It can be full or read-only. A [read-only token](/agentkit/mcp/read-only-tokens/) sees and calls only the server’s read-only tools. * Check the user’s connected accounts before the run. A tool call fails until the account for its connection is `ACTIVE`. [Mint session tokens](/agentkit/mcp/session-tokens/) covers when to mint for one-off, scheduled, concurrent and long-running agents. ## What the agent sees [Section titled “What the agent sees”](#what-the-agent-sees) `tools/list` returns the tools you chose for the server. Tools from a connection the user hasn’t authorized don’t work until they do. `tools/list` also includes a `connect_account` tool. When the user still needs to connect an app, the agent calls it to get an authorization link, and shows the link to the user. A tool call returns the same output as [`execute_tool`](/agentkit/tools/scalekit-optimized-tools/#read-the-result): the app’s response under `data`. If the app reports an error in a successful (`2xx`) response, that error is in `data`. If the app answers with an error status, the tool call returns an error result (`isError: true`) whose message starts with `tool execution failed`. ## Data flow [Section titled “Data flow”](#data-flow) 1. The MCP client sends a request to the server URL with the session token. 2. Scalekit checks the token against the server and the user, and finds the user’s connected account for the tool’s connection. 3. Scalekit calls the app’s API with that account’s credentials and returns the result to the client. Your agent never receives the user’s OAuth token or API key. Responses pass through to the MCP client and aren’t stored. See [what Scalekit stores](/agentkit/security/#what-scalekit-stores). ## Next [Section titled “Next”](#next) [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/)Choose the tools and get the server URL. [Mint session tokens](/agentkit/mcp/session-tokens/)Authorize a user's run and connect an MCP client. [Use a connection's MCP server](/agentkit/mcp/connection-mcp-servers/)Every tool of one connection, with no server to create. --- # DOCUMENT BOUNDARY --- # What is AgentKit > How AgentKit connects your AI agent to each user's Gmail, Slack, GitHub and other apps, what you can build with it, and what Scalekit handles for you. **AgentKit lets your AI agent act in third-party apps on behalf of each of your users.** A user connects their Gmail, Slack, Salesforce or GitHub account once through an authorization link. Scalekit stores and refreshes their tokens, and your agent calls prebuilt tools such as `gmail_send_message` with that user’s ID. Your code never handles OAuth tokens. 614 [connectors](/agentkit/connectors/) · Python and Node.js SDKs · REST API · Any MCP client New to AgentKit? Start with the [Quickstart](/agentkit/quickstart/). Looking for something specific? See [all AgentKit sections](#all-agentkit-sections). Giving your own team's AI tools access to company apps? That’s [Agent Gateway](/agentkit/agent-gateway/overview/): one MCP URL for Claude, ChatGPT, Cursor and other AI clients, set up in the dashboard. See [Choose AgentKit or Agent Gateway](/agentkit/agent-gateway/overview/#choose-agentkit-or-agent-gateway). ## What you can build [Section titled “What you can build”](#what-you-can-build) * **An inbox assistant** reads, labels and drafts email in each user’s Gmail or Outlook. * **A sales copilot** logs calls and updates deals in Salesforce or HubSpot after every meeting. * **An engineering agent** files issues and reviews pull requests in GitHub, Linear or Jira. ## How it works [Section titled “How it works”](#how-it-works) 1. Once per app1 · You set up a connectionin the dashboard 2. Once per user2 · Your user approves accessauthorization link 3. Scalekit3 · Scalekit stores tokensconnected account is ACTIVE once any user verification passes 4. Every tool call4 · Your agent calls a toolidentifier + tool name Gmail, Slack, GitHub…API call with the user's tokenScalekit adds the token, calls the API and returns JSON to your agent. Steps 1 and 2 happen once. Step 4 happens on every call. Your code never handles an OAuth token. [How AgentKit works](/agentkit/concepts/) covers the full model, every term and every connected-account status. ## Two ways to give your agent tools [Section titled “Two ways to give your agent tools”](#two-ways-to-give-your-agent-tools) | | From your code (SDK) | From an MCP client | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | You get | Tool schemas and `execute_tool` in Python or Node.js | A [Virtual MCP server](/agentkit/mcp/overview/) URL, with a session token for each user | | Works with | [LangChain](/agentkit/examples/langchain/), [Google ADK](/agentkit/examples/google-adk/), [OpenAI](/agentkit/examples/openai/), [Anthropic](/agentkit/examples/anthropic/), [Vercel AI SDK](/agentkit/examples/vercel-ai/), [Mastra](/agentkit/examples/mastra/), your own loop | [Claude Managed Agents](/agentkit/examples/claude-managed-agents/), [CrewAI](/agentkit/examples/crewai/), any MCP client | | Pick it when | You build and host the agent | The agent already exists and speaks MCP | ## What Scalekit handles, and what you build [Section titled “What Scalekit handles, and what you build”](#what-scalekit-handles-and-what-you-build) | Scalekit handles | You build | | --------------------------------------------------- | ---------------------------------------------------------- | | OAuth flows and consent screens for every connector | Pick which apps to connect, once, in the dashboard | | Encrypted token storage and refresh | Pass a stable, hard-to-guess ID for each user | | Tool schemas, tool execution and the API proxy | [Verify users](/agentkit/user-verification/) in production | | Webhooks when an account’s status changes | Choose which tools each agent gets | ## Built for production [Section titled “Built for production”](#built-for-production) * **Compliance**: SOC 2 Type II and ISO 27001 certified. GDPR and CCPA compliant. [HIPAA](https://www.scalekit.com/legal/hipaa) eligible, with a Business Associate Agreement on Enterprise plans. * **Token vault**: every OAuth token and API key is encrypted with a key unique to your environment before it’s stored. That key is wrapped by a master key in Google Cloud KMS, held apart from the database, so a copy of the database alone yields nothing usable. * **Tool call data**: responses pass through to your agent and aren’t stored. Tool call logs record the call’s metadata, not its content. See [what Scalekit stores](/agentkit/security/#what-scalekit-stores). * **Encryption**: AES-256 at rest and TLS 1.3 in transit. [Bring your own key](/agentkit/encryption-keys/) from your Google Cloud KMS; revoking it makes the stored data unusable to Scalekit. * **Data residency**: separate [US and EU deployments](/agentkit/environments/#choose-a-region) with no shared state. The EU region runs in Frankfurt. * **Self-hosted**: with an enterprise license, run AgentKit [in your own infrastructure](/agentkit/self-hosted/), on Kubernetes or Docker. Once installed, it has no connection to Scalekit’s infrastructure. [Security and compliance](/agentkit/security/) covers how credentials are stored, what Scalekit keeps, and what your app is responsible for. Audit reports and penetration test summaries are available under NDA from the [Trust Center](https://www.scalekit.com/trust-center). Read the [Data Processing Agreement](https://www.scalekit.com/legal/data-processing-agreement), which lists sub-processors. See [pricing](https://www.scalekit.com/pricing) for plans. ## All AgentKit sections [Section titled “All AgentKit sections”](#all-agentkit-sections) Every part of the AgentKit docs, in the order you build. Each section matches a group in the sidebar. ### Start Learn what AgentKit does and make your first tool call. * [What is AgentKit](/agentkit/overview/) * [Quickstart](/agentkit/quickstart/) * [How AgentKit works](/agentkit/concepts/) ### Connect users Set up an app connection, then have each user authorize it. * [Set up a connection](/agentkit/connections/) * [Authorize a user](/agentkit/tools/authorize/) * [Hosted Connect Widget](/agentkit/hosted-connect-widget/) * [Verify users](/agentkit/user-verification/) * [Manage connected accounts](/agentkit/connected-accounts/) * [Account events](/agentkit/account-events/) ### Call tools with the SDK Run prebuilt tools, or call any API endpoint as the user. * [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/) * [Call any API](/agentkit/tools/custom-tools/) ### Call tools over MCP Give any MCP client a per-user set of tools. * [How Virtual MCP servers work](/agentkit/mcp/overview/) * [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) * [Connection MCP servers](/agentkit/mcp/connection-mcp-servers/) * [Mint session tokens](/agentkit/mcp/session-tokens/) * [Read-only tokens](/agentkit/mcp/read-only-tokens/) * [Connect any MCP client](/agentkit/mcp/connect-any-client/) ### Triggers (Beta) Have your agent act when something changes in a connected app. * [How triggers work](/agentkit/triggers/overview/) * [Set up a trigger](/agentkit/triggers/set-up/) * [Receive events](/agentkit/triggers/receive-events/) * [Manage subscriptions](/agentkit/triggers/manage/) ### Agent Gateway Give your team's AI tools one governed MCP URL for your apps. * [What is Agent Gateway](/agentkit/agent-gateway/overview/) * [Quickstart](/agentkit/agent-gateway/quickstart/) * [Connect AI clients](/agentkit/agent-gateway/connect-ai-clients/) * [Users and access](/agentkit/agent-gateway/users-and-access/) * [Access policies](/agentkit/agent-gateway/access-policies/) * [Plans and limits](/agentkit/agent-gateway/plans-and-limits/) ### Frameworks Wire AgentKit into your agent framework. * [Choose a framework](/agentkit/examples/) * [LangChain](/agentkit/examples/langchain/) * [LangGraph](/agentkit/examples/langgraph/) * [Google ADK](/agentkit/examples/google-adk/) * [OpenAI](/agentkit/examples/openai/) * [OpenAI Agents SDK](/agentkit/examples/openai-agents-sdk/) * [Anthropic](/agentkit/examples/anthropic/) * [Vercel AI SDK](/agentkit/examples/vercel-ai/) * [Mastra](/agentkit/examples/mastra/) * [CrewAI](/agentkit/examples/crewai/) * [Claude Managed Agents](/agentkit/examples/claude-managed-agents/) ### Agent platforms Use AgentKit from coding agents and agent platforms. * [Coding agents](/agentkit/coding-agents/) * [Hermes](/agentkit/hermes/) ### Add your own connector Connect an app or internal API that isn't in the catalog. * [When to add one](/agentkit/bring-your-own-connector/overview/) * [Create a connector](/agentkit/bring-your-own-connector/create-connector/) * [Call your connector](/agentkit/bring-your-own-connector/making-tool-calls/) ### Manage your workspace Environments, API credentials, team members and billing. * [Environments and regions](/agentkit/environments/) * [API credentials](/agentkit/api-credentials/) * [Team members and roles](/agentkit/team-members/) * [Billing](/agentkit/billing/) * [Enterprise features](/agentkit/enterprise-features/) * [Log streaming](/agentkit/log-streaming/) * [Encryption keys](/agentkit/encryption-keys/) * [Pricing](https://www.scalekit.com/pricing) ### Go to production Check everything before launch, and harden your setup. * [Launch checklist](/agentkit/advanced/launch-checklist/) * [Security and compliance](/agentkit/security/) * [Brand the widget](/agentkit/advanced/branding/) * [Use your own OAuth app](/agentkit/advanced/bring-your-own-oauth/) * [Custom domain](/agentkit/advanced/custom-domain/) * [Self-hosted deployment](/agentkit/self-hosted/) ### Help Fix common errors, or move an existing integration over. * [Troubleshooting](/agentkit/troubleshooting/) * [Migrate from Composio](/agentkit/advanced/migrate-from-composio/) ### Connectors Every app AgentKit connects to, with its tools. * [Browse connectors](/agentkit/connectors/) ### Reference Every SDK method, REST endpoint and webhook event. * [API reference](/agentkit/reference/) --- # DOCUMENT BOUNDARY --- # How triggers work > How AgentKit triggers notify your app when something changes in a user's connected app: webhook and polling triggers, and how events are delivered. A **trigger** tells your app when something happens in an app your users have connected, such as a new lead in Salesforce, a new activity in Pipedrive or a message that mentions your app in Slack. Scalekit sends each event to your server as a signed HTTP request, so your agent can act on it without polling the app itself. This page explains the model: the two kinds of triggers, what you subscribe, and how events are delivered. To start receiving events, go to [Set up a trigger](/agentkit/triggers/set-up/). Triggers are in beta [Contact us](/support/contact-us/) to turn triggers on for your environment. ## How it works [Section titled “How it works”](#how-it-works) You set triggers up in the dashboard, and your server receives the events: 1. **Add a delivery endpoint.** A URL on your server that receives trigger events. One endpoint can receive events from every connection in the environment, or only from the connections you choose. 2. **Subscribe.** On a [connection](/agentkit/concepts/)’s **Triggers** tab, subscribe a connected account, or the whole connection, to a trigger such as **Lead created**. Each subscription can set filters, such as a lead source or a Slack channel, and for polling triggers how often to check. 3. **Scalekit detects the change.** It either receives the event from the app or finds it by checking the app on a schedule. Which one depends on the trigger: see [Two kinds of triggers](#two-kinds-of-triggers). 4. **Your server receives a signed request.** Scalekit sends one `POST` per event to each delivery endpoint that covers the connection. Your server verifies the signature, returns a `2xx` status and hands the event to your agent. ## Two kinds of triggers [Section titled “Two kinds of triggers”](#two-kinds-of-triggers) Each trigger is either a webhook trigger or a polling trigger. The **Triggers** tab tags each one **Webhook** or **Polled**, and shows whether you subscribe it **Per user** or **Per connection**. | | Webhook triggers | Polling triggers | | ---------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | | How Scalekit learns about a change | The app sends the event to Scalekit | Scalekit checks the app on an interval and sends one event per new or changed record | | How fast events arrive | Within seconds | Up to one interval after the change, plus a few seconds | | What you subscribe | Usually the whole connection, once | Each connected account, separately. The account must be `ACTIVE` | | Setup in the app | Register a Scalekit URL and copy the app’s signing secret into Scalekit | None | | Examples | Slack **App mentioned**, **Message posted** | Salesforce **Lead created**, Pipedrive **Activity created or updated** | ### Webhook triggers [Section titled “Webhook triggers”](#webhook-triggers) The app pushes events to a webhook URL that Scalekit generates for the connection. You register that URL in your own app’s settings at the provider and paste the provider’s signing secret into Scalekit, so it can reject forged requests. Each connector’s page shows where these settings are, as in [Slack triggers](/agentkit/connectors/slack/#triggers). The provider sends every event your app at the provider subscribes to. Scalekit delivers only the events that match a trigger subscription and its filters, and drops the rest, so subscribe your app only to the events your triggers use. A connection-wide trigger fires once per event for the whole connection, not once per user. The event names the connection but no connected account. Your app decides which of its users, if any, should act on it. ### Polling triggers [Section titled “Polling triggers”](#polling-triggers) Scalekit checks the app for each subscribed connected account, using that account’s access. Each trigger has its own default and minimum interval, which the dashboard shows when you subscribe. * **Polling starts when you subscribe.** Older records aren’t sent. A record created or changed shortly before you subscribed can still arrive in the first check. * **One event per record per check.** A record that changes several times between two checks produces one event, not one per change. * **The interval can be longer than you asked for.** When an environment has many polling subscriptions, Scalekit can check less often to share capacity. The subscription then shows **Throttled**, and events still arrive. * **Polling pauses when the account’s access stops working.** If a user revokes access or the token can’t be refreshed, the subscription shows **Credential issue — paused**. It resumes on its own once the user reconnects the account. ## Delivery [Section titled “Delivery”](#delivery) Every event reaches your endpoint as an HTTP `POST` with a JSON body, signed with the endpoint’s signing secret. The body has the same shape for both kinds of triggers: which trigger fired, the connection, the connected account for per-user triggers, and the app’s own record in `payload`. [Receive trigger events](/agentkit/triggers/receive-events/) shows how to verify the request and explains each field. * **At least once.** An event can arrive more than once, for example after a retry. Each event carries a `dedupe_key`; skip one you’ve already handled. * **Retried until your server accepts it.** A delivery that doesn’t get a `2xx` response is retried with increasing delays. * **Not in order.** Events can arrive out of order. Use `occurred_at` when order matters. ## Next [Section titled “Next”](#next) [Set up a trigger](/agentkit/triggers/set-up/)Add a delivery endpoint and subscribe to a polling or webhook trigger. [Receive trigger events](/agentkit/triggers/receive-events/)Verify each request in Python or Node.js and handle repeats. --- # DOCUMENT BOUNDARY --- # Role based access control (RBAC) > Control what authenticated users can access in your application based on their roles and permissions When users access features in your application, your app needs to control what actions they can perform. These permissions might be set by your app as defaults or by organization administrators. For example, in a project management application, you can allow some users to create projects while restricting others to only view existing projects. Role-based access control (RBAC) provides the framework to implement these permissions systematically. After users authenticate through Scalekit, your application receives an access token containing their roles and permissions. Use this token to make authorization decisions and control access to features and resources. ![Authorization overview: after sign-up or login, the app reads roles and permissions from the token to allow or deny actions](/d2/docs/authenticate/authz/overview-0.svg) Access tokens contain two key components for authorization: **Roles** group related permissions together and define what users can do in your system. Common examples include Admin, Manager, Editor, and Viewer. Roles can inherit permissions from other roles, creating hierarchical access levels. **Permissions** represent specific actions users can perform, formatted as `resource:action` patterns like `projects:create` or `tasks:read`. Use permissions for granular access control when you need precise control over individual capabilities. Access token contents ```json { "aud": ["skc_987654321098765432"], "client_id": "skc_987654321098765432", "exp": 1750850145, "iat": 1750849845, "iss": "http://example.localhost:8889", "jti": "tkn_987654321098765432", "nbf": 1750849845, "roles": ["project_manager", "member"], "oid": "org_69615647365005430", "permissions": ["projects:create", "projects:read", "tasks:assign"], "sid": "ses_987654321098765432", "sub": "usr_987654321098765432" } ``` Scalekit automatically assigns the `admin` role to the first user in each organization and the `member` role to subsequent users. Your application uses the role and permission information from Scalekit to make final authorization decisions at runtime. Start by defining the roles and permissions your application needs. --- # DOCUMENT BOUNDARY --- # Multi-App Authentication > Share authentication across web, mobile, and desktop applications with a unified session Register multiple applications as OAuth clients that share a single Scalekit user session. Users authenticate once and gain access everywhere across your web app, mobile app, desktop client, and documentation site. Each application gets its own OAuth client with appropriate credentials based on its type, while all apps share the same underlying session. [Check out the example apps](https://github.com/scalekit-inc/multiapp-demo) Use multi-app authentication when you ship multiple apps (web, mobile, desktop, or SPA), users expect to stay signed in across surfaces, or you need centralized session control and auditability. Each app gets its own OAuth client for clearer audit logs, safer scope boundaries, and easier maintenance. This eliminates friction from repeated logins and closes security gaps from inconsistent session handling. ![Multi-app login: App A and App B share one Scalekit session, so logging in to one signs the user in to the other](/d2/docs/authenticate/fsa/multiapp/overview-0.svg) ## How multi-app authentication works [Section titled “How multi-app authentication works”](#how-multi-app-authentication-works) 1. [Register](/authenticate/fsa/multiapp/manage-apps/) each application as an OAuth client in Scalekit. 2. User logs into any app. 3. Scalekit creates a session for that user. 4. Other apps detect the session and skip the login prompt. 5. Logging out of any app terminates the shared session. Each app must clear its own local state Revoking the Scalekit session does not automatically clear your application’s local state. Each app must clear its own session and stored tokens. A failed **refresh token exchange** is a reliable signal that the shared session has been revoked. For proactive sign-out across all applications, configure [back-channel logout URLs](/authenticate/fsa/multiapp/manage-apps/#configure-redirect-urls) so Scalekit can notify each app when the shared session is terminated. ## Application types and authentication flows [Section titled “Application types and authentication flows”](#application-types-and-authentication-flows) Each application is registered separately in Scalekit and receives its own OAuth client. Choose the application type based on whether it has a backend server that can securely store credentials: | App Type | Description | Has Backend? | Uses Secret? | Auth Flow | | --------------------------------------------------------------------------- | ----------------------------------------------------------- | :----------: | :----------: | ------------------ | | [**Web app** (Express, Django, Rails)](/authenticate/fsa/multiapp/web-app) | Server-rendered or backend-driven apps with secure secrets. | ✓ | ✓ | Authorization Code | | [**SPA** (React, Vue, Angular)](/authenticate/fsa/multiapp/single-page-app) | Frontend-only apps running fully in the browser. | ✗ | ✗ | Auth Code + PKCE | | [**Mobile** (iOS, Android)](/authenticate/fsa/multiapp/native-app) | iOS or Android apps using system browser flows. | ✗ | ✗ | Auth Code + PKCE | | [**Desktop** (Electron, Tauri)](/authenticate/fsa/multiapp/native-app) | Electron or native desktop apps with deep links. | ✗ | ✗ | Auth Code + PKCE | Even though each app has a different `client_id`, they all rely on the same Scalekit user session. Separate clients per app give you clearer audit logs, safer scope boundaries, and easier long-term maintenance. ## Implementation steps [Section titled “Implementation steps”](#implementation-steps) 1. **Create applications in Scalekit** — [Create applications](/authenticate/fsa/multiapp/manage-apps) in Scalekit for each of your apps. During setup, select the app type based on whether it has a backend and needs client secrets. 2. **Configure redirect URLs for each app** — Redirects are registered endpoints in Scalekit that control where users are sent during authentication flows. [Configure redirect URLs](/authenticate/fsa/multiapp/manage-apps/#configure-redirect-urls) for each application. 3. **Implement login flow for each app** — Once your applications are registered, each app follows an OAuth-based authentication flow. Use the [login implementation guide](/authenticate/fsa/implement-login/) for implementing login/signup flow in your apps. 4. **Manage sessions and token refresh** — After users successfully authenticate in any of your apps, you receive session tokens that manage their access. Use the [session management guide](/authenticate/fsa/manage-session/) to manage sessions in your apps. Validate access tokens on each request Validate access tokens by checking the issuer, audience (which must include the application’s `client_id`), `iat`, and `exp`. Store tokens securely, and use the `/oauth/token` endpoint with the `refresh_token` grant to obtain new access, refresh, and ID tokens when needed. 5. **Implement logout** — Initiate logout by calling the `/oidc/logout` endpoint with the relevant parameters. Clear your local application session when refresh token exchange fails, or configure back-channel logout to proactively sign users out across all applications sharing the same session. Follow the [logout implementation guide](/authenticate/fsa/logout/) to implement logout in your apps. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) Why am I getting a redirect URI mismatch error? The exact URI (including trailing slashes and query parameters) must match what’s configured in **Dashboard > Developers > Applications > \[Your App] > Redirects**. Common mismatches include: * `http` vs `https` * Missing or extra trailing slash * Different port numbers in development Why aren’t my apps sharing authentication state? Verify all applications are registered in the same Scalekit environment. Apps in different environments maintain separate session pools and cannot share authentication state. Why are users prompted to login on every app? Check the following: * All apps use the same Scalekit environment URL * The browser allows third-party cookies (required for session detection) * The user is using the same browser across apps Why is the refresh token being rejected? The Scalekit session may have been revoked from another application, or the refresh token has expired. Redirect the user to log in again to establish a new session. --- # DOCUMENT BOUNDARY --- # Overview: MCP server authentication > Secure your Model Context Protocol (MCP) servers with Scalekit's drop-in OAuth 2.1 authorization solution Model Context Protocol (MCP) is an open standard that gives AI apps a consistent, secure way to connect to external tools and data sources. A helpful way to picture it is USB‑C for AI integrations: instead of building a custom connector for every service, MCP provides one interface that works across different models, platforms, and backends. That makes it much easier to build agent-style apps that can actually do work, but it also makes authorization a bigger deal, because once an agent can act on your behalf, you need clear, tight control over what it can access and what actions it’s allowed to take. At its core, MCP follows a client-server architecture where a host application can connect to multiple servers: * **MCP hosts**: AI applications like Claude Desktop, IDEs, or custom AI tools that need to access external resources * **MCP clients**: Protocol clients that maintain connections between hosts and servers * **MCP servers**: Lightweight programs that expose specific capabilities (tools, data, or services) through the standardized protocol * **Data sources**: Local files, databases, APIs, and services that MCP servers can access This architecture enables a ecosystem where AI models can seamlessly integrate with hundreds of different services without requiring custom code for each integration. ## The path to secure MCP: OAuth 2.1 integration [Section titled “The path to secure MCP: OAuth 2.1 integration”](#the-path-to-secure-mcp-oauth-21-integration) Recognizing these challenges, the MCP specification evolved to incorporate robust authorization mechanisms. The Model Context Protocol provides authorization capabilities at the transport level, enabling MCP clients to make requests to restricted MCP servers on behalf of resource owners. The **MCP specification chose OAuth 2.1 as its authorization framework** for several compelling reasons | | | | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Industry standard | OAuth 2.1 is a well-established, widely-adopted standard for delegated authorization, with extensive tooling and ecosystem support. | | Security best practices | OAuth 2.1 incorporates lessons learned from OAuth 2.0, removing deprecated flows and enforcing security measures like PKCE (Proof Key for Code Exchange). | | Flexibility | Supports multiple grant types suitable for different MCP use cases: **Authorization code**: When AI agents act on behalf of human users **Client credentials**: For machine-to-machine integrations | | Ecosystem compatibility | Works with existing identity providers and authorization servers, making it easier for enterprises to integrate MCP into their existing security infrastructure. | This authorization mechanism is based on established specifications listed below, but implements a selected subset of their features to ensure security and interoperability while maintaining simplicity: * **OAuth 2.1**: Core authorization framework with enhanced security * **OAuth 2.0 Authorization Server Metadata (RFC8414)**: Standardized server discovery * **OAuth 2.0 Dynamic Client Registration Protocol (RFC7591)**: Automatic client registration * **OAuth 2.0 Protected Resource Metadata (RFC9728)**: Resource server discovery * **Client ID Metadata Document (CIMD)**: Lets authorization servers fetch client metadata directly from a client-hosted document for authorization ## The authorization flow in practice [Section titled “The authorization flow in practice”](#the-authorization-flow-in-practice) Now let’s zoom in and see how the MCP OAuth 2.1 flow unfolds step-by-step: ### Discovery phase [Section titled “Discovery phase”](#discovery-phase) 1. **MCP client** encounters a protected MCP server 2. **Server** responds with `401 Unauthorized` and `WWW-Authenticate` header pointing to Scalekit Auth Server 3. **Client** discovers Scalekit Auth Server capabilities through metadata endpoints ![MCP login discovery: the client reads the protected resource metadata, then Scalekit's authorization server metadata](/d2/docs/authenticate/mcp/overview-0.svg) ### Authorization phase [Section titled “Authorization phase”](#authorization-phase) 4. **Client** registers with Scalekit Auth Server (if using DCR) 5. **Scalekit Auth Server** issues client credentials (if using DCR) 6. **Client** initiates appropriate OAuth flow 7. **User** grants consent (for Authorization Code flow) 8. **Scalekit Auth Server** issues access token with appropriate scopes ### Client registration [Section titled “Client registration”](#client-registration) #### Dynamic client registration [Section titled “Dynamic client registration”](#dynamic-client-registration) MCP clients and authorization servers SHOULD support the OAuth 2.1 Dynamic Client Registration Protocol to allow MCP clients to obtain OAuth client IDs without user interaction. This enables seamless onboarding of new AI agents without manual configuration. ![MCP dynamic client registration: the client registers, the user consents, and the client exchanges the code for tokens](/d2/docs/authenticate/mcp/overview-1.svg) #### Client ID Metadata Document (CIMD) [Section titled “Client ID Metadata Document (CIMD)”](#client-id-metadata-document-cimd) MCP clients SHOULD support the Client ID Metadata Document (CIMD) specification, which allows clients to publish their OAuth client metadata at a well-known URL under their control. This enables authorization servers to automatically retrieve and validate client metadata without requiring an explicit dynamic registration request, simplifying onboarding for new AI agents while maintaining secure, decentralized client configuration. ![MCP Client ID Metadata Document flow: the client identifies with a metadata URL, the user consents, and the client gets tokens](/d2/docs/authenticate/mcp/overview-2.svg) ### Access phase [Section titled “Access phase”](#access-phase) 9. **Client** includes access token in requests to MCP server 10. **MCP server** validates token and enforces scope-based permissions 11. **Server** processes request and returns response 12. **All interactions** are logged for audit and compliance ![MCP access and refresh tokens: the client calls tools with a bearer token that the server validates](/d2/docs/authenticate/mcp/overview-3.svg) ## Key security enhancements in MCP OAuth 2.1 [Section titled “Key security enhancements in MCP OAuth 2.1”](#key-security-enhancements-in-mcp-oauth-21) MCP’s OAuth 2.1 profile reduces a few common risks in the authorization code flow. The key enhancements are: * **Mandatory PKCE**: Clients must use PKCE to help prevent authorization code interception. * **Strict redirect URI validation**: Servers must only allow pre-registered redirect URIs and enforce an exact match to reduce redirect attacks. * **Short-lived tokens**: Authorization servers should issue short-lived access tokens to limit impact if a token leaks. * **Granular scopes**: Use narrow scopes (for example, `todo:read`, `todo:write`) so apps request only what they need and users can understand what they’re granting. --- # DOCUMENT BOUNDARY --- # Overview > Passwordless authentication provides a secure and convenient way to authenticate users without the need for passwords. Passwordless authentication is an authentication method that allows users to access a system without the need for passwords. It is a secure and convenient way to authenticate users, as it eliminates the risk of password-related vulnerabilities and makes it easier for users to access a system. Passwordless authentication can be implemented using different methods, such as Email OTP, Email Magic Link, Passkeys and more. Scalekit supports both headless implementation of Passwordless authentication and also complete passwordless implementation via OIDC. Developers can choose the model that fits best based on their implementation needs, context etc. The main benefits of using passwordless authentication over traditional password-based authentication include: * **Improved security**: Passwordless authentication eliminates the risk of password-related vulnerabilities such as phishing, credential stuffing and password cracking. * **Better user experience**: Passwordless authentication provides a seamless and convenient way for users to access a system, without the need to remember and enter passwords. * **Reduced support costs**: With passwordless authentication, users do not need to reset their passwords or contact support for password-related issues, which reduces the support costs. * **Modern authentication**: Passwordless authentication aligns with current security best practices and provides a modern and secure way to authenticate users. ## Authentication methods [Section titled “Authentication methods”](#authentication-methods) Scalekit supports multiple passwordless authentication methods: * **Verification Code (OTP)**: Users receive a one-time passcode via email * **Magic Link** : Users receive a link via email that the user needs to click to verify their email address. * **Magic Link + Verification Code** : Users receive a link and a one-time passcode via email and the users can choose either of the options to verify their email address. * **Passkeys** Coming soon : Users authenticate using their biometric data. * **TOTP (Authenticator App)** Coming soon : Users authenticate using a time-based one-time passcode generated by an authenticator app. ## Implementation choices [Section titled “Implementation choices”](#implementation-choices) When implementing passwordless authentication, you have two options: **Headless Implementation**: You can use our APIs to implement passwordless authentication without any dependence on our UI. You can implement your own UI to collect the OTP from your users or handle the magic link validation. **OIDC Implementation**: We handle both the security and UI implementation of the OTP and/or magic link workflow. As part of the implementation, you will redirect the user to Scalekit’s OIDC Endpoint to complete the email OTP and/or magic link workflow. Once verified, we will send the user back to your pre-configured redirect url endpoint with the email address of the user so that you can complete the workflow. [Headless Implementation](/passwordless/quickstart)Learn how to implement Email OTP based passwordless authentication using our headless SDK [OIDC Implementation](/passwordless/oidc)Learn how to implement Email OTP based passwordless authentication using OIDC --- # DOCUMENT BOUNDARY --- # Self-hosted deployment overview > Deploy Scalekit on your own Kubernetes cluster to meet data residency, compliance, and network isolation requirements. You will run the full Scalekit platform on your own Kubernetes cluster. This helps you keep all authentication data inside your network for data residency, compliance, and isolation from the public internet. Use self-hosted deployment when your organization requires: * **Data residency**: Auth data must remain in a specific region or on-premises location * **Network isolation**: The auth service must not be reachable from the public internet * **Compliance**: Regulations such as FedRAMP, HIPAA, or internal security policies prohibit use of SaaS auth services * **Air-gapped environments**: Your deployment environment has no outbound internet access ## How self-hosted deployment works [Section titled “How self-hosted deployment works”](#how-self-hosted-deployment-works) Scalekit is deployed on Kubernetes via a Helm chart managed through the Scalekit distribution portal. You configure the deployment with a `values.yaml` file, apply Kubernetes secrets to your cluster, and connect the cluster to the portal. The portal handles chart delivery, migrations, and upgrades. ## Components in a self-hosted deployment [Section titled “Components in a self-hosted deployment”](#components-in-a-self-hosted-deployment) A self-hosted deployment runs as a single Kubernetes `Deployment` with multiple containers: | Component | Description | | ---------------- | -------------------------------------------------------------------- | | **Auth service** | Core service handling login, token issuance, sessions, SSO, and SCIM | | **Dashboard** | Admin web UI for managing your Scalekit instance | | **Flagd** | Feature flag sidecar for runtime configuration | | **Webhooks** | Webhook delivery service | | **OpenFGA** | Fine-grained authorization engine (optional) | ## Infrastructure you must provide [Section titled “Infrastructure you must provide”](#infrastructure-you-must-provide) Scalekit does not bundle a database or cache in production. You provision and manage these separately: | Dependency | Requirement | Notes | | -------------- | ------------- | ----------------------------------------------------------- | | **Kubernetes** | 1.27 or later | Any managed or self-managed cluster | | **PostgreSQL** | 15 or later | Three databases required: `scalekit`, `webhooks`, `openfga` | | **Redis** | 6.2 or later | Used for sessions, caching, and job queues | | **SMTP** | Any provider | Postmark and SendGrid have first-class support | Evaluation shortcut The Helm chart includes optional PostgreSQL and Redis subcharts. Enable them to spin up Scalekit quickly without provisioning external services. **Use subcharts for evaluation only.** They are not suitable for production. Access required Self-hosted deployment requires a Scalekit enterprise license. You receive your Helm chart access and container registry token through the Scalekit distribution portal as part of onboarding. ## Choose your path [Section titled “Choose your path”](#choose-your-path) | Path | When to use | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **[Quickstart: Deploy Scalekit on Kubernetes](/self-hosted/quickstart/)** | Get Scalekit running fast using bundled PostgreSQL and Redis. Ideal for evaluation and proof-of-concept. No external databases or `kubectl secret` commands needed. | | **[Full installation](/self-hosted/system-requirements/)** | Production deployment with external PostgreSQL and Redis, full secret management, and your choice of ingress controller. | Next, Quickstart: Deploy Scalekit on Kubernetes will get you a working evaluation instance so you can test before full deployment. --- # DOCUMENT BOUNDARY --- # Agent Gateway quickstart > Enable Agent Gateway in the Scalekit dashboard, choose its connections, copy the MCP endpoint URL and connect your first AI client. In this quickstart you enable Agent Gateway in one environment, check which connections it serves and connect your first AI client. It takes a few minutes, and everything happens in the Scalekit dashboard. Available on request Agent Gateway is available on request. [Contact us](/support/contact-us/) to enable it for your workspace. ## Before you start [Section titled “Before you start”](#before-you-start) * **Agent Gateway enabled for your workspace.** [Contact us](/support/contact-us/) if **Agent Gateway** isn’t in the dashboard’s product switcher. * **At least one connection** in the environment, such as Gmail or Outlook. You can also add one during setup. [Set up connections](/agentkit/connections/) covers each connector’s options. * **An AI client that supports remote MCP servers**, such as Claude, ChatGPT, Cursor or VS Code. 1. ## Open Agent Gateway [Section titled “Open Agent Gateway”](#open-agent-gateway) In the Scalekit dashboard, open the environment that has your connections. Then open the product switcher at the top of the left navigation and choose **Agent Gateway**. Select **Getting Started** to open **Get started with Gateway**. 2. ## Enable the Gateway [Section titled “Enable the Gateway”](#enable-the-gateway) Select **Enable Gateway**. Scalekit creates the Gateway’s MCP endpoint for this environment and adds you as its first user. 3. ## Add users [Section titled “Add users”](#add-users) This step is optional. To give teammates access now, add each person’s email address. They get access straight away, with no invitation email. You can add people later under **Identity**. Select **Next** to continue. 4. ## Check your connections [Section titled “Check your connections”](#check-your-connections) **Add connections** lists the environment’s connections. Check that the apps you want are there, then select **Next**. To add an app, select **Add connection** and follow the connector’s setup. Agents can only use tools from connections listed here. 5. ## Copy the MCP endpoint URL [Section titled “Copy the MCP endpoint URL”](#copy-the-mcp-endpoint-url) **Your Gateway is ready** shows the **MCP endpoint URL**. Select **Copy URL**. Under **Connect your agent**, pick your AI client to see how to add the URL to it. Some clients, such as Cursor, open with a single click. ## Check it worked [Section titled “Check it worked”](#check-it-worked) 1. Add the MCP endpoint URL to your AI client, as in [Connect AI clients](/agentkit/agent-gateway/connect-ai-clients/). 2. Sign in when the client opens the sign-in page, and approve the consent screen. 3. Ask the client to use one of your connections, for example “List my five most recent emails”. If you haven’t connected that app yet, the client gives you a link to connect it. Open it, sign in to the app and ask again. 4. In the dashboard, open **Logs**. The tool call appears there with its tool, connector and status. ## Next [Section titled “Next”](#next) * [Connect AI clients](/agentkit/agent-gateway/connect-ai-clients/): setup for Claude, ChatGPT, Cursor, VS Code, Claude Code, Windsurf and Grok Bot. * [Users and access](/agentkit/agent-gateway/users-and-access/): add your team and sign them in with SSO. * [Access policies](/agentkit/agent-gateway/access-policies/): choose which tools each group can use on each connection. --- # DOCUMENT BOUNDARY --- # Quickstart > Make your first AgentKit tool call in Python, Node.js or cURL: install the SDK, add your credentials, and call GitHub as yourself. AgentKit lets your AI agents act in your users’ apps on their behalf. New to it? Read [What is AgentKit](/agentkit/overview/). Adding login to your own app instead? See the [SaaSKit quickstart](/authenticate/fsa/quickstart/). Connect your own GitHub account and make your first tool call as that user. New Scalekit environments include a GitHub connection, so there’s nothing to set up in GitHub. **About 10 minutes.** Using a coding agent?Copy AgentKit prompt ## Before you start [Section titled “Before you start”](#before-you-start) * A Scalekit account at [app.scalekit.com](https://app.scalekit.com) * Python 3.10+, Node.js 18.14+, or `curl` and [`jq`](https://jqlang.org/) * Your environment URL, client ID and client secret. Open **Developers** > **Settings** > **API Credentials**: copy the **Environment URL** and **Client ID** from **Environment details**, then select **Generate new secret** under **Client secrets**. The secret is shown once, so copy it right away. See [API credentials](/agentkit/api-credentials/). * The GitHub connection name from **AgentKit** > **Connections**. New environments call it `github-connect`. No GitHub connection? Create one in **AgentKit** > **Connections** (see [Set up a connection](/agentkit/connections/)) and use its name. * **AgentKit** > **Settings** > **User Verification** set to **None** or **Scalekit users only**. With **Scalekit users only**, stay signed in to the dashboard in the same browser when you approve access. 1. ## Install the SDK [Section titled “Install the SDK”](#install-the-sdk) * Python ```sh pip install scalekit-sdk-python python-dotenv ``` * Node.js ```sh npm init -y npm pkg set type=module npm install @scalekit-sdk/node dotenv npm install -D tsx ``` 2. ## Add your credentials [Section titled “Add your credentials”](#add-your-credentials) Create a `.env` file next to your script. Don’t commit it. .env ```sh SCALEKIT_ENVIRONMENT_URL= SCALEKIT_CLIENT_ID= SCALEKIT_CLIENT_SECRET= GITHUB_CONNECTION_NAME=github-connect ``` 3. ## Write the script [Section titled “Write the script”](#write-the-script) Save this as `agent.py`, `agent.ts` or `agent.sh`. It connects the user if needed, then calls GitHub as them. * Python agent.py ```python 1 import os 2 3 from dotenv import load_dotenv 4 from scalekit import ScalekitClient 5 from scalekit.common.exceptions import ScalekitServerException 6 7 load_dotenv() 8 9 scalekit_client = ScalekitClient( 10 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 11 client_id=os.environ["SCALEKIT_CLIENT_ID"], 12 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 13 ) 14 actions = scalekit_client.actions 15 16 connection_name = os.environ["GITHUB_CONNECTION_NAME"] 17 user_id = "user_123" # your app's ID for this user: stable and hard to guess 18 19 # Find this user's connected account, or create it on the first run 20 account = actions.get_or_create_connected_account( 21 connection_name=connection_name, 22 identifier=user_id, 23 ).connected_account 24 25 if account.status != "ACTIVE": 26 link = actions.get_authorization_link( 27 connection_name=connection_name, 28 identifier=user_id, 29 ).link 30 print(f"Open this link and approve access:\n{link}") 31 input("Press Enter when you're done...") 32 33 account = actions.get_connected_account_details( 34 connection_name=connection_name, 35 identifier=user_id, 36 ).connected_account 37 if account.status != "ACTIVE": 38 raise SystemExit(f"The account is {account.status}, not ACTIVE. Run the script again.") 39 40 # Call GitHub as this user. The tool is read-only. 41 try: 42 result = actions.execute_tool( 43 tool_name="github_user_get_authenticated", 44 tool_input={}, 45 connected_account_id=account.id, 46 ) 47 except ScalekitServerException as e: 48 raise SystemExit(f"Tool call failed: {e.error_code} {e.message}") 49 print(result.data) ``` * Node.js agent.ts ```ts 1 import 'dotenv/config'; 2 import { createInterface } from 'node:readline/promises'; 3 import { ConnectorStatus, ScalekitClient, ScalekitServerException } from '@scalekit-sdk/node'; 4 5 const scalekit = new ScalekitClient( 6 process.env.SCALEKIT_ENVIRONMENT_URL!, 7 process.env.SCALEKIT_CLIENT_ID!, 8 process.env.SCALEKIT_CLIENT_SECRET!, 9 ); 10 const actions = scalekit.actions; 11 12 const connectionName = process.env.GITHUB_CONNECTION_NAME!; 13 const userId = 'user_123'; // your app's ID for this user: stable and hard to guess 14 15 // Find this user's connected account, or create it on the first run 16 let { connectedAccount: account } = await actions.getOrCreateConnectedAccount({ 17 connectionName, 18 identifier: userId, 19 }); 20 21 if (account?.status !== ConnectorStatus.ACTIVE) { 22 const { link } = await actions.getAuthorizationLink({ connectionName, identifier: userId }); 23 console.log(`Open this link and approve access:\n${link}`); 24 const rl = createInterface({ input: process.stdin, output: process.stdout }); 25 await rl.question("Press Enter when you're done..."); 26 rl.close(); 27 28 ({ connectedAccount: account } = await actions.getConnectedAccount({ 29 connectionName, 30 identifier: userId, 31 })); 32 if (account?.status !== ConnectorStatus.ACTIVE) { 33 throw new Error('The account is not ACTIVE yet. Run the script again.'); 34 } 35 } 36 37 // Call GitHub as this user. The tool is read-only. 38 try { 39 const result = await actions.executeTool({ 40 toolName: 'github_user_get_authenticated', 41 toolInput: {}, 42 connectedAccountId: account.id, 43 }); 44 console.log(result.data); 45 } catch (err) { 46 if (!(err instanceof ScalekitServerException)) throw err; 47 console.error('Tool call failed:', err.errorCode, err.message); 48 process.exit(1); 49 } ``` * cURL agent.sh ```bash 1 #!/usr/bin/env bash 2 set -euo pipefail 3 set -a; source .env; set +a 4 5 USER_ID="user_123" # your app's ID for this user: stable and hard to guess 6 API="$SCALEKIT_ENVIRONMENT_URL/api/v1" 7 8 # Exchange your API credentials for an access token 9 TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \ 10 -d grant_type=client_credentials \ 11 -d client_id="$SCALEKIT_CLIENT_ID" \ 12 -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token) 13 14 account_status() { 15 curl -sS -G "$API/connected_accounts/details" \ 16 -H "Authorization: Bearer $TOKEN" \ 17 --data-urlencode "connector=$GITHUB_CONNECTION_NAME" \ 18 --data-urlencode "identifier=$USER_ID" | 19 jq -r '.connected_account.status // "NOT_CREATED"' 20 } 21 22 if [ "$(account_status)" != "ACTIVE" ]; then 23 # Creates the connected account on the first run, and returns a link 24 LINK=$(curl -sS "$API/connected_accounts/magic_link" \ 25 -H "Authorization: Bearer $TOKEN" \ 26 -H "Content-Type: application/json" \ 27 -d "{\"connector\": \"$GITHUB_CONNECTION_NAME\", \"identifier\": \"$USER_ID\"}" | 28 jq -r .link) 29 printf 'Open this link and approve access:\n%s\n' "$LINK" 30 read -r -p "Press Enter when you're done..." 31 32 STATUS=$(account_status) 33 if [ "$STATUS" != "ACTIVE" ]; then 34 echo "The account is $STATUS, not ACTIVE. Run the script again." >&2 35 exit 1 36 fi 37 fi 38 39 # Call GitHub as this user. The tool is read-only. 40 curl -sS "$API/execute_tool" \ 41 -H "Authorization: Bearer $TOKEN" \ 42 -H "Content-Type: application/json" \ 43 -d "{\"tool_name\": \"github_user_get_authenticated\", \"connector\": \"$GITHUB_CONNECTION_NAME\", \"identifier\": \"$USER_ID\", \"params\": {}}" | 44 jq .data ``` `user_123` stands in for your app’s user ID. [Choose an identifier](/agentkit/concepts/#choose-an-identifier) explains what to use in production. 4. ## Run it [Section titled “Run it”](#run-it) * Python ```sh python agent.py ``` * Node.js ```sh npx tsx agent.ts ``` * cURL ```sh bash agent.sh ``` The first run prints a link. Open it, approve access on GitHub, then press Enter. You’ll see your GitHub profile. Run it again: the account is already `ACTIVE`, so the script skips the link. ## If it doesn’t work [Section titled “If it doesn’t work”](#if-it-doesnt-work) ### `connection not found for the given key` [Section titled “connection not found for the given key”](#connection-not-found-for-the-given-key) `GITHUB_CONNECTION_NAME` doesn’t match the dashboard exactly. Copy it again from **AgentKit** > **Connections**. ### The account stays `PENDING_AUTH` [Section titled “The account stays PENDING_AUTH”](#the-account-stays-pending_auth) The approval wasn’t finished, or the link expired. Links last five minutes by default. Run the script again for a fresh link. ### `user_verify_url is required when user_verify_mode is b2b` [Section titled “user_verify_url is required when user_verify_mode is b2b”](#user_verify_url-is-required-when-user_verify_mode-is-b2b) User verification is set to **Custom user verifier**, so the link request needs a verify URL. For this quickstart, use **None** or **Scalekit users only** in **AgentKit** > **Settings** > **User Verification**. To set up the custom user verifier, see [Verify users](/agentkit/user-verification/). ### `KeyError: 'SCALEKIT_ENVIRONMENT_URL'` or `TypeError: Invalid URL` [Section titled “KeyError: 'SCALEKIT_ENVIRONMENT_URL' or TypeError: Invalid URL”](#keyerror-scalekit_environment_url-or-typeerror-invalid-url) The `.env` file wasn’t loaded. Run the script from the folder that contains it. ### `401` or `invalid_client` [Section titled “401 or invalid_client”](#401-or-invalid_client) The client ID or secret is wrong, or the secret was regenerated. ### Still stuck? [Section titled “Still stuck?”](#still-stuck) Find the error message or the account status on [Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/). It covers each connected-account status, the errors on Scalekit’s connection page, OAuth app misconfiguration and provider-specific errors. ## Next [Section titled “Next”](#next) [How AgentKit works](/agentkit/concepts/)What you just created, and why. [Set up a connection](/agentkit/connections/)Connect Gmail, Slack or any other app. [Choose a framework](/agentkit/examples/)LangChain, OpenAI, Anthropic and more. Before real users connect accounts, [verify users](/agentkit/user-verification/). --- # DOCUMENT BOUNDARY --- # SaaSKit: Add auth to my app > SaaSKit — Hosted auth pages, managed sessions, secure logout. Purpose built. Simple where it counts You’ll implement sign-up, login, and logout flows with secure session management and user management included. The foundation you build here extends to features like workspaces, enterprise SSO, MCP authentication, and SCIM provisioning. See Demo [Play](https://youtube.com/watch?v=098_9blgM90) Review the authentication sequence Scalekit handles the complex authentication flow while you focus on your core product: ![Full-Stack Authentication Flow](/.netlify/images?url=_astro%2Fnew-1.BmdCP8EN.png\&w=4096\&h=4584\&dpl=6ae471fc143c9a4c27550463) 1. **User initiates sign-in** - Your app redirects to Scalekit’s hosted auth page 2. **Identity verification** - User authenticates via their preferred method 3. **Secure callback** - Scalekit returns user profile and session tokens 4. **Session creation** - Your app establishes a secure user session 5. **Protected access** - User accesses your application’s features Using a coding agent?Copy SaaSKit prompt *** 1. ## Set up Scalekit [Section titled “Set up Scalekit”](#set-up-scalekit) Use the following instructions to install the SDK for your technology stack. * Node.js ```bash npm install @scalekit-sdk/node ``` * Python ```sh pip install scalekit-sdk-python ``` * Go ```sh go get -u github.com/scalekit-inc/scalekit-sdk-go ``` * Java ```groovy /* Gradle users - add the following to your dependencies in build file */ implementation "com.scalekit:scalekit-sdk-java:2.1.3" ``` ```xml com.scalekit scalekit-sdk-java 2.1.3 ``` If you haven’t already, add your Scalekit credentials to your environment variables file: .env ```sh SCALEKIT_ENVIRONMENT_URL= SCALEKIT_CLIENT_ID= SCALEKIT_CLIENT_SECRET= COOKIE_ENCRYPTION_SECRET= # openssl rand -base64 32 — encrypts the session cookie REDIRECT_URI=http://localhost:3000/callback ``` Node.js and Python use `/callback`. Go and Java use `/auth/callback`. Register the path for your stack. ### Register redirect URLs for your app [Section titled “Register redirect URLs for your app”](#register-redirect-urls-for-your-app) You need to register redirect URLs for your application. Go to **Scalekit dashboard** → **Authentication** → **Redirect URLs** and configure: * **Allowed callback URLs**: The endpoint where users are sent after successful authentication to exchange authorization codes and retrieve profile information. [Learn more](/guides/dashboard/redirects/#allowed-callback-urls) * **Initiate login URL**: The endpoint in your app that redirects users to Scalekit’s `/authorize` endpoint. Required when authentication is not initiated from your app, for example, when a user accepts an organization invitation or starts sign-in directly from their identity provider (IdP-initiated SSO). [Learn more](/guides/dashboard/redirects/#initiate-login-url) 2. ## Redirect users to sign up (or) login [Section titled “Redirect users to sign up (or) login”](#redirect-users-to-sign-up-or-login) An authorization URL is an endpoint that redirects users to Scalekit’s sign-in page. Use the Scalekit SDK to construct this URL with your redirect URI and required scopes. * Node.js * Express server.ts ```ts 1 import express from 'express'; 2 import { ScalekitAuth } from '@scalekit-sdk/node/express'; 3 4 const auth = new ScalekitAuth({ 5 envUrl: process.env.SCALEKIT_ENVIRONMENT_URL, 6 clientId: process.env.SCALEKIT_CLIENT_ID, 7 clientSecret: process.env.SCALEKIT_CLIENT_SECRET, 8 redirectUri: process.env.REDIRECT_URI, 9 cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET, 10 }); 11 12 const app = express(); 13 app.use(auth.router); 14 15 app.get('/account', auth.requiresAuth, (req, res) => { 16 res.json({ sub: req.scalekitUser?.sub }); 17 }); 18 19 app.listen(3000); ``` Open `http://localhost:3000/account`. A missing session goes to `/login`. After login, the visitor returns to `/account`. Register `http://localhost:3000/callback` as the dashboard Redirect URI. * Next.js lib/auth.ts ```ts import { ScalekitClient } from '@scalekit-sdk/node'; import { ScalekitAuthNext } from '@scalekit-sdk/node/next'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET! ); export const auth = new ScalekitAuthNext({ client: scalekit, redirectUri: process.env.REDIRECT_URI!, cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET!, }); ``` app/login/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createLoginHandler(); ``` app/callback/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createCallbackHandler(); ``` app/logout/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createLogoutHandler(); ``` app/account/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.withAuth(async (request, { user }) => { return Response.json({ sub: user?.sub }); }); ``` Open `http://localhost:3000/account`. A missing session goes to `/login`. * Python server.ts ```ts 1 import express from 'express'; 2 import { ScalekitAuth } from '@scalekit-sdk/node/express'; 3 4 const auth = new ScalekitAuth({ 5 envUrl: process.env.SCALEKIT_ENVIRONMENT_URL, 6 clientId: process.env.SCALEKIT_CLIENT_ID, 7 clientSecret: process.env.SCALEKIT_CLIENT_SECRET, 8 redirectUri: process.env.REDIRECT_URI, 9 cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET, 10 }); 11 12 const app = express(); 13 app.use(auth.router); 14 15 app.get('/account', auth.requiresAuth, (req, res) => { 16 res.json({ sub: req.scalekitUser?.sub }); 17 }); 18 19 app.listen(3000); ``` Open `http://localhost:3000/account`. A missing session goes to `/login`. After login, the visitor returns to `/account`. Register `http://localhost:3000/callback` as the dashboard Redirect URI. * Go lib/auth.ts ```ts import { ScalekitClient } from '@scalekit-sdk/node'; import { ScalekitAuthNext } from '@scalekit-sdk/node/next'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET! ); export const auth = new ScalekitAuthNext({ client: scalekit, redirectUri: process.env.REDIRECT_URI!, cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET!, }); ``` app/login/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createLoginHandler(); ``` app/callback/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createCallbackHandler(); ``` app/logout/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createLogoutHandler(); ``` app/account/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.withAuth(async (request, { user }) => { return Response.json({ sub: user?.sub }); }); ``` Open `http://localhost:3000/account`. A missing session goes to `/login`. * Java * Flask ```bash pip install "scalekit-sdk-python[flask]" ``` app.py ```py 1 import os 2 from flask import Flask 3 from scalekit.frameworks.flask import ScalekitAuth 4 5 app = Flask(__name__) 6 auth = ScalekitAuth( 7 app, 8 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 9 client_id=os.environ["SCALEKIT_CLIENT_ID"], 10 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 11 redirect_uri=os.environ["REDIRECT_URI"], 12 cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], 13 # HTTP localhost drops a Secure cookie, so the session never sticks. 14 # A missing Secure flag on real HTTP lets the browser send sk_session in cleartext. 15 cookie_secure=False, 16 ) 17 18 @app.route("/account") 19 @auth.requires_auth 20 def account(): 21 return {"sub": auth.current_user["sub"]} 22 23 if __name__ == "__main__": 24 app.run(port=3000) ``` Open `http://localhost:3000/account`. A missing session goes to `/login`. Register `http://localhost:3000/callback` as the dashboard Redirect URI. * FastAPI ```bash pip install "scalekit-sdk-python[fastapi]" ``` app.py ```py 1 import os 2 from fastapi import Depends, FastAPI 3 from scalekit.frameworks.fastapi import ScalekitAuth 4 5 app = FastAPI() 6 auth = ScalekitAuth( 7 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 8 client_id=os.environ["SCALEKIT_CLIENT_ID"], 9 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 10 redirect_uri=os.environ["REDIRECT_URI"], 11 cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], 12 # HTTP localhost drops a Secure cookie, so the session never sticks. 13 # A missing Secure flag on real HTTP lets the browser send sk_session in cleartext. 14 cookie_secure=False, 15 ) 16 auth.install(app) 17 18 @app.get("/account") 19 async def account(user: dict = Depends(auth.requires_auth)): 20 return {"sub": user["sub"]} ``` ```bash uvicorn app:app --port 3000 ``` Open `http://localhost:3000/account`. Register `http://localhost:3000/callback` as the dashboard Redirect URI. Set cookie_secure=False on local HTTP `cookie_secure` defaults to `True`. Some browsers drop a `Secure` cookie on plain `http://localhost`, which looks like a session that never sticks. Set `cookie_secure=False` for local HTTP. Set it `True` in production. * Express ```bash pip install "scalekit-sdk-python[flask]" ``` app.py ```py 1 import os 2 from flask import Flask 3 from scalekit.frameworks.flask import ScalekitAuth 4 5 app = Flask(__name__) 6 auth = ScalekitAuth( 7 app, 8 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 9 client_id=os.environ["SCALEKIT_CLIENT_ID"], 10 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 11 redirect_uri=os.environ["REDIRECT_URI"], 12 cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], 13 # HTTP localhost drops a Secure cookie, so the session never sticks. 14 # A missing Secure flag on real HTTP lets the browser send sk_session in cleartext. 15 cookie_secure=False, 16 ) 17 18 @app.route("/account") 19 @auth.requires_auth 20 def account(): 21 return {"sub": auth.current_user["sub"]} 22 23 if __name__ == "__main__": 24 app.run(port=3000) ``` Open `http://localhost:3000/account`. A missing session goes to `/login`. Register `http://localhost:3000/callback` as the dashboard Redirect URI. * Next.js ```bash pip install "scalekit-sdk-python[fastapi]" ``` app.py ```py 1 import os 2 from fastapi import Depends, FastAPI 3 from scalekit.frameworks.fastapi import ScalekitAuth 4 5 app = FastAPI() 6 auth = ScalekitAuth( 7 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 8 client_id=os.environ["SCALEKIT_CLIENT_ID"], 9 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 10 redirect_uri=os.environ["REDIRECT_URI"], 11 cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], 12 # HTTP localhost drops a Secure cookie, so the session never sticks. 13 # A missing Secure flag on real HTTP lets the browser send sk_session in cleartext. 14 cookie_secure=False, 15 ) 16 auth.install(app) 17 18 @app.get("/account") 19 async def account(user: dict = Depends(auth.requires_auth)): 20 return {"sub": user["sub"]} ``` ```bash uvicorn app:app --port 3000 ``` Open `http://localhost:3000/account`. Register `http://localhost:3000/callback` as the dashboard Redirect URI. * Flask internal/http/auth.go ```go 1 // Must match the allowed callback URL you registered in the dashboard 2 redirectUri := "http://localhost:3000/auth/callback" 3 4 // Request user profile data (openid, profile, email) and session tracking (offline_access) 5 // offline_access enables refresh tokens so users can stay logged in across sessions 6 options := scalekit.AuthorizationUrlOptions{ 7 Scopes: []string{"openid", "profile", "email", "offline_access"} 8 } 9 10 authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options) 11 // Generated URL will look like: 12 // https:///oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile%20email%20offline_access&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback 13 if err != nil { 14 // Handle error based on your application's error handling strategy 15 panic(err) 16 } 17 18 c.Redirect(http.StatusFound, authorizationUrl.String()) ``` * FastAPI AuthController.java ```java import com.scalekit.internal.http.AuthorizationUrlOptions; import java.net.URL; import java.util.Arrays; // Must match the allowed callback URL you registered in the dashboard String redirectUri = "http://localhost:3000/auth/callback"; // Request user profile data (openid, profile, email) and session tracking (offline_access) // offline_access enables refresh tokens so users can stay logged in across sessions AuthorizationUrlOptions options = new AuthorizationUrlOptions(); options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access")); URL authorizationUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options); // Generated URL will look like: // https:///oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile%20email%20offline_access&redirect_uri=https%3A%2F%2Fyourapp.com%2Fcallback ``` This redirects users to Scalekit’s managed sign-in page where they can authenticate. The page includes default authentication methods for users to toggle between sign in and sign up. Match your redirect URLs exactly Ensure the redirect URL in your code matches what you configured in the Scalekit dashboard, including protocol (`https://`), domain, port, and path. 3. ## Get user details from the callback [Section titled “Get user details from the callback”](#get-user-details-from-the-callback) After successful authentication, Scalekit creates a user record and sends the user information to your callback endpoint. In authentication flow, Scalekit redirects to your callback URL with an authorization code. Your application exchanges this code for the user’s profile information and session tokens. * Node.js * Express ```ts app.use(auth.router); // GET /callback writes sk_session ``` * Next.js app/callback/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createCallbackHandler(); ``` * Python ```ts app.use(auth.router); // GET /callback writes sk_session ``` * Go app/callback/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createCallbackHandler(); ``` * Java * Flask ```py auth = ScalekitAuth(app, ...) # registers GET /callback ``` * FastAPI ```py auth.install(app) # registers GET /callback ``` * Express ```py auth = ScalekitAuth(app, ...) # registers GET /callback ``` * Next.js ```py auth.install(app) # registers GET /callback ``` * Flask internal/http/auth_callback.go ```go 17 collapsed lines 1 package main 2 3 import ( 4 "log" 5 "net/http" 6 "os" 7 "github.com/gin-gonic/gin" 8 "github.com/scalekit-inc/scalekit-sdk-go" 9 ) 10 11 // Create Scalekit client instance 12 var scalekitClient = scalekit.NewScalekitClient( 13 os.Getenv("SCALEKIT_ENVIRONMENT_URL"), 14 os.Getenv("SCALEKIT_CLIENT_ID"), 15 os.Getenv("SCALEKIT_CLIENT_SECRET"), 16 ) 17 18 const redirectUri = "http://localhost:3000/auth/callback" 19 20 func callbackHandler(c *gin.Context) { 21 code := c.Query("code") 22 errorParam := c.Query("error") 23 errorDescription := c.Query("error_description") 9 collapsed lines 24 25 if errorParam != "" { 26 c.JSON(http.StatusUnauthorized, gin.H{ 27 "error": errorParam, 28 "error_description": errorDescription, 29 }) 30 return 31 } 32 33 // Exchange the authorization code for user profile and session tokens 34 // Returns: User (profile info), IdToken (JWT with user claims), AccessToken (JWT with roles/permissions), RefreshToken 35 options := scalekit.AuthenticationOptions{} 36 authResult, err := scalekitClient.AuthenticateWithCode( 37 c.Request.Context(), code, redirectUri, options, 9 collapsed lines 38 ) 39 40 if err != nil { 41 log.Printf("Error exchanging code: %v", err) 42 c.JSON(http.StatusInternalServerError, gin.H{ 43 "error": "Failed to authenticate user", 44 }) 45 return 46 } 47 48 user := authResult.User 49 // IdToken: Decode to access full user profile (sub, oid, email, name) 50 // AccessToken: Contains roles and permissions for authorization decisions 51 // RefreshToken: Use to obtain new access tokens when they expire 52 53 // "user" object contains the user's profile information 54 // Next step: Create a session and log in the user 55 c.Redirect(http.StatusFound, "/dashboard/profile") 56 } ``` * FastAPI CallbackController.java ```java 10 collapsed lines 1 import com.scalekit.ScalekitClient; 2 import com.scalekit.internal.http.AuthenticationOptions; 3 import com.scalekit.internal.http.AuthenticationResponse; 4 import org.springframework.web.bind.annotation.*; 5 import org.springframework.web.servlet.view.RedirectView; 6 import org.springframework.http.ResponseEntity; 7 import org.springframework.http.HttpStatus; 8 import java.util.HashMap; 9 import java.util.Map; 10 11 @RestController 12 public class CallbackController { 13 14 private final String redirectUri = "http://localhost:3000/auth/callback"; 15 16 @GetMapping("/auth/callback") 17 public Object callback( 18 @RequestParam(required = false) String code, 19 @RequestParam(required = false) String error, 20 @RequestParam(name = "error_description", required = false) String errorDescription 21 ) { 4 collapsed lines 22 if (error != null) { 23 // handle error 24 } 25 26 try { 27 // Exchange the authorization code for user profile and session tokens 28 // Returns: user (profile info), idToken (JWT with user claims), accessToken (JWT with roles/permissions), refreshToken 29 AuthenticationOptions options = new AuthenticationOptions(); 30 AuthenticationResponse authResult = scalekit 31 .authentication() 32 .authenticateWithCode(code,redirectUri,options); 33 34 var user = authResult.getIdTokenClaims(); 35 // idToken: Decode to access full user profile (sub, oid, email, name) 36 // accessToken: Contains roles and permissions for authorization decisions 37 // refreshToken: Use to obtain new access tokens when they expire 38 39 // "user" object contains the user's profile information 8 collapsed lines 40 // Next step: Create a session and log in the user 41 return new RedirectView("/dashboard/profile"); 42 43 } catch (Exception err) { 44 // Handle exception (e.g., log error, return error response) 45 } 46 } 47 } ``` The `authResult` object contains: * `user` - Common user details with email, name, and verification status * `idToken` - JWT containing verified full user identity claims (includes: `sub` user ID, `oid` organization ID, `email`, `name`, `exp` expiration) * `accessToken` - Short-lived token that determines current access context (includes: `sub` user ID, `oid` organization ID, `roles`, `permissions`, `exp` expiration) * `refreshToken` - Long-lived token to obtain new access tokens - Auth result ```js 1 { 2 user: { 3 email: "john.doe@example.com", 4 emailVerified: true, 5 givenName: "John", 6 name: "John Doe", 7 id: "usr_74599896446906854" 8 }, 9 idToken: "eyJhbGciO..", // Decode for full user details 10 11 accessToken: "eyJhbGciOi..", 12 refreshToken: "rt_8f7d6e5c4b3a2d1e0f9g8h7i6j..", 13 expiresIn: 299 // in seconds 14 } ``` - Decoded ID token ID token decoded ```json 1 { 2 "at_hash": "ec_jU2ZKpFelCKLTRWiRsg", 3 "aud": [ 4 "skc_58327482062864390" 5 ], 6 "azp": "skc_58327482062864390", 7 "c_hash": "6wMreK9kWQQY6O5R0CiiYg", 8 "client_id": "skc_58327482062864390", 9 "email": "john.doe@example.com", 10 "email_verified": true, 11 "exp": 1742975822, 12 "family_name": "Doe", 13 "given_name": "John", 14 "iat": 1742974022, 15 "iss": "https://scalekit-z44iroqaaada-dev.scalekit.cloud", 16 "name": "John Doe", 17 "oid": "org_59615193906282635", 18 "sid": "ses_65274187031249433", 19 "sub": "usr_63261014140912135" 20 } ``` - Decoded access token Decoded access token ```json 1 { 2 "aud": [ 3 "prd_skc_7848964512134X699" 4 ], 5 "client_id": "prd_skc_7848964512134X699", 6 "exp": 1758265247, 7 "iat": 1758264947, 8 "iss": "https://login.devramp.ai", 9 "jti": "tkn_90928731115292X63", 10 "nbf": 1758264947, 11 "oid": "org_89678001X21929734", 12 "permissions": [ 13 "workspace_data:write", 14 "workspace_data:read" 15 ], 16 "roles": [ 17 "admin" 18 ], 19 "sid": "ses_90928729571723X24", 20 "sub": "usr_8967800122X995270", 21 // External identifiers if updated on Scalekit 22 "xoid": "ext_org_123", // Organization ID 23 "xuid": "ext_usr_456", // User ID 24 } ``` The user details are packaged in the form of JWT tokens. Decode the `idToken` to access full user profile information (email, name, organization ID) and the `accessToken` to check user roles and permissions for authorization decisions. See [Complete login with code exchange](/authenticate/fsa/complete-login/) for detailed token claim references and verification instructions. 4. ## Create and manage user sessions [Section titled “Create and manage user sessions”](#create-and-manage-user-sessions) The access token is a JWT that contains the user’s permissions and roles. It expires in 5 minutes (default) but [can be configured](/authenticate/fsa/manage-session/#configure-session-security-and-duration). When it expires, use the refresh token to obtain a new access token. The refresh token is long-lived and designed for this purpose. The Scalekit SDK provides methods to refresh access tokens automatically. However, you must log the user out when the refresh token itself expires or becomes invalid. * Node.js * Express ```ts app.get('/account', auth.requiresAuth, (req, res) => { res.json({ sub: req.scalekitUser?.sub }); }); ``` * Next.js app/account/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.withAuth(async (request, { user }) => { return Response.json({ sub: user?.sub }); }); ``` * Python ```ts app.get('/account', auth.requiresAuth, (req, res) => { res.json({ sub: req.scalekitUser?.sub }); }); ``` * Go app/account/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.withAuth(async (request, { user }) => { return Response.json({ sub: user?.sub }); }); ``` * Java * Flask ```py @app.route("/account") @auth.requires_auth def account(): return {"sub": auth.current_user["sub"]} ``` * FastAPI ```py @app.get("/account") async def account(user: dict = Depends(auth.requires_auth)): return {"sub": user["sub"]} ``` * Express ```py @app.route("/account") @auth.requires_auth def account(): return {"sub": auth.current_user["sub"]} ``` * Next.js ```py @app.get("/account") async def account(user: dict = Depends(auth.requires_auth)): return {"sub": user["sub"]} ``` * Flask ```go 8 collapsed lines 1 import ( 2 "net/http" 3 "os" 4 ) 5 6 // Set SameSite mode for CSRF protection 7 c.SetSameSite(http.SameSiteStrictMode) 8 9 // Store access token in HttpOnly cookie with Path scoping to API routes 10 c.SetCookie( 11 "accessToken", 12 authResult.AccessToken, 13 authResult.ExpiresIn-60, // seconds in Gin 14 "/api", 15 "", 16 os.Getenv("GIN_MODE") == "release", 17 true, 18 ) 19 20 // Store refresh token in separate HttpOnly cookie with Path scoped to refresh endpoint 21 c.SetCookie( 22 "refreshToken", 23 authResult.RefreshToken, 24 0, // No expiry for refresh token cookie 25 "/auth/refresh", 26 "", 27 os.Getenv("GIN_MODE") == "release", 28 true, 29 ) ``` * FastAPI ```java 6 collapsed lines 1 import javax.servlet.http.Cookie; 2 import javax.servlet.http.HttpServletResponse; 3 4 // Store access token in HttpOnly cookie with Path scoping to API routes 5 Cookie accessTokenCookie = new Cookie("accessToken", authResult.getAccessToken()); 6 accessTokenCookie.setMaxAge(authResult.getExpiresIn() - 60); // seconds in Spring 7 accessTokenCookie.setHttpOnly(true); 8 accessTokenCookie.setSecure(true); 9 accessTokenCookie.setPath("/api"); 10 response.addCookie(accessTokenCookie); 11 12 // Store refresh token in separate HttpOnly cookie with Path scoped to refresh endpoint 13 Cookie refreshTokenCookie = new Cookie("refreshToken", authResult.getRefreshToken()); 14 refreshTokenCookie.setHttpOnly(true); 15 refreshTokenCookie.setSecure(true); 16 refreshTokenCookie.setPath("/auth/refresh"); 17 response.addCookie(refreshTokenCookie); 18 response.setHeader("Set-Cookie", 19 response.getHeader("Set-Cookie") + "; SameSite=Strict"); ``` This sets browser cookies with the session tokens. Every request to your backend needs to verify the `accessToken` to ensure the user is authenticated. If expired, use the `refreshToken` to get a new access token. * Node.js * Express ```ts app.get('/dashboard', auth.requiresAuth, (req, res) => { res.json({ user: req.scalekitUser }); }); ``` * Next.js middleware.ts ```ts import { auth } from './lib/auth'; export default auth.createMiddleware({ publicRoutes: ['/'], }); export const config = { runtime: 'nodejs', matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], }; ``` * Python ```ts app.get('/dashboard', auth.requiresAuth, (req, res) => { res.json({ user: req.scalekitUser }); }); ``` * Go middleware.ts ```ts import { auth } from './lib/auth'; export default auth.createMiddleware({ publicRoutes: ['/'], }); export const config = { runtime: 'nodejs', matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], }; ``` * Java * Flask ```py @app.route("/dashboard") @auth.requires_auth def dashboard(): return {"user": auth.current_user} ``` * FastAPI ```py @app.get("/dashboard") async def dashboard(user: dict = Depends(auth.requires_auth)): return {"user": user} ``` * Express ```py @app.route("/dashboard") @auth.requires_auth def dashboard(): return {"user": auth.current_user} ``` * Next.js ```py @app.get("/dashboard") async def dashboard(user: dict = Depends(auth.requires_auth)): return {"user": user} ``` * Flask ```go 5 collapsed lines 1 import ( 2 "context" 3 "net/http" 4 ) 5 6 // verifyToken is a middleware that ensures a valid access token or refreshes it if expired. 7 func verifyToken(next http.HandlerFunc) http.HandlerFunc { 8 return func(w http.ResponseWriter, r *http.Request) { 9 // Retrieve the access token from the user's cookie 10 cookie, err := r.Cookie("accessToken") 4 collapsed lines 11 if err != nil { 12 // No access token cookie found; reject the request 13 http.Error(w, `{"message": "No access token provided"}`, http.StatusUnauthorized) 14 return 15 } 16 17 accessToken := cookie.Value 18 19 // Decrypt the access token before validation 20 decryptedAccessToken, err := decrypt(accessToken) 5 collapsed lines 21 if err != nil { 22 // Could not decrypt access token; treat as invalid 23 http.Error(w, `{"message": "Token decryption failed"}`, http.StatusUnauthorized) 24 return 25 } 26 27 // Validate the access token using the Scalekit SDK 28 isValid, err := scalekitClient.ValidateAccessToken(r.Context(), decryptedAccessToken) 29 if err != nil || !isValid { 30 // Access token is invalid or expired 31 32 // Attempt to retrieve the stored refresh token 33 refreshToken, err := getStoredRefreshToken(r) 5 collapsed lines 34 if err != nil { 35 // No refresh token is available; cannot continue 36 http.Error(w, `{"message": "No refresh token available"}`, http.StatusUnauthorized) 37 return 38 } 39 40 // Use the refresh token to obtain a new access token from Scalekit 41 tokenResponse, err := scalekitClient.RefreshAccessToken(r.Context(), refreshToken) 5 collapsed lines 42 if err != nil { 43 // Refresh attempt failed; likely an expired or invalid refresh token 44 http.Error(w, `{"message": "Token refresh failed"}`, http.StatusUnauthorized) 45 return 46 } 47 48 // Save the new refresh token so it can be reused for future requests 49 err = storeRefreshToken(tokenResponse.RefreshToken) 5 collapsed lines 50 if err != nil { 51 // Could not store the new refresh token 52 http.Error(w, `{"message": "Failed to store refresh token"}`, http.StatusInternalServerError) 53 return 54 } 55 56 // Encrypt the new access token before setting it in the cookie 57 encryptedNewAccessToken, err := encrypt(tokenResponse.AccessToken) 5 collapsed lines 58 if err != nil { 59 // Could not encrypt new access token 60 http.Error(w, `{"message": "Token encryption failed"}`, http.StatusInternalServerError) 61 return 62 } 63 64 // Issue a new accessToken cookie with updated credentials 31 collapsed lines 65 newCookie := &http.Cookie{ 66 Name: "accessToken", 67 Value: encryptedNewAccessToken, 68 HttpOnly: true, 69 Secure: true, 70 Path: "/", 71 SameSite: http.SameSiteStrictMode, 72 } 73 http.SetCookie(w, newCookie) 74 75 // Mark the token as valid in the request context and proceed 76 r = r.WithContext(context.WithValue(r.Context(), "tokenValid", true)) 77 } else { 78 // The access token is valid; continue with marked context 79 r = r.WithContext(context.WithValue(r.Context(), "tokenValid", true)) 80 } 81 82 // Pass the request along to the next handler in the chain 83 next(w, r) 84 } 85 } 86 87 // dashboardHandler demonstrates a protected route that requires authentication. 88 func dashboardHandler(w http.ResponseWriter, r *http.Request) { 89 w.Header().Set("Content-Type", "application/json") 90 w.Write([]byte(`{ 91 "message": "This is a protected route", 92 "tokenValid": true 93 }`)) 94 } 95 96 // Usage example: 97 // Attach middleware to the /dashboard route: 98 // http.HandleFunc("/dashboard", verifyToken(dashboardHandler)) ``` * FastAPI ```java 6 collapsed lines 1 import javax.servlet.http.HttpServletRequest; 2 import javax.servlet.http.HttpServletResponse; 3 import javax.servlet.http.Cookie; 4 import org.springframework.web.servlet.HandlerInterceptor; 5 6 @Component 7 public class TokenVerificationInterceptor implements HandlerInterceptor { 8 @Override 9 public boolean preHandle( 10 HttpServletRequest request, 11 HttpServletResponse response, 12 Object handler 13 ) throws Exception { 14 try { 15 // Get access token from cookie 16 String accessToken = getCookieValue(request, "accessToken"); 17 String refreshToken = getCookieValue(request, "refreshToken"); 18 19 // Decrypt the tokens 20 String decryptedAccessToken = decrypt(accessToken); 21 String decryptedRefreshToken = decrypt(refreshToken); 22 23 // Use Scalekit SDK to validate the token 24 boolean isValid = scalekit.authentication().validateAccessToken(decryptedAccessToken); 25 26 27 // Use refreshToken to get a new access token 28 AuthenticationResponse tokenResponse = scalekit 29 .authentication() 30 .refreshToken(decryptedRefreshToken); 31 32 // Update the cookie with the new access token and refresh token 33 String encryptedNewAccessToken = encrypt(tokenResponse.getAccessToken()); 34 String encryptedNewRefreshToken = encrypt(tokenResponse.getRefreshToken()); 35 36 Cookie accessTokenCookie = new Cookie("accessToken", encryptedNewAccessToken); 37 accessTokenCookie.setHttpOnly(true); 38 accessTokenCookie.setSecure(true); 39 accessTokenCookie.setPath("/"); 40 response.addCookie(accessTokenCookie); 41 42 Cookie refreshTokenCookie = new Cookie("refreshToken", encryptedNewRefreshToken); 43 refreshTokenCookie.setHttpOnly(true); 44 refreshTokenCookie.setSecure(true); 45 refreshTokenCookie.setPath("/"); 46 response.addCookie(refreshTokenCookie); 47 48 return true; 49 } catch (Exception e) { 50 // handle exception 51 } 52 } 13 collapsed lines 53 54 private String getCookieValue(HttpServletRequest request, String cookieName) { 55 Cookie[] cookies = request.getCookies(); 56 if (cookies != null) { 57 for (Cookie cookie : cookies) { 58 if (cookieName.equals(cookie.getName())) { 59 return cookie.getValue(); 60 } 61 } 62 } 63 return null; 64 } 65 } ``` Authenticated users can access your dashboard. The app enforces session policies using session tokens. To change session policies, go to Dashboard > Authentication > Session Policy in the Scalekit dashboard. 5. ## Log out the user [Section titled “Log out the user”](#log-out-the-user) Session persistence depends on the session policy configured in the Scalekit dashboard. To log out a user, clear local session data and invalidate the user’s session in Scalekit. * Node.js * Express ```html Log out ``` * Next.js app/logout/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createLogoutHandler(); ``` ```html Log out ``` * Python ```html Log out ``` * Go app/logout/route.ts ```ts import { auth } from '../../lib/auth'; export const GET = auth.createLogoutHandler(); ``` ```html Log out ``` * Java * Flask ```html Log out ``` * FastAPI ```html Log out ``` * Express ```html Log out ``` * Next.js ```html Log out ``` * Flask ```go 8 collapsed lines 1 package main 2 3 import ( 4 "net/http" 5 "github.com/gin-gonic/gin" 6 "github.com/scalekit-inc/scalekit-sdk-go" 7 ) 8 9 func logoutHandler(c *gin.Context) { 10 // Clear all session data including cookies and local storage 11 clearSessionData() 12 13 // Generate Scalekit logout URL 14 options := scalekit.LogoutUrlOptions{ 15 IdTokenHint: idToken, 16 PostLogoutRedirectUri: postLogoutRedirectUri, 17 } 18 logoutUrl, err := scalekitClient.GetLogoutUrl(options) 19 if err != nil { 20 c.JSON(http.StatusInternalServerError, gin.H{ 21 "error": "Failed to generate logout URL", 22 }) 23 return 24 } 25 26 // Redirect to Scalekit's logout endpoint 27 // Note: This is a one-time use URL that becomes invalid after use 28 c.Redirect(http.StatusFound, logoutUrl.String()) 29 } ``` * FastAPI ```java 5 collapsed lines 1 import com.scalekit.internal.http.LogoutUrlOptions; 2 import org.springframework.web.bind.annotation.*; 3 import org.springframework.web.servlet.view.RedirectView; 4 import java.net.URL; 5 6 @RestController 7 public class LogoutController { 8 9 @GetMapping("/logout") 10 public RedirectView logout() { 11 12 clearSessionData(); 13 14 15 LogoutUrlOptions options = new LogoutUrlOptions(); 16 options.setIdTokenHint(idToken); 17 options.setPostLogoutRedirectUri(postLogoutRedirectUri); 18 19 URL logoutUrl = scalekit.authentication() 20 .getLogoutUrl(options); 21 22 23 // Note: This is a one-time use URL that becomes invalid after use 24 return new RedirectView(logoutUrl.toString()); 25 } 26 } ``` The logout process completes when Scalekit invalidates the user’s session and redirects them to your [registered post-logout URL](/guides/dashboard/redirects/#post-logout-url). This single integration unlocks multiple authentication methods, including Magic Link & OTP, social sign-ins, enterprise single sign-on (SSO), and robust user management features. As you continue working with Scalekit, you’ll discover even more features that enhance your authentication workflows. --- # DOCUMENT BOUNDARY --- # Add OAuth 2.1 authorization to MCP servers > Secure your Model Context Protocol (MCP) servers with Scalekit's drop-in OAuth 2.1 authorization solution and protect your AI integrations This guide shows you how to add production-ready OAuth 2.1 authorization to your Model Context Protocol (MCP) server using Scalekit. You’ll learn how to secure your MCP server so that only authenticated and authorized users can access your tools through AI hosts like Claude Desktop, Cursor, or VS Code. Using a coding agent?Copy MCP prompt See the integration in action [Play](https://youtube.com/watch?v=-gFAWf5aSLw) MCP servers expose tools that AI hosts can discover and execute to interact with your resources. For example: * A sales team member could use Claude Desktop to view customer information, update records, or set follow-up reminders * A developer could use VS Code or Cursor with a GitHub MCP server to perform everyday GitHub actions through chat * An autonomous agent could use an MCP server to perform actions such as look up the account details in a CRM system When you build MCP servers, multiple AI hosts may need to discover and use your server to interact with your resources. Scalekit handles the complex authentication and authorization for you, so you can focus on building better tools and improving functionality. Using FastMCP? If you’re using FastMCP, you can use Scalekit plugin and add auth to your MCP Server in just 5 lines of code. Please follow the [integration guide](/authenticate/mcp/fastmcp-quickstart). 1. ## Get Scalekit SDK [Section titled “Get Scalekit SDK”](#get-scalekit-sdk) To get started, make sure you have your Scalekit account and API credentials ready. If you haven’t created a Scalekit account yet, you can [sign up and get a free account](https://app.scalekit.com/ws/signup). Next, install the Scalekit SDK for your language: * Node.js ```bash npm install @scalekit-sdk/node ``` * Python ```sh pip install scalekit-sdk-python ``` Use the Scalekit dashboard to register your MCP server and configure MCP hosts (or AI agents following the MCP client protocol) to use Scalekit as the authorization server. The Scalekit SDK validates tokens after users have been authenticated and authorized to access your MCP server. 2. ## Add MCP server to get drop-in OAuth2.1 authorization server [Section titled “Add MCP server to get drop-in OAuth2.1 authorization server”](#add-mcp-server-to-get-drop-in-oauth21-authorization-server) In the Scalekit dashboard, go to **MCP servers** and select **Add MCP server**. ![Add MCP server](/.netlify/images?url=_astro%2Fmcp-create.wpqhshLD.png\&w=1068\&h=864\&dpl=6ae471fc143c9a4c27550463) 1. Provide a **name** for your MCP server to help you identify it easily. This name appears on the Consent page that MCP hosts display to users when authorizing access to your MCP server. 2. Enable **dynamic client registration** for MCP hosts. This allows MCP hosts to automatically register with Scalekit (and your authorization server), eliminating the need for manual registration and making it easier for users to adopt your MCP server secur. 3. Enable **Client ID Metadata Document (CIMD)** to allow your authorization server to fetch client metadata from MCP hosts and authorize them automatically. 4. Click **Save** to register the server. Note: If your MCP server is intended for use by public MCP clients such as Claude, Cursor, or VS Code, it is recommended to keep both DCR and CIMD enabled. Clients that support CIMD will use the CIMD flow, while clients that do not yet support CIMD can fall back to Dynamic Client Registration. This ensures your MCP server remains compatible with the widest range of MCP clients while preserving a smooth authorization experience. Toggling DCR or CIMD? If you enable or disable DCR or CIMD, be sure to restart your MCP server. Certain MCP frameworks, like FastMCP, cache authorization server details, and a restart ensures the updated configuration is correctly applied. Advanced settings * **Server URL**: Your MCP server’s unique identifier, typically your server’s URL (e.g., `https://mcp.yourapp.com`). This is an optional field. If not provided, Scalekit will use the generated resource_id as the resource identifier. If provided, access tokens minted by Scalekit will have the resource identifier as `aud` claim along with the Scalekit generated resource_id. * **Access token lifetime**: Recommended 300-3600 seconds (5 minutes to 1 hour). This setting applies to this MCP server only and is separate from the access token lifetime configured for a Web Application. * **Scopes**: Define the permissions your MCP server needs, such as `todo:read` or `todo:write`. These scopes are pre-approved when users authenticate to use your MCP server, streamlining the authorization process. 3. ## Let MCP clients discover your OAuth2.1 authorization server [Section titled “Let MCP clients discover your OAuth2.1 authorization server”](#let-mcp-clients-discover-your-oauth21-authorization-server) MCP protocol directs any MCP client to discover your OAuth2.1 authorization server by calling a public endpoint on your MCP server. This endpoint is called `.well-known/oauth-protected-resource` and your MCP server must host this endpoint. ![MCP server setup](/.netlify/images?url=_astro%2Fmcp-metadata.BIWBrsCY.png\&w=1126\&h=1326\&dpl=6ae471fc143c9a4c27550463) Copy the resource metadata JSON from **Dashboard > MCP Servers > Your server > Metadata JSON** and implement it in your `.well-known/oauth-protected-resource` endpoint. The `authorization_servers` field contains your Scalekit resource identifier, which clients use to initiate the OAuth flow. * Node.js ```javascript // MCP client discovery endpoint // Use case: Allow MCP clients to discover OAuth authorization server configuration app.get('/.well-known/oauth-protected-resource', (req, res) => { res.json({ // From Scalekit dashboard > MCP servers > Your server > Metadata JSON "authorization_servers": [ "https:///resources/" ], "bearer_methods_supported": [ "header" // Bearer token in Authorization header ], "resource": "https://mcp.yourapp.com", // Your MCP server URL "resource_documentation": "https://mcp.yourapp.com/docs", // A URL to the documentation of the resource server "scopes_supported": ["todo:read", "todo:write"] // Dashboard-configured scopes }); }); ``` * Python ```python from fastapi import FastAPI from fastapi.responses import JSONResponse app = FastAPI() # OAuth Protected Resource Metadata endpoint - Required for MCP client discovery # Copy the actual authorization server URL and metadata from your Scalekit dashboard. # The values shown here are examples - replace with your actual configuration. @app.get("/.well-known/oauth-protected-resource") async def get_oauth_protected_resource(): return JSONResponse({ "authorization_servers": [ "https:///resources/" ], "bearer_methods_supported": [ "header" ], "resource": "https://mcp.yourapp.com", "resource_documentation": "https://mcp.yourapp.com/docs", "scopes_supported": ["todo:read", "todo:write"] }) ``` 4. ## Validate all MCP client requests have a valid access token [Section titled “Validate all MCP client requests have a valid access token”](#validate-all-mcp-client-requests-have-a-valid-access-token) Your MCP server should validate that all incoming requests contain a valid access token. Leverage Scalekit SDKs to validate tokens and verify essential claims such as `aud` (audience), `iss` (issuer), `exp` (expiration), `iat` (issued at), and `scope` (permissions). The code in this step expects tokens whose issuer (`iss`) is `/resources/`. If you see the **Resource bound issuer** option on your MCP server in the Scalekit dashboard, your workspace was created before October 21, 2026. Until you enable that option, your tokens carry your environment URL as the issuer, and this code rejects them with a `401` and `unexpected "iss" claim value`. Follow [Migrate to the new MCP spec](/authenticate/mcp/migrate-to-2026-07-28-mcp-spec/) first. If you need any assistance or help, contact . * Node.js auth-config.js ```javascript 10 collapsed lines 1 import { Scalekit } from '@scalekit-sdk/node'; 2 3 // Initialize Scalekit client with environment credentials 4 // Reference installation guide for client setup details 5 const scalekit = new Scalekit( 6 process.env.SCALEKIT_ENVIRONMENT_URL, 7 process.env.SCALEKIT_CLIENT_ID, 8 process.env.SCALEKIT_CLIENT_SECRET 9 ); 10 11 // Resource configuration 12 // Get these values from Scalekit dashboard > MCP servers > Your server 13 // For FastMCP: Use base URL with trailing slash (e.g., https://mcp.example.com/) 14 const RESOURCE_ID = 'res_123456789'; // Resource ID (res_...) from Dashboard > MCP servers > Your server. Used as the token audience and in the issuer. 15 const METADATA_ENDPOINT = 'https://your-mcp-server.com/.well-known/oauth-protected-resource'; 16 17 // WWW-Authenticate header for unauthorized responses 18 // This helps clients understand how to authenticate properly 19 export const WWWHeader = { 20 HeaderKey: 'WWW-Authenticate', 21 HeaderValue: `Bearer realm="OAuth", resource_metadata="${METADATA_ENDPOINT}"` 22 }; ``` * Python auth_config.py ```python 12 collapsed lines 1 from scalekit import ScalekitClient 2 from scalekit.common.scalekit import TokenValidationOptions 3 import os 4 5 # Initialize Scalekit client with environment credentials 6 # Reference installation guide for client setup details 7 scalekit_client = ScalekitClient( 8 env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), 9 client_id=os.getenv("SCALEKIT_CLIENT_ID"), 10 client_secret=os.getenv("SCALEKIT_CLIENT_SECRET") 11 ) 12 13 # Resource configuration 14 # Get these values from Scalekit dashboard > MCP servers > Your server 15 # For FastMCP: Use base URL with trailing slash (e.g., https://mcp.example.com/) 16 RESOURCE_ID = "res_123456789" # Resource ID (res_...) from Dashboard > MCP servers > Your server. Used as the token audience and in the issuer. 17 METADATA_ENDPOINT = "https://your-mcp-server.com/.well-known/oauth-protected-resource" 18 19 # WWW-Authenticate header for unauthorized responses 20 # This helps clients understand how to authenticate properly 21 WWW_HEADER = { 22 "WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{METADATA_ENDPOINT}"' 23 } ``` Extract the Bearer token from incoming MCP client requests. MCP clients send tokens in the `Authorization: Bearer ` header format. * Node.js ```javascript // Extract Bearer token from Authorization header // Use case: Validate requests from AI hosts like Claude Desktop, Cursor, or VS Code const authHeader = req.headers['authorization']; const token = authHeader?.startsWith('Bearer ') ? authHeader.split('Bearer ')[1]?.trim() : null; if (!token) { throw new Error('Missing or invalid Bearer token'); } ``` * Python ```python # Extract Bearer token from Authorization header # Use case: Validate requests from AI hosts like Claude Desktop, Cursor, or VS Code auth_header = request.headers.get("Authorization", "") token = None if auth_header.startswith("Bearer "): token = auth_header.split("Bearer ")[1].strip() if not token: raise ValueError("Missing or invalid Bearer token") ``` Validate the token’s issuer and audience to ensure it was issued for your specific MCP server. The issuer (`iss`) tells you who minted the token. For your MCP server, it is your environment URL followed by `/resources/` and your resource ID. Set `RESOURCE_ID` to the resource ID (`res_...`) shown for your MCP server in the Scalekit dashboard. Tokens for your MCP server also include it as an audience. * Node.js Validate token ```javascript // Security: Validate token against configured resource audience // This ensures the token was issued for your specific MCP server const ENV_URL = process.env.SCALEKIT_ENVIRONMENT_URL; await scalekit.validateToken(token, { // Issuer is your environment URL followed by this MCP server's resource path issuer: `${ENV_URL}/resources/${RESOURCE_ID}`, audience: [RESOURCE_ID] }); ``` * Python Validate token ```python 1 ENV_URL = os.getenv("SCALEKIT_ENVIRONMENT_URL") 2 # Issuer is your environment URL followed by this MCP server's resource path 3 ISSUER = f"{ENV_URL}/resources/{RESOURCE_ID}" 4 5 # Method 1: validate_access_token - Returns boolean (True/False) 6 # Use this method when you only need to verify token validity without detailed error information. 7 # This approach is suitable for simple authorization checks where you don't need token claims. 8 def validate_token_with_issuer_audience(token: str) -> bool: 9 """ 10 Validates a token and returns True if valid, False otherwise. 11 12 :param token: The token to validate 13 :return: True if token is valid, False otherwise 14 """ 15 options = TokenValidationOptions( 16 issuer=ISSUER, 17 audience=[RESOURCE_ID] 18 ) 19 20 try: 21 is_valid = scalekit_client.validate_access_token(token, options=options) 22 return is_valid 23 except Exception as ex: 24 print(f"Token validation failed: {ex}") 25 return False 26 27 # Method 2: validate_token - Returns token claims/payload 28 # Use this method when you need access to token claims (user info, scopes, etc.) or detailed error information. 29 # This approach is suitable for authorization that requires specific user context or scope validation. 30 def validate_token_and_get_claims(token: str) -> dict: 31 """ 32 Validates a token with specific audience and raises exception on failure. 33 34 :param token: The token to validate 35 :raises: ScalekitValidateTokenFailureException if validation fails 36 """ 37 options = TokenValidationOptions( 38 issuer=ISSUER, 39 audience=[RESOURCE_ID], 40 required_scopes=["todo:read", "todo:write"] # Optional: validate specific scopes for finer access control 41 ) 42 43 scalekit_client.validate_token(token, options=options) ``` * Go middleware/auth.go ```go 1 // Security: validate the token's issuer and audience — confirms it was issued by 2 // your Scalekit environment for your specific MCP server. 3 envURL := os.Getenv("SCALEKIT_ENVIRONMENT_URL") 4 5 ok, err := scalekitClient.ValidateTokenWithOptions(ctx, token, &scalekit.ValidateTokenOptions{ 6 // Issuer is your environment URL followed by this MCP server's resource path 7 Issuer: envURL + "/resources/" + RESOURCE_ID, 8 Audience: []string{RESOURCE_ID}, 9 }) 10 if err != nil || !ok { 11 // Reject: return 401 with the WWW-Authenticate header and stop processing. 12 w.Header().Set("WWW-Authenticate", wwwAuthenticateHeader) 13 http.Error(w, "invalid token", http.StatusUnauthorized) 14 return 15 } 16 // Token is valid — continue handling the request. 17 18 // Prefer validating scopes per tool-call rather than in this global check, so each 19 // tool enforces only the scope it needs. ``` * Java middleware/AuthMiddleware.java ```java 1 // Security: validate the token's issuer and audience — confirms it was issued by 2 // your Scalekit environment for your specific MCP server. 3 String envUrl = System.getenv("SCALEKIT_ENVIRONMENT_URL"); 4 5 boolean isValid = scalekitClient.authentication().validateAccessToken( 6 token, 7 TokenValidationOptions.builder() 8 // Issuer is your environment URL followed by this MCP server's resource path 9 .issuer(envUrl + "/resources/" + RESOURCE_ID) 10 .audience(List.of(RESOURCE_ID)) 11 .build() 12 ); 13 if (!isValid) { 14 // Reject: return 401 with the WWW-Authenticate header and stop processing. 15 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 16 response.setHeader("WWW-Authenticate", wwwAuthenticateHeader); 17 return; 18 } 19 // Token is valid — continue handling the request. 20 21 // Prefer enforcing scopes per tool-call rather than in this global check: read the 22 // token's "scope" claim via validateAccessTokenAndGetClaims(token, options) inside 23 // each tool and verify it contains the scope that tool needs. ``` #### Complete middleware implementation [Section titled “Complete middleware implementation”](#complete-middleware-implementation) Combine token extraction and validation into a complete authentication middleware that protects all your MCP endpoints. * Node.js ```javascript import { Scalekit } from '@scalekit-sdk/node'; import { NextFunction, Request, Response } from 'express'; const scalekit = new Scalekit( process.env.SCALEKIT_ENVIRONMENT_URL, process.env.SCALEKIT_CLIENT_ID, process.env.SCALEKIT_CLIENT_SECRET ); const RESOURCE_ID = 'res_123456789'; // Resource ID (res_...) from Dashboard > MCP servers > Your server. Used as the token audience and in the issuer. const METADATA_ENDPOINT = 'https://your-mcp-server.com/.well-known/oauth-protected-resource'; export const WWWHeader = { HeaderKey: 'WWW-Authenticate', HeaderValue: `Bearer realm="OAuth", resource_metadata="${METADATA_ENDPOINT}"` }; export async function authMiddleware(req: Request, res: Response, next: NextFunction) { try { // Security: Allow public access to well-known endpoints for metadata discovery // This enables MCP clients to discover your OAuth configuration if (req.path.includes('.well-known')) { return next(); } // Extract Bearer token from Authorization header const authHeader = req.headers['authorization']; const token = authHeader?.startsWith('Bearer ') ? authHeader.split('Bearer ')[1]?.trim() : null; if (!token) { throw new Error('Missing or invalid Bearer token'); } // Security: Validate token against configured resource audience const envUrl = process.env.SCALEKIT_ENVIRONMENT_URL; await scalekit.validateToken(token, { issuer: `${envUrl}/resources/${RESOURCE_ID}`, audience: [RESOURCE_ID] }); next(); } catch (err) { // Return proper OAuth 2.0 error response with WWW-Authenticate header return res .status(401) .set(WWWHeader.HeaderKey, WWWHeader.HeaderValue) .end(); } } // Apply authentication middleware to all MCP endpoints app.use('/', authMiddleware); ``` * Python ```python from scalekit import ScalekitClient from scalekit.common.scalekit import TokenValidationOptions from fastapi import Request, HTTPException, status from fastapi.responses import Response import os scalekit_client = ScalekitClient( env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET") ) RESOURCE_ID = "res_123456789" # Resource ID (res_...) from Dashboard > MCP servers > Your server. Used as the token audience and in the issuer. METADATA_ENDPOINT = "https://your-mcp-server.com/.well-known/oauth-protected-resource" # WWW-Authenticate header for unauthorized responses WWW_HEADER = { "WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{METADATA_ENDPOINT}"' } async def auth_middleware(request: Request, call_next): # Security: Allow public access to well-known endpoints for metadata discovery if request.url.path.startswith("/.well-known"): return await call_next(request) # Extract Bearer token from Authorization header auth_header = request.headers.get("Authorization", "") token = None if auth_header.startswith("Bearer "): token = auth_header.split("Bearer ")[1].strip() if not token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, headers=WWW_HEADER ) # Security: Validate token against configured resource audience try: env_url = os.getenv("SCALEKIT_ENVIRONMENT_URL") options = TokenValidationOptions( issuer=f"{env_url}/resources/{RESOURCE_ID}", audience=[RESOURCE_ID] ) scalekit_client.validate_token(token, options=options) except Exception: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, headers=WWW_HEADER ) return await call_next(request) # Apply authentication middleware to all MCP endpoints app.middleware("http")(auth_middleware) ``` 5. ## Implement scope-based tool authorization Optional [Section titled “Implement scope-based tool authorization ”](#implement-scope-based-tool-authorization-) Add scope validation at the MCP tool execution level to ensure tools are only executed when the user has authorized the MCP client with the required permissions. This provides fine-grained access control and follows the principle of least privilege. * Node.js ```diff 1 // Security: Validate token has required scope for this specific tool execution 2 // Use case: Ensure users only have access to authorized MCP tools 3 try { 4 await scalekit.validateToken( 5 token, { 6 audience: [RESOURCE_ID], 7 requiredScopes: [scope] 8 } 9 ); 10 } catch(error) { 11 // Return OAuth 2.0 compliant error for insufficient scope 12 return res.status(403).json({ 13 error: 'insufficient_scope', 14 error_description: `Required scope: ${scope}`, 15 scope: scope 16 }); 17 } ``` * Python ```diff 1 # Security: Validate token has required scope for this specific tool execution 2 # Use case: Ensure users only have access to authorized MCP tools 3 try: 4 scalekit_client.validate_access_token( 5 token, 6 options=TokenValidationOptions( 7 audience=[RESOURCE_ID], 8 +required_scopes=[scope] 9 ) 10 ) 11 except ScalekitValidateTokenFailureException as ex: 12 # Return OAuth 2.0 compliant error for insufficient scope 13 return { 14 "error": "insufficient_scope", 15 "error_description": f"Required scope: {scope}", 16 "scope": scope 17 } ``` Fine-grained access control Implement scope-based authorization to provide granular control over which tools and resources each client can access. This improves security by limiting potential damage from compromised tokens and ensures users only access appropriate MCP functionality. 6. ## Enable additional authentication methods [Section titled “Enable additional authentication methods”](#enable-additional-authentication-methods) Beyond the OAuth 2.1 authorization you’ve implemented, you can enable additional authentication methods that work seamlessly with your MCP server’s token validation: **[Enterprise SSO](/mcp/auth-methods/enterprise/)** Enable organizations to authenticate through their identity providers (Okta, Azure AD, Google Workspace). Your MCP server continues validating tokens the same way, while Scalekit handles: * Centralized access control through existing enterprise identity systems * Single sign-on experience for organization members * Compliance with corporate security policies Organization owned domains Authentication through Enterprise SSO for MCP users requires the organization administrators to register the domain their organization owns with Scalekit through [the admin portal](/sso/guides/onboard-enterprise-customers/). **[Social logins](/mcp/auth-methods/social/)** Allow users to authenticate via Google, GitHub, Microsoft, and other social providers. Your existing token validation logic remains unchanged while providing: * Quick onboarding for individual users * Familiar authentication experience * Reduced friction for personal and small team use cases These authentication methods require no changes to your MCP server implementation—you continue validating tokens exactly as shown in the previous steps. **[Bring your own auth](/mcp/auth-methods/custom-auth/)** allows you to use your own authentication system to authenticate users to your MCP server. Your MCP server now has production-ready OAuth 2.1 authorization! You’ve successfully implemented a secure authorization flow that protects your MCP tools and ensures only authenticated users can access them through AI hosts. **Try the demo**: Download and run our [sample MCP server](https://github.com/scalekit-inc/mcp-auth-demos) with authentication already configured to see the complete integration in action. Production deployment checklist Before deploying to production, ensure you: * Configure proper CORS policies for your MCP server endpoints * Set up monitoring and logging for authorization events * Use HTTPS for all communications * Store client secrets securely using environment variables or secret management systems * Configure appropriate token lifetimes based on your security requirements * Test with various AI hosts (Claude Desktop, Cursor, VS Code) to verify compatibility * Configure a [custom domain](/guides/custom-domain/) for your Scalekit environment so the OAuth consent screen shows a branded URL (e.g., `auth.yourapp.com`) instead of the auto-generated one In summary, **Scalekit OAuth authorization server** Acts as the identity provider for your MCP server. * Authenticates users and agents * Issues access tokens with fine-grained scopes * Manages OAuth 2.1 flows (authorization code, client credentials) * Supports dynamic client registration for easy onboarding **Your MCP server** Validates incoming access tokens and enforces the permissions encoded in each token. Only requests with valid, authorized tokens are allowed. This separation of responsibilities ensures a clear boundary: Scalekit handles identity and token issuance, while your MCP server focuses on business logic of executing the actual tool calls. --- # DOCUMENT BOUNDARY --- # Add Modular SCIM provisioning > Automate user provisioning with SCIM. Directory API and webhooks for real-time user data sync This guide shows you how to automate user provisioning with SCIM using Scalekit’s Directory API and webhooks. You’ll learn to sync user data in real-time, create webhook endpoints for instant updates, and build automated provisioning workflows that keep your application’s user data synchronized with your customers’ directory providers. See the SCIM provisioning in action [Play](https://youtube.com/watch?v=SBJLtQaIbUk) With [SCIM Provisioning](/directory/guides/user-provisioning-basics) from Scalekit, you can: * Use **webhooks** to listen for events from your customers’ directory providers (e.g., user updates, group changes) * Use **REST APIs** to list users, groups, and directories on demand Scalekit abstracts the complexities of various directory providers, giving you a single interface to automate user lifecycle management. This enables you to create accounts for new hires during onboarding, deactivate accounts when employees depart, and adjust access levels as employees change roles. Review the SCIM provisioning sequence ![SCIM provisioning sequence: directory changes flow to Scalekit and on to your app through webhooks](/d2/docs/directory/scim/quickstart-0.svg) ![SCIM Quickstart](/.netlify/images?url=_astro%2Fscim-chart.D8FO-9f1.png\&w=5776\&h=1924\&dpl=6ae471fc143c9a4c27550463) Using a coding agent?Copy SCIM prompt ## User provisioning with Scalekit’s directory API [Section titled “User provisioning with Scalekit’s directory API”](#user-provisioning-with-scalekits-directory-api) Scalekit’s directory API allows you to fetch information about users, groups, and directories associated with an organization on-demand. This approach is ideal for scheduled synchronization tasks, bulk data imports, or when you need to ensure your application’s user data matches the latest directory provider state. Let’s explore how to use the Directory API to retrieve user and group data programmatically. 1. ### Setting up the SDK [Section titled “Setting up the SDK”](#setting-up-the-sdk) Before you begin, ensure that your organization [has a directory set up in Scalekit](/guides/user-management/scim-provisioning/). Scalekit offers language-specific SDKs for fast SSO integration. Use the installation instructions below for your technology stack: * Node.js ```bash npm install @scalekit-sdk/node ``` * Python ```sh pip install scalekit-sdk-python ``` * Go ```sh go get -u github.com/scalekit-inc/scalekit-sdk-go ``` * Java ```groovy /* Gradle users - add the following to your dependencies in build file */ implementation "com.scalekit:scalekit-sdk-java:2.1.3" ``` ```xml com.scalekit scalekit-sdk-java 2.1.3 ``` Navigate to **Dashboard > Developers > Settings > API Credentials** to obtain your credentials. Store your credentials securely in environment variables: .env ```shell # Get these values from Dashboard > Developers > Settings > API Credentials SCALEKIT_ENVIRONMENT_URL='https://b2b-app-dev.scalekit.com' SCALEKIT_CLIENT_ID='' SCALEKIT_CLIENT_SECRET='' ``` 2. ### Initialize the SDK and make your first API call [Section titled “Initialize the SDK and make your first API call”](#initialize-the-sdk-and-make-your-first-api-call) Initialize the Scalekit client with your environment variables and make your first API call to list organizations. * cURL Terminal ```bash # Security: Replace with a valid access token from Scalekit # This token authorizes your API requests to access organization data # Use case: Verify API connectivity and test authentication # Examples: Initial setup testing, debugging integration issues curl -L "https://$SCALEKIT_ENVIRONMENT_URL/api/v1/organizations?page_size=5" \ -H "Authorization: Bearer " ``` * Node.js Node.js ```javascript 4 collapsed lines 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 3 // Initialize Scalekit client with environment variables 4 // Security: Always use environment variables for sensitive credentials 5 const scalekit = new ScalekitClient( 6 process.env.SCALEKIT_ENVIRONMENT_URL, 7 process.env.SCALEKIT_CLIENT_ID, 8 process.env.SCALEKIT_CLIENT_SECRET, 9 ); 10 11 try { 12 // Use case: Retrieve organizations for bulk user provisioning workflows 13 // Examples: Multi-tenant applications, enterprise customer onboarding 14 const { organizations } = await scalekit.organization.listOrganization({ 15 pageSize: 5, 16 }); 17 18 console.log(`Organization name: ${organizations[0].display_name}`); 19 console.log(`Organization ID: ${organizations[0].id}`); 20 } catch (error) { 21 console.error('Failed to list organizations:', error); 22 // Handle error appropriately for your application 23 } ``` * Python Python ```python 4 collapsed lines 1 from scalekit import ScalekitClient 2 import os 3 4 # Initialize the SDK client with environment variables 5 # Security: Use os.getenv() to securely access credentials 6 scalekit_client = ScalekitClient( 7 env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), 8 client_id=os.getenv("SCALEKIT_CLIENT_ID"), 9 client_secret=os.getenv("SCALEKIT_CLIENT_SECRET") 10 ) 11 12 try: 13 # Use case: Sync user data across multiple organizations 14 # Examples: Scheduled provisioning tasks, HR system integration 15 org_list = scalekit_client.organization.list_organizations(page_size=100) 16 17 if org_list: 18 print(f'Organization details: {org_list[0]}') 19 print(f'Organization ID: {org_list[0].id}') 20 except Exception as error: 21 print(f'Error listing organizations: {error}') 22 # Implement appropriate error handling for your use case ``` * Go Go ```go 10 collapsed lines 1 package main 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 8 "github.com/scalekit/scalekit-go" 9 ) 10 11 // Initialize Scalekit client with environment variables 12 // Security: Always load credentials from environment, not hardcoded 13 scalekitClient := scalekit.NewScalekitClient( 14 os.Getenv("SCALEKIT_ENVIRONMENT_URL"), 15 os.Getenv("SCALEKIT_CLIENT_ID"), 16 os.Getenv("SCALEKIT_CLIENT_SECRET"), 17 ) 18 19 // Use case: Get specific organization for directory sync operations 20 // Examples: Targeted user provisioning, organization-specific workflows 21 organization, err := scalekitClient.Organization.GetOrganization( 22 ctx, 23 organizationId, 24 ) 25 if err != nil { 26 // Handle error appropriately for your application 27 return fmt.Errorf("failed to get organization: %w", err) 28 } ``` * Java Java ```java 8 collapsed lines 1 import com.scalekit.ScalekitClient; 2 3 // Initialize Scalekit client with environment variables 4 // Security: Use System.getenv() to securely access credentials 5 ScalekitClient scalekitClient = new ScalekitClient( 6 System.getenv("SCALEKIT_ENVIRONMENT_URL"), 7 System.getenv("SCALEKIT_CLIENT_ID"), 8 System.getenv("SCALEKIT_CLIENT_SECRET") 9 ); 10 11 try { 12 // Use case: List organizations for automated provisioning workflows 13 // Examples: Enterprise customer setup, multi-tenant management 14 ListOrganizationsResponse organizations = scalekitClient.organizations() 15 .listOrganizations(5, ""); 16 17 if (!organizations.getOrganizations().isEmpty()) { 18 Organization firstOrg = organizations.getOrganizations().get(0); 19 System.out.println("Organization name: " + firstOrg.getDisplayName()); 20 System.out.println("Organization ID: " + firstOrg.getId()); 21 } 22 } catch (ScalekitException error) { 23 System.err.println("Failed to list organizations: " + error.getMessage()); 24 // Implement appropriate error handling 25 } ``` 3. ### Retrieve a directory [Section titled “Retrieve a directory”](#retrieve-a-directory) After successfully listing organizations, you’ll need to retrieve the specific directory to begin syncing user and group data. You can retrieve directories using either the organization and directory IDs, or fetch the primary directory for an organization. * Node.js Node.js ```javascript try { // Use case: Get specific directory when organization has multiple directories // Examples: Department-specific provisioning, multi-division companies const { directory } = await scalekit.directory.getDirectory('', ''); console.log(`Directory name: ${directory.name}`); // Use case: Get primary directory for simple provisioning workflows // Examples: Small organizations, single-directory setups const { directory } = await scalekit.directory.getPrimaryDirectoryByOrganizationId(''); console.log(`Primary directory ID: ${directory.id}`); } catch (error) { console.error('Failed to retrieve directory:', error); // Handle error appropriately for your application } ``` * Python Python ```python 1 try: 2 # Use case: Access specific directory for targeted user sync operations 3 # Examples: Regional offices, business unit-specific provisioning 4 directory = scalekit_client.directory.get_directory( 5 organization_id='', directory_id='' 6 ) 7 print(f'Directory name: {directory.name}') 8 9 # Use case: Get primary directory for streamlined user management 10 # Examples: Standard employee provisioning, main company directory 11 primary_directory = scalekit_client.directory.get_primary_directory_by_organization_id( 12 organization_id='' 13 ) 14 print(f'Primary directory ID: {primary_directory.id}') 15 except Exception as error: 16 print(f'Error retrieving directory: {error}') 17 # Implement appropriate error handling ``` * Go Go ```go // Use case: Retrieve specific directory for granular access control // Examples: Multi-tenant environments, department-level provisioning directory, err := scalekitClient.Directory().GetDirectory(ctx, organizationId, directoryId) if err != nil { return fmt.Errorf("failed to get directory: %w", err) } fmt.Printf("Directory name: %s\n", directory.Name) // Use case: Get primary directory for simplified user management // Examples: Automated provisioning workflows, bulk user imports directory, err := scalekitClient.Directory().GetPrimaryDirectoryByOrganizationId(ctx, organizationId) if err != nil { return fmt.Errorf("failed to get primary directory: %w", err) } fmt.Printf("Primary directory ID: %s\n", directory.ID) ``` * Java Java ```java 1 try { 2 // Use case: Access specific directory for detailed user management 3 // Examples: Custom provisioning logic, directory-specific rules 4 Directory directory = scalekitClient.directories() 5 .getDirectory("", ""); 6 System.out.println("Directory name: " + directory.getName()); 7 8 // Use case: Get primary directory for standard provisioning workflows 9 // Examples: Employee onboarding, automated user sync 10 Directory primaryDirectory = scalekitClient.directories() 11 .getPrimaryDirectoryByOrganizationId(""); 12 System.out.println("Primary directory ID: " + primaryDirectory.getId()); 13 } catch (ScalekitException error) { 14 System.err.println("Failed to retrieve directory: " + error.getMessage()); 15 // Implement appropriate error handling 16 } ``` 4. ### List users in a directory [Section titled “List users in a directory”](#list-users-in-a-directory) Once you have the directory information, you can fetch users within that directory. This is commonly used for bulk user synchronization and maintaining an up-to-date user database. * Node.js Node.js ```javascript try { // Use case: Bulk user synchronization and provisioning // Examples: New customer onboarding, scheduled user data sync const { users } = await scalekit.directory.listDirectoryUsers('', ''); // Process each user for provisioning or updates users.forEach(user => { console.log(`User email: ${user.email}, Name: ${user.name}`); // TODO: Implement your user provisioning logic here }); } catch (error) { console.error('Failed to list directory users:', error); // Handle error appropriately for your application } ``` * Python Python ```python try: # Use case: Automated user provisioning workflows # Examples: HR system integration, bulk user imports directory_users = scalekit_client.directory.list_directory_users( organization_id='', directory_id='' ) # Process each user for local database updates for user in directory_users: print(f'User email: {user.email}, Name: {user.name}') # TODO: Implement your user synchronization logic here except Exception as error: print(f'Error listing directory users: {error}') # Implement appropriate error handling ``` * Go Go ```go 1 // Configure pagination options for large user directories 2 options := &ListDirectoryUsersOptions{ 3 PageSize: 50, // Adjust based on your needs 4 PageToken: "", 5 } 6 7 // Use case: Paginated user retrieval for large directories 8 // Examples: Enterprise customer provisioning, regular sync jobs 9 directoryUsers, err := scalekitClient.Directory().ListDirectoryUsers(ctx, organizationId, directoryId, options) 10 if err != nil { 11 return fmt.Errorf("failed to list directory users: %w", err) 12 } 13 14 // Process each user 15 for _, user := range directoryUsers.Users { 16 fmt.Printf("User email: %s, Name: %s\n", user.Email, user.Name) 17 // TODO: Implement your user provisioning logic 18 } ``` * Java Java ```java 1 // Configure options for user listing with pagination 2 var options = ListDirectoryResourceOptions.builder() 3 .pageSize(50) // Adjust based on your requirements 4 .pageToken("") 5 .includeDetail(true) // Include detailed user information 6 .build(); 7 8 try { 9 // Use case: Enterprise user management and synchronization 10 // Examples: Scheduled sync tasks, user provisioning automation 11 ListDirectoryUsersResponse usersResponse = scalekitClient.directories() 12 .listDirectoryUsers(directory.getId(), organizationId, options); 13 14 // Process each user for provisioning 15 for (User user : usersResponse.getUsers()) { 16 System.out.println("User email: " + user.getEmail() + ", Name: " + user.getName()); 17 // TODO: Implement your user provisioning logic here 18 } 19 } catch (ScalekitException error) { 20 System.err.println("Failed to list directory users: " + error.getMessage()); 21 // Implement appropriate error handling 22 } ``` Customer onboarding use case When setting up a new customer account, use the `listDirectoryUsers` function to automatically connect to their directory and start syncing user data. This enables immediate user provisioning without manual user creation. Deactivated and deleted users The list returns active users only. A user who is deactivated or deleted in the directory provider no longer appears, and `updated_after` doesn’t return them either. To find users removed while your app wasn’t receiving webhooks, page through the full list and compare it with your records. See [Recover from missed events](/guides/webhooks-best-practices/#recover-from-missed-events). 5. ### List groups in a directory [Section titled “List groups in a directory”](#list-groups-in-a-directory) Groups are essential for implementing role-based access control (RBAC) in your application. After retrieving users, you can fetch groups to manage permissions and access levels based on organizational structure. * Node.js Node.js ```javascript 1 try { 2 // Use case: Role-based access control implementation 3 // Examples: Department-level permissions, project-based access 4 const { groups } = await scalekit.directory.listDirectoryGroups( 5 '', 6 '', 7 ); 8 9 // Process each group for RBAC setup 10 groups.forEach(group => { 11 console.log(`Group name: ${group.name}, ID: ${group.id}`); 12 // TODO: Implement your group-based permission logic here 13 }); 14 } catch (error) { 15 console.error('Failed to list directory groups:', error); 16 // Handle error appropriately for your application 17 } ``` * Python Python ```python try: # Use case: Department-based access control # Examples: Engineering vs Sales permissions, project team access directory_groups = scalekit_client.directory.list_directory_groups( directory_id='', organization_id='' ) # Process each group for permission mapping for group in directory_groups: print(f'Group name: {group.name}, ID: {group.id}') # TODO: Implement your group-based permission logic here except Exception as error: print(f'Error listing directory groups: {error}') # Implement appropriate error handling ``` * Go Go ```go 1 // Configure pagination for group listing 2 options := &ListDirectoryGroupsOptions{ 3 PageSize: 25, // Adjust based on expected group count 4 PageToken: "", 5 } 6 7 // Use case: Organizational role management 8 // Examples: Enterprise role hierarchy, department-based access 9 directoryGroups, err := scalekitClient.Directory().ListDirectoryGroups(ctx, organizationId, directoryId, options) 10 if err != nil { 11 return fmt.Errorf("failed to list directory groups: %w", err) 12 } 13 14 // Process each group for RBAC implementation 15 for _, group := range directoryGroups.Groups { 16 fmt.Printf("Group name: %s, ID: %s\n", group.Name, group.ID) 17 // TODO: Implement your group-based permission logic 18 } ``` * Java Java ```java 1 // Configure options for detailed group information 2 var options = ListDirectoryResourceOptions.builder() 3 .pageSize(25) // Adjust based on your requirements 4 .pageToken("") 5 .includeDetail(true) // Include group membership details 6 .build(); 7 8 try { 9 // Use case: Enterprise permission management 10 // Examples: Role assignments, access level configurations 11 ListDirectoryGroupsResponse groupsResponse = scalekitClient.directories() 12 .listDirectoryGroups(directory.getId(), organizationId, options); 13 14 // Process each group for permission mapping 15 for (Group group : groupsResponse.getGroups()) { 16 System.out.println("Group name: " + group.getName() + ", ID: " + group.getId()); 17 // TODO: Implement your group-based permission logic here 18 } 19 } catch (ScalekitException error) { 20 System.err.println("Failed to list directory groups: " + error.getMessage()); 21 // Implement appropriate error handling 22 } ``` Role-based access control Use group information to implement role-based access control in your application. Map directory groups to application roles and permissions to automatically assign access levels based on a user’s organizational memberships. Scalekit’s Directory API provides a simple way to fetch user and group information on-demand. Refer to our [API reference](https://docs.scalekit.com/apis/) to explore more capabilities. ## Realtime user provisioning with webhooks [Section titled “Realtime user provisioning with webhooks”](#realtime-user-provisioning-with-webhooks) While the Directory API is perfect for scheduled synchronization, webhooks enable immediate, real-time user provisioning. When directory providers send events to Scalekit, we forward them instantly to your application, allowing you to respond to user changes as they happen. This approach is ideal for scenarios requiring immediate action, such as new employee onboarding or emergency access revocation. 1. ### Create a secure webhook endpoint [Section titled “Create a secure webhook endpoint”](#create-a-secure-webhook-endpoint) Create a webhook endpoint to receive real-time events from directory providers. After implementing your endpoint, register it in **Dashboard > Webhooks** where you’ll receive a secret for payload verification. Critical security requirement Always verify webhook signatures before processing events. This prevents unauthorized parties from triggering your provisioning logic and protects against replay attacks. * Node.js Express.js ```javascript 1 app.post( 2 '/webhook', 3 express.raw({ type: 'application/json' }), 4 async (req, res) => { 5 const secret = process.env.SCALEKIT_WEBHOOK_SECRET 6 const payload = req.body.toString() 7 const headers = { 8 'webhook-id': req.headers['webhook-id'], 9 'webhook-timestamp': req.headers['webhook-timestamp'], 10 'webhook-signature': req.headers['webhook-signature'], 11 } 12 13 try { 14 scalekit.verifyWebhookPayload(secret, headers, payload) 15 } catch (error) { 16 console.error('Webhook signature verification failed:', error) 17 return res.status(401).json({ error: 'Invalid signature' }) 18 } 19 20 const event = JSON.parse(payload) 21 22 try { 23 const { email, name } = event.data 24 25 // Process the webhook event based on its type 26 switch (event.type) { 27 case 'organization.directory.user_created': 28 await createUserAccount(email, name); 29 break; 30 case 'organization.directory.user_updated': 31 await updateUserAccount(email, name); 32 break; 33 case 'organization.directory.user_deleted': 34 await deactivateUserAccount(email); 35 break; 36 default: 37 console.log(`Unhandled event type: ${event.type}`); 38 } 39 40 res.status(201).json({ message: 'Webhook processed successfully' }); 41 } catch (processingError) { 42 console.error('Failed to process webhook event:', processingError); 43 res.status(500).json({ error: 'Processing failed' }); 44 } 45 }); ``` * Python FastAPI ```python 1 from fastapi import FastAPI, Request, HTTPException 2 import os 3 import json 4 5 app = FastAPI() 6 7 @app.post("/webhook") 8 async def api_webhook(request: Request): 9 # Security: ALWAYS verify webhook signatures before processing events 10 # This prevents unauthorized webhook calls and replay attacks 11 12 raw_body = await request.body() 13 headers = { 14 "webhook-id": request.headers.get("webhook-id"), 15 "webhook-timestamp": request.headers.get("webhook-timestamp"), 16 "webhook-signature": request.headers.get("webhook-signature"), 17 } 18 19 try: 20 scalekit_client.verify_webhook_payload( 21 secret=os.getenv("SCALEKIT_WEBHOOK_SECRET"), 22 headers=headers, 23 payload=raw_body, 24 ) 25 except Exception as verification_error: 26 print(f"Webhook verification failed: {verification_error}") 27 raise HTTPException(status_code=401, detail="Webhook verification failed") 28 29 body = json.loads(raw_body.decode("utf-8")) 30 31 try: 32 event_type = body.get("type") 33 event_data = body.get("data", {}) 34 email = event_data.get("email") 35 name = event_data.get("name") 36 37 if event_type == "organization.directory.user_created": 38 await create_user_account(email, name) 39 elif event_type == "organization.directory.user_updated": 40 await update_user_account(email, name) 41 elif event_type == "organization.directory.user_deleted": 42 await deactivate_user_account(email) 43 44 return JSONResponse(status_code=201, content={"status": "processed"}) 45 46 except Exception as processing_error: 47 print(f"Failed to process webhook: {processing_error}") 48 raise HTTPException(status_code=500, detail="Event processing failed") ``` * Java Spring Boot ```java 1 @PostMapping("/webhook") 2 public ResponseEntity webhook( 3 @RequestBody String body, 4 @RequestHeader Map headers) { 5 6 // Security: ALWAYS verify webhook signatures before processing 7 // This prevents malicious webhook calls and protects against replay attacks 8 9 String secret = System.getenv("SCALEKIT_WEBHOOK_SECRET"); 10 11 Map verifyHeaders = new HashMap<>(); 12 verifyHeaders.put("webhook-id", headers.get("webhook-id")); 13 verifyHeaders.put("webhook-timestamp", headers.get("webhook-timestamp")); 14 verifyHeaders.put("webhook-signature", headers.get("webhook-signature")); 15 16 try { 17 // Java throws WebHookException. It does not return false. 18 scalekitClient.webhook() 19 .verifyWebhookPayload(secret, verifyHeaders, body.getBytes(java.nio.charset.StandardCharsets.UTF_8)); 20 } catch (Exception verificationError) { 21 System.err.println("Webhook verification failed: " + verificationError.getMessage()); 22 return ResponseEntity.badRequest().body("Webhook verification failed"); 23 } 24 25 try { 26 // Use case: Real-time user lifecycle management 27 // Examples: Employee onboarding, access termination, role modifications 28 ObjectMapper mapper = new ObjectMapper(); 29 JsonNode rootNode = mapper.readTree(body); 30 31 String eventType = rootNode.get("type").asText(); 32 JsonNode data = rootNode.get("data"); 33 34 switch (eventType) { 35 case "organization.directory.user_created": 36 String email = data.get("email").asText(); 37 String name = data.get("name").asText(); 38 createUserAccount(email, name); 39 break; 40 case "organization.directory.user_updated": 41 updateUserAccount(data); 42 break; 43 case "organization.directory.user_deleted": 44 deactivateUserAccount(data.get("email").asText()); 45 break; 46 default: 47 System.out.println("Unhandled event type: " + eventType); 48 } 49 50 return ResponseEntity.status(HttpStatus.CREATED).body("Webhook processed"); 51 52 } catch (Exception processingError) { 53 System.err.println("Failed to process webhook event: " + processingError.getMessage()); 54 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) 55 .body("Event processing failed"); 56 } 57 } ``` * Go Go ```go 1 // Security: Store webhook secret securely in environment variables 2 // Get this from Dashboard > Webhooks after registering your endpoint 3 webhookSecret := os.Getenv("SCALEKIT_WEBHOOK_SECRET") 4 5 http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) { 6 // Security: ALWAYS verify webhook signatures before processing events 7 // This prevents unauthorized webhook calls and replay attacks 8 9 if r.Method != http.MethodPost { 10 http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) 11 return 12 } 13 14 body, err := io.ReadAll(r.Body) 15 if err != nil { 16 http.Error(w, err.Error(), http.StatusBadRequest) 17 return 18 } 19 defer r.Body.Close() 20 21 // Extract webhook headers for verification 22 headers := map[string]string{ 23 "webhook-id": r.Header.Get("webhook-id"), 24 "webhook-signature": r.Header.Get("webhook-signature"), 25 "webhook-timestamp": r.Header.Get("webhook-timestamp"), 26 } 27 28 // Verify webhook signature to prevent malicious requests 29 valid, err := scalekitClient.VerifyWebhookPayload(webhookSecret, headers, body) 30 if err != nil || !valid { 31 http.Error(w, "Invalid webhook signature", http.StatusUnauthorized) 32 return 33 } 34 35 // Use case: Instant user provisioning and lifecycle management 36 // Examples: Real-time onboarding, emergency access revocation, role synchronization 37 var webhookEvent WebhookEvent 38 if err := json.Unmarshal(body, &webhookEvent); err != nil { 39 http.Error(w, "Invalid webhook payload", http.StatusBadRequest) 40 return 41 } 42 43 switch webhookEvent.Type { 44 case "organization.directory.user_created": 45 err = createUserAccount(webhookEvent.Data.Email, webhookEvent.Data.Name) 46 case "organization.directory.user_updated": 47 err = updateUserAccount(webhookEvent.Data) 48 case "organization.directory.user_deleted": 49 err = deactivateUserAccount(webhookEvent.Data.Email) 50 default: 51 fmt.Printf("Unhandled event type: %s\n", webhookEvent.Type) 52 } 53 54 if err != nil { 55 http.Error(w, "Failed to process webhook", http.StatusInternalServerError) 56 return 57 } 58 59 w.WriteHeader(http.StatusCreated) 60 w.Write([]byte(`{"status": "processed"}`)) 61 }) ``` Webhook endpoint example A typical webhook endpoint URL would be: `https://your-app.com/api/webhooks/scalekit`. Ensure this URL is publicly accessible and uses HTTPS for security. 2. ### Register your webhook endpoint [Section titled “Register your webhook endpoint”](#register-your-webhook-endpoint) After implementing your secure webhook endpoint, register it in the Scalekit dashboard to start receiving events: 1. Navigate to **Dashboard > Webhooks** 2. Click **+Add Endpoint** 3. Enter your webhook endpoint URL (e.g., `https://your-app.com/api/webhooks/scalekit`) 4. Add a meaningful description for your reference 5. Select the event types you want to receive. Common choices include: * `organization.directory.user_created` - New user provisioning * `organization.directory.user_updated` - User profile changes * `organization.directory.user_deleted` - User deactivated or deleted * `organization.directory.group_created` - New group creation * `organization.directory.group_updated` - Group modifications Once registered, your webhook endpoint will start receiving event payloads from directory providers in real-time. Testing webhooks Use request bin services like Beeceptor or webhook.site for initial testing. Refer to our [webhook setup guide](/directory/reference/directory-events/) for detailed testing instructions. 3. ### Process webhook events [Section titled “Process webhook events”](#process-webhook-events) Scalekit standardizes event payloads across different directory providers, ensuring consistent data structure regardless of whether your customers use Azure AD, Okta, Google Workspace, or other providers. When directory changes occur, Scalekit sends events with the following structure. This example is shortened. See [Directory events](/directory/reference/directory-events/) for every field and event. Webhook event payload ```json 1 { 2 "spec_version": "1", 3 "id": "evt_53891546994442316", 4 "type": "organization.directory.user_created", 5 "occurred_at": "2025-01-06T18:44:25.153954Z", 6 "environment_id": "env_53814739859406915", 7 "organization_id": "org_53879494091473415", 8 "object": "DirectoryUser", 9 "data": { 10 "active": true, 11 "dp_id": "00u1a2b3c4d5e6f7g8h9", 12 "email": "john.doe@acmecorp.com", 13 "groups": [ 14 { 15 "id": "dirgroup_53891546960887900", 16 "name": "Engineering" 17 } 18 ], 19 "id": "diruser_53891546960887884", 20 "name": "John Doe", 21 "organization_id": "org_53879494091473415", 22 "preferred_username": "john.doe@acmecorp.com" 23 } 24 } ``` Webhook delivery and retry policy Scalekit retries a delivery with exponential backoff until your server returns a `2xx` status code within 30 seconds. After the eighth attempt fails, the delivery is marked as failed. That happens about 27 hours after the first attempt. If an endpoint fails every delivery for 5 days, Scalekit disables it. Delivery order isn’t guaranteed, so use `occurred_at` when order matters. | Attempt | Delay after the previous attempt | | ------- | -------------------------------- | | 1 | Immediately | | 2 | 5 seconds | | 3 | 5 minutes | | 4 | 30 minutes | | 5 | 2 hours | | 6 | 5 hours | | 7 | 10 hours | | 8 | 10 hours | You have now successfully implemented and registered a webhook endpoint, enabling your application to receive real-time events for automated user provisioning. Your system can now respond instantly to directory changes, providing seamless user lifecycle management. Refer to our [webhook implementation guide](/authenticate/implement-workflows/implement-webhooks/) for the complete list of available event types and payload structures. --- # DOCUMENT BOUNDARY --- # Choose a framework > Give your agent Gmail, Slack and other tools as each user: complete AgentKit examples for LangChain, LangGraph, OpenAI Agents SDK, Mastra and more. Each framework example builds a working agent that reads a user’s Gmail inbox with Scalekit-authenticated tools. Claude Managed Agents also creates Google Calendar events, and Hermes works with GitHub, Gmail, Slack and other apps. ## No agent loop to build [Section titled “No agent loop to build”](#no-agent-loop-to-build) These platforms manage the agent harness for you. Pass a Scalekit skill or MCP URL, describe the task, and the platform handles tool discovery, execution, and session state. [Claude Managed Agents](/agentkit/examples/claude-managed-agents/)Anthropic runs the agent loop. Pass a Scalekit MCP URL, describe a task, and Claude handles tool discovery, execution, and retries. [Hermes](/agentkit/hermes/)Nous Research agent runtime. Install the Scalekit plugin and call a connection by name. [Hermes example repo](https://github.com/scalekit-developers/hermes-agentkit-example)Companion repo for the Hermes how-to. Install hermes-delegated-auth and run one GitHub read as an opaque identifier. ## Build your own agent loop [Section titled “Build your own agent loop”](#build-your-own-agent-loop) These integrations give you full control. Fetch Scalekit tool schemas, wire them into your framework, and run the tool-use loop yourself. * **[LangChain](/agentkit/examples/langchain/)** (Python, SDK, native adapter): Scalekit returns native LangChain tool objects. No schema reshaping needed. * **[LangGraph](/agentkit/examples/langgraph/)** (Python and Node.js, SDK or MCP): Wire Scalekit tools into a graph with a `ToolNode`. * **[Google ADK](/agentkit/examples/google-adk/)** (Python, SDK, native adapter): Scalekit returns native ADK tool objects. No schema reshaping needed. * **[Anthropic](/agentkit/examples/anthropic/)** (Python and Node.js, SDK, direct): Tool schemas use `input_schema`, which matches Anthropic’s format exactly. * **[OpenAI](/agentkit/examples/openai/)** (Python and Node.js, SDK, direct): Rename `input_schema` to `parameters` to match OpenAI’s function format. * **[OpenAI Agents SDK](/agentkit/examples/openai-agents-sdk/)** (Python and Node.js, SDK or MCP): Turn Scalekit tools into function tools, or connect a Virtual MCP server natively. * **[Vercel AI SDK](/agentkit/examples/vercel-ai/)** (Node.js, SDK, `tool()` helper): Wrap tools with `tool()` and `jsonSchema()`. No manual schema conversion needed. * **[CrewAI](/agentkit/examples/crewai/)** (Python, MCP): `MCPServerAdapter` connects to a Scalekit MCP URL. Tool discovery is automatic. * **[Mastra](/agentkit/examples/mastra/)** (Node.js, MCP): Native MCP support via `@mastra/mcp`. Tool discovery is automatic. ## Working examples on GitHub [Section titled “Working examples on GitHub”](#working-examples-on-github) ### [Connect LangChain agents to Gmail](https://github.com/scalekit-inc/sample-langchain-agent) [Securely connect a LangChain agent to Gmail using Scalekit for authentication. Python example for tool authorization.](https://github.com/scalekit-inc/sample-langchain-agent) ### [Connect Google GenAI agents to Gmail](https://github.com/scalekit-inc/google-adk-agent-example) [Build a Google ADK agent that securely accesses Gmail tools. Python example demonstrating Scalekit auth integration.](https://github.com/scalekit-inc/google-adk-agent-example) ### [Connect agents to Slack tools](https://github.com/scalekit-inc/python-connect-demos/tree/main/direct) [Authorize Python agents to use Slack tools with Scalekit. Direct integration example for secure tool access.](https://github.com/scalekit-inc/python-connect-demos/tree/main/direct) ### [Connect CrewAI agents to Gmail](https://github.com/scalekit-developers/crewai-scalekit-example) [Multi-agent email triage crew using CrewAI with Scalekit-authenticated Gmail tools via MCP.](https://github.com/scalekit-developers/crewai-scalekit-example) ### [Meeting prep agent](https://github.com/scalekit-inc/meeting-prep-agent-example) [Pulls context from Google Cal, Gmail, HubSpot, and Slack before each external meeting. Delivers a structured brief in under 60 seconds using delegated user identity.](https://github.com/scalekit-inc/meeting-prep-agent-example) ### [Browse all agent auth examples](https://github.com/scalekit-developers/agent-auth-examples) [A curated collection of working examples showing how to build agents that authenticate and access tools using Scalekit.](https://github.com/scalekit-developers/agent-auth-examples) --- # DOCUMENT BOUNDARY --- # Anthropic > Build an Anthropic agent with Scalekit-authenticated tools. Scalekit returns tool schemas in Anthropic's native format; no conversion needed. Build an agent using Anthropic’s Claude that reads a user’s Gmail inbox. Scalekit returns tool schemas with `input_schema`, the exact format Anthropic’s tool use API expects. ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your Anthropic API key as `ANTHROPIC_API_KEY` to it. * The samples load `.env` with `load_dotenv()` from `python-dotenv` in Python, and `import 'dotenv/config'` in Node.js. ## Install [Section titled “Install”](#install) * Python ```sh pip install scalekit-sdk-python python-dotenv anthropic ``` * Node.js ```sh npm install @scalekit-sdk/node @anthropic-ai/sdk dotenv ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize [Section titled “Initialize”](#initialize) * Python ```python import os import scalekit.client from dotenv import load_dotenv import anthropic from google.protobuf.json_format import MessageToDict load_dotenv() scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions client = anthropic.Anthropic() ``` * Node.js ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; import Anthropic from '@anthropic-ai/sdk'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const anthropic = new Anthropic(); ``` ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) * Python ```python 1 # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 2 connection_name = "gmail" 3 identifier = "user_123" # your app's unique user ID 4 5 response = actions.get_or_create_connected_account( 6 connection_name=connection_name, identifier=identifier 7 ) 8 if response.connected_account.status != "ACTIVE": 9 link = actions.get_authorization_link( 10 connection_name=connection_name, identifier=identifier 11 ) 12 print("Authorize Gmail:", link.link) 13 input("Press Enter after authorizing...") 14 # Fetch the account again to pick up the new status 15 response = actions.get_or_create_connected_account( 16 connection_name=connection_name, identifier=identifier 17 ) 18 19 if response.connected_account.status != "ACTIVE": 20 raise RuntimeError( 21 f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." 22 ) ``` * Node.js ```typescript 1 import { createInterface } from 'node:readline/promises'; 2 import { ConnectorStatus } from '@scalekit-sdk/node'; 3 4 // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 5 const connectionName = 'gmail'; 6 const identifier = 'user_123'; // your app's unique user ID 7 8 let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 9 connectionName, 10 identifier, 11 }); 12 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 13 const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); 14 console.log('Authorize Gmail:', link); 15 const rl = createInterface({ input: process.stdin, output: process.stdout }); 16 await rl.question('Press Enter after authorizing...'); 17 rl.close(); 18 // Fetch the account again to pick up the new status 19 ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 20 connectionName, 21 identifier, 22 })); 23 } 24 25 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 26 throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); 27 } ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Run the agent [Section titled “Run the agent”](#run-the-agent) Fetch tools scoped to this user, then run the full Claude tool-use loop. [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. * Python ```python 1 # Fetch tools scoped to this user 2 scoped_response, _ = actions.tools.list_scoped_tools( 3 identifier="user_123", 4 filter={"connection_names": ["gmail"]}, 5 page_size=100, # up to 100 tools per page; page with next_page_token for more 6 ) 7 llm_tools = [ 8 { 9 "name": MessageToDict(t.tool).get("definition", {}).get("name"), 10 "description": MessageToDict(t.tool).get("definition", {}).get("description", ""), 11 "input_schema": MessageToDict(t.tool).get("definition", {}).get("input_schema", {}), 12 } 13 for t in scoped_response.tools 14 ] 15 16 # Run the agent loop 17 messages = [{"role": "user", "content": "Fetch my last 5 unread emails and summarize them"}] 18 19 def ask(): 20 return client.messages.create( 21 model="claude-sonnet-4-6", 22 max_tokens=4096, 23 tools=llm_tools, 24 messages=messages, 25 ) 26 27 response = ask() 28 while response.stop_reason == "tool_use": 29 tool_results = [] 30 for block in response.content: 31 if block.type == "tool_use": 32 result = actions.execute_tool( 33 tool_name=block.name, 34 identifier="user_123", 35 connection_name="gmail", 36 tool_input=block.input, 37 ) 38 tool_results.append({ 39 "type": "tool_result", 40 "tool_use_id": block.id, 41 "content": str(result.data), 42 }) 43 44 messages.append({"role": "assistant", "content": response.content}) 45 messages.append({"role": "user", "content": tool_results}) 46 response = ask() 47 48 # end_turn is the normal finish. Others, such as max_tokens or refusal, end early. 49 if response.stop_reason != "end_turn": 50 print(f"Stopped early: {response.stop_reason}") 51 print("".join(block.text for block in response.content if block.type == "text")) ``` * Node.js ```typescript 1 // Fetch tools scoped to this user 2 const { tools } = await scalekit.tools.listScopedTools('user_123', { 3 filter: { connectionNames: ['gmail'] }, 4 pageSize: 100, // up to 100 tools per page; page with nextPageToken for more 5 }); 6 // Tool definitions are typed as generic JSON, so read each field explicitly 7 const llmTools: Anthropic.Tool[] = tools.flatMap((t) => { 8 const definition = t.tool?.definition; 9 if (!definition) return []; 10 return [{ 11 name: String(definition.name), 12 description: String(definition.description ?? ''), 13 input_schema: definition.input_schema as Anthropic.Tool.InputSchema, 14 }]; 15 }); 16 17 // Run the agent loop 18 const messages: Anthropic.MessageParam[] = [ 19 { role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }, 20 ]; 21 22 const ask = () => 23 anthropic.messages.create({ 24 model: 'claude-sonnet-4-6', 25 max_tokens: 4096, 26 tools: llmTools, 27 messages, 28 }); 29 30 let response = await ask(); 31 while (response.stop_reason === 'tool_use') { 32 const toolResults: Anthropic.ToolResultBlockParam[] = []; 33 for (const block of response.content) { 34 if (block.type === 'tool_use') { 35 const result = await scalekit.actions.executeTool({ 36 toolName: block.name, 37 identifier: 'user_123', 38 connector: 'gmail', 39 toolInput: block.input as Record, 40 }); 41 toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result.data) }); 42 } 43 } 44 messages.push({ role: 'assistant', content: response.content }); 45 messages.push({ role: 'user', content: toolResults }); 46 response = await ask(); 47 } 48 49 // end_turn is the normal finish. Others, such as max_tokens or refusal, end early. 50 if (response.stop_reason !== 'end_turn') console.log(`Stopped early: ${response.stop_reason}`); 51 for (const block of response.content) { 52 if (block.type === 'text') console.log(block.text); 53 } ``` ## Use MCP instead [Section titled “Use MCP instead”](#use-mcp-instead) The Messages API can connect to a Scalekit Virtual MCP server through Anthropic’s [MCP connector](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector) (beta). Anthropic’s API calls the MCP server for you, so there’s no tool loop to write. Pass the server URL and a session token for this user. * Python ```python 1 from datetime import timedelta 2 3 # Returned by create_config when you created the Virtual MCP server 4 config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] 5 mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] 6 7 # Mint a fresh session token for this user before each agent run 8 mcp_token = actions.mcp.create_session_token( 9 mcp_config_id=config_id, 10 identifier="user_123", 11 expiry=timedelta(hours=1), 12 ).token 13 14 response = client.beta.messages.create( 15 model="claude-sonnet-4-6", 16 max_tokens=4096, 17 messages=[{"role": "user", "content": "Fetch my last 5 unread emails and summarize them"}], 18 mcp_servers=[ 19 {"type": "url", "url": mcp_url, "name": "scalekit", "authorization_token": mcp_token} 20 ], 21 tools=[{"type": "mcp_toolset", "mcp_server_name": "scalekit"}], 22 betas=["mcp-client-2025-11-20"], 23 ) 24 print("".join(block.text for block in response.content if block.type == "text")) ``` * Node.js ```typescript 1 // Mint the token on your backend with scalekit.actions.mcp.createSessionToken 2 // (see Mint session tokens) and pass it in here. 3 const mcpUrl = process.env.SCALEKIT_MCP_SERVER_URL!; // mcp_server_url from your Virtual MCP server 4 const mcpToken = process.env.SCALEKIT_MCP_SESSION_TOKEN!; // minted on your backend for this user 5 6 const response = await anthropic.beta.messages.create({ 7 model: 'claude-sonnet-4-6', 8 max_tokens: 4096, 9 messages: [{ role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }], 10 mcp_servers: [{ type: 'url', url: mcpUrl, name: 'scalekit', authorization_token: mcpToken }], 11 tools: [{ type: 'mcp_toolset', mcp_server_name: 'scalekit' }], 12 betas: ['mcp-client-2025-11-20'], 13 }); 14 for (const block of response.content) { 15 if (block.type === 'text') console.log(block.text); 16 } ``` Other MCP hosts, such as Claude Code, can connect to the same URL. Send the session token as an `Authorization: Bearer` header, and mint a new token when it expires (at most 24 hours). See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](/agentkit/mcp/session-tokens/) to create the server, check the user’s connections and mint a token. ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # Claude Managed Agents > Give Claude Managed Agents per-user tools with AgentKit: create a Virtual MCP server, keep each user's session token in their own Anthropic vault, and run it. Run a background agent that reads Gmail and creates Google Calendar events — without managing any agent loop. Anthropic handles tool discovery, execution, retries, and session state. You provide the task. Complete working demo on GitHub Browse the full source for this guide at [GitHub](https://github.com/scalekit-inc/python-connect-demos/tree/main/claude-managed-agents). Scalekit connects the agent to user-authorized tools via a [Virtual MCP server](/agentkit/mcp/overview/). Before each run, you mint a short-lived session token and store it in that user’s own Anthropic vault. The agent accesses the MCP server using the vault credential. ## Before you start [Section titled “Before you start”](#before-you-start) * Create Gmail and Google Calendar connections in **AgentKit** > **Connections**. The samples use the connection names `gmail` and `googlecalendar`, so change them if yours differ. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add to it your [Anthropic API key](https://platform.claude.com/settings/keys) with access to the Managed Agents beta as `ANTHROPIC_API_KEY`, and your Anthropic environment ID as `ANTHROPIC_ENVIRONMENT_ID`. * The samples load `.env` with `load_dotenv()` from `python-dotenv`. ## How it works [Section titled “How it works”](#how-it-works) The flow has three phases: 1. **Build** (one-time) — Create a Virtual MCP server and a Claude Managed Agent. Save `mcp_id` and `agent_id`. 2. **Authorize user for external connections** (once per user) — Authorize the user’s Gmail and Google Calendar accounts. 3. **Run a session** (per agent run) — Check connections, mint a session token, store it in that user’s own Anthropic vault, and start a session. Never share one vault across users: a session acts as whichever user’s token is in its vault. ## Install [Section titled “Install”](#install) ```sh pip install anthropic scalekit-sdk-python python-dotenv ``` ## Build [Section titled “Build”](#build) Run this once to create your Virtual MCP server and Claude Managed Agent. Save the returned `mcp_id` and `agent_id` — you reuse them for every user and every session. builder.py ```python 1 import os 2 import anthropic 3 from scalekit import ScalekitClient 4 from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping 5 from dotenv import load_dotenv 6 7 load_dotenv() 8 9 anthropic_client = anthropic.Anthropic() 10 scalekit_client = ScalekitClient( 11 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 12 client_id=os.environ["SCALEKIT_CLIENT_ID"], 13 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 14 ) 15 16 GMAIL_TOOLS = ["gmail_fetch_mails"] 17 GCAL_TOOLS = [ 18 "googlecalendar_list_calendars", 19 "googlecalendar_list_events", 20 "googlecalendar_get_event_by_id", 21 "googlecalendar_create_event", 22 "googlecalendar_update_event", 23 ] 24 25 vmcp_response = scalekit_client.actions.mcp.create_config( 26 name="email-calendar-demo", 27 connection_tool_mappings=[ 28 McpConfigConnectionToolMapping( 29 connection_name="gmail", 30 tools=GMAIL_TOOLS, 31 ), 32 McpConfigConnectionToolMapping( 33 connection_name="googlecalendar", 34 tools=GCAL_TOOLS, 35 ), 36 ], 37 ) 38 39 mcp_id = vmcp_response.config.id 40 mcp_server_url = vmcp_response.config.mcp_server_url 41 42 agent = anthropic_client.beta.agents.create( 43 name="Email Meeting Manager", 44 model="claude-haiku-4-5", 45 system=( 46 "You are an email and calendar assistant. When invoked, you will:\n" 47 "1. Fetch the single most recent unread email from Gmail.\n" 48 "2. Summarize it in 2-3 sentences.\n" 49 "3. Create a Google Calendar event titled 'Action Required: ' " 50 "with your summary as the description." 51 ), 52 mcp_servers=[ 53 { 54 "type": "url", 55 "name": "email-calendar-mcp", 56 "url": mcp_server_url, 57 } 58 ], 59 tools=[ 60 {"type": "agent_toolset_20260401", "default_config": {"enabled": True}}, 61 { 62 "type": "mcp_toolset", 63 "mcp_server_name": "email-calendar-mcp", 64 "default_config": { 65 "enabled": True, 66 "permission_policy": {"type": "always_allow"}, 67 }, 68 }, 69 ], 70 ) 71 72 print("Add these to your .env file:") 73 print(f"SCALEKIT_MCP_CONFIG_ID={mcp_id}") 74 print(f"ANTHROPIC_AGENT_ID={agent.id}") ``` Run `python builder.py` once and add the two printed lines to your `.env` file. The next two scripts read them from there. The agent definition references the `mcp_server_url` but carries no auth credentials. Authentication is injected at runtime via the Anthropic vault. ## Authorize user for external connections [Section titled “Authorize user for external connections”](#authorize-user-for-external-connections) Each user authorizes their Gmail and Google Calendar accounts once. All future agent sessions for that user reuse those connections. executor_setup.py ```python 1 import os 2 from scalekit import ScalekitClient 3 from dotenv import load_dotenv 4 5 load_dotenv() 6 7 scalekit_client = ScalekitClient( 8 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 9 client_id=os.environ["SCALEKIT_CLIENT_ID"], 10 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 11 ) 12 13 mcp_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] # printed by builder.py 14 identifier = "user_123" # your app's unique user ID 15 16 accounts_response = scalekit_client.actions.mcp.list_mcp_connected_accounts( 17 config_id=mcp_id, 18 identifier=identifier, 19 ) 20 21 for account in accounts_response.connected_accounts: 22 status = (account.connected_account_status or "").upper() 23 if status != "ACTIVE": 24 auth_response = scalekit_client.actions.get_authorization_link( 25 identifier=identifier, 26 connection_name=account.connection_name, 27 ) 28 print(f"{account.connection_name} needs auth: {auth_response.link}") 29 else: 30 print(f"✓ {account.connection_name} is ACTIVE") ``` Surface the auth link in your app UI or send it via email. Users only need to do this once. ## Run a session [Section titled “Run a session”](#run-a-session) Run `executor.py` for each agent run. It stops before starting a session if any connection isn’t `ACTIVE`, so the agent never runs with a tool it can’t call. 1. ## Set up the clients and the run [Section titled “Set up the clients and the run”](#set-up-the-clients-and-the-run) executor.py ```python 1 import json 2 import os 3 import sys 4 from datetime import timedelta 5 from pathlib import Path 6 7 import anthropic 8 from dotenv import load_dotenv 9 from scalekit import ScalekitClient 10 11 load_dotenv() 12 13 anthropic_client = anthropic.Anthropic() 14 scalekit_client = ScalekitClient( 15 env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], 16 client_id=os.environ["SCALEKIT_CLIENT_ID"], 17 client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], 18 ) 19 20 mcp_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] # printed by builder.py 21 agent_id = os.environ["ANTHROPIC_AGENT_ID"] # printed by builder.py 22 identifier = "user_123" # your app's unique user ID 23 prompt = "Process my most recent unread email." 24 25 # Each user gets their own vault, created on their first run. 26 # This example keeps the IDs in a local file. In production, store them 27 # in your database, keyed by the user. 28 VAULTS_FILE = Path("vaults.json") 29 vaults = json.loads(VAULTS_FILE.read_text()) if VAULTS_FILE.exists() else {} 30 user_vault = vaults.get(identifier) ``` 2. ## Check that connections are active [Section titled “Check that connections are active”](#check-that-connections-are-active) OAuth tokens for connected accounts can expire or be revoked, so check every connection before minting a token. If any isn’t `ACTIVE`, stop and send the user back through `executor_setup.py`. executor.py ```python accounts_response = scalekit_client.actions.mcp.list_mcp_connected_accounts( config_id=mcp_id, identifier=identifier, ) inactive = [ a.connection_name for a in accounts_response.connected_accounts if (a.connected_account_status or "").upper() != "ACTIVE" ] if inactive: sys.exit(f"Not ACTIVE: {', '.join(inactive)}. Run executor_setup.py to reauthorize.") ``` 3. ## Mint a session token and store it in the vault [Section titled “Mint a session token and store it in the vault”](#mint-a-session-token-and-store-it-in-the-vault) Mint a short-lived session token and store it in this user’s Anthropic vault. Claude Managed Agents access the MCP server using the vault credential, not a direct bearer header, and the session acts as whichever user’s token is in the vault. So give each user their own vault, and never put two users’ tokens in the same one. executor.py ```python 1 configs_response = scalekit_client.actions.mcp.list_configs(filter_id=mcp_id) 2 mcp_server_url = configs_response.configs[0].mcp_server_url 3 4 token_response = scalekit_client.actions.mcp.create_session_token( 5 mcp_config_id=mcp_id, 6 identifier=identifier, 7 expiry=timedelta(hours=1), 8 ) 9 token = token_response.token 10 11 # Update this user's own credential, or create their vault on their first run. 12 # Never reuse one vault across users: a session acts as whoever's token is in it. 13 if user_vault: 14 vault_id = user_vault["vault_id"] 15 anthropic_client.beta.vaults.credentials.update( 16 user_vault["credential_id"], 17 vault_id=vault_id, 18 auth={"type": "static_bearer", "token": token}, 19 ) 20 else: 21 vault = anthropic_client.beta.vaults.create(display_name=f"email-calendar-{identifier}") 22 vault_id = vault.id 23 credential = anthropic_client.beta.vaults.credentials.create( 24 vault_id, 25 display_name="email-calendar-credential", 26 auth={ 27 "type": "static_bearer", 28 "mcp_server_url": mcp_server_url, 29 "token": token, 30 }, 31 ) 32 vaults[identifier] = {"vault_id": vault_id, "credential_id": credential.id} 33 VAULTS_FILE.write_text(json.dumps(vaults, indent=2)) ``` 4. ## Start the session [Section titled “Start the session”](#start-the-session) Pass `vault_ids` to the session so the agent can authenticate against the MCP server. executor.py ```python 1 session = anthropic_client.beta.sessions.create( 2 agent=agent_id, 3 environment_id=os.environ["ANTHROPIC_ENVIRONMENT_ID"], 4 vault_ids=[vault_id], 5 ) 6 7 with anthropic_client.beta.sessions.events.stream(session_id=session.id) as stream: 8 anthropic_client.beta.sessions.events.send( 9 session_id=session.id, 10 events=[{"type": "user.message", "content": [{"type": "text", "text": prompt}]}], 11 ) 12 for event in stream: 13 if event.type == "agent.message": 14 for block in event.content: 15 if block.type == "text": 16 print(block.text, end="", flush=True) 17 elif event.type == "agent.mcp_tool_use": 18 print(f"\n→ {event.name}", flush=True) 19 elif event.type == "session.status_terminated": 20 break 21 elif event.type == "session.status_idle": 22 # The session also goes idle while it waits on you (requires_action). 23 # Stop only when it's idle for another reason, such as end_turn. 24 if event.stop_reason.type != "requires_action": 25 break ``` ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # CrewAI > Build a CrewAI agent with Scalekit-authenticated Gmail tools via MCP. CrewAI's MCPServerAdapter connects to a Scalekit MCP URL for automatic tool discovery. Build a CrewAI agent that reads a user’s Gmail inbox. Scalekit handles OAuth, token storage, and exposes tools over MCP. CrewAI’s `MCPServerAdapter` discovers the tools and their schemas from the server. Some schemas need a small patch, covered below. [Full code on GitHub](https://github.com/scalekit-developers/crewai-scalekit-example) ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. * Create a Virtual MCP server named `gmail-user-tools` that includes the Gmail tools. See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/). * The samples load `.env` with `load_dotenv()` from `python-dotenv`. ## Install [Section titled “Install”](#install) ```sh pip install crewai "crewai-tools[mcp]" scalekit-sdk-python python-dotenv ``` ## Initialize [Section titled “Initialize”](#initialize) ```python import os from scalekit import ScalekitClient from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv()) scalekit_client = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions ``` ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) ```python 1 # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 2 connection_name = "gmail" 3 identifier = "user_123" # your app's unique user ID 4 5 response = actions.get_or_create_connected_account( 6 connection_name=connection_name, identifier=identifier 7 ) 8 if response.connected_account.status != "ACTIVE": 9 link = actions.get_authorization_link( 10 connection_name=connection_name, identifier=identifier 11 ) 12 print("Authorize Gmail:", link.link) 13 input("Press Enter after authorizing...") 14 # Fetch the account again to pick up the new status 15 response = actions.get_or_create_connected_account( 16 connection_name=connection_name, identifier=identifier 17 ) 18 19 if response.connected_account.status != "ACTIVE": 20 raise RuntimeError( 21 f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." 22 ) ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Build and run the agent [Section titled “Build and run the agent”](#build-and-run-the-agent) Get the Virtual MCP server URL and mint a session token, then pass both to `MCPServerAdapter`. CrewAI discovers all available Gmail tools from the MCP server: ```python 1 from crewai import Agent, Crew, LLM, Task 2 from crewai_tools import MCPServerAdapter 3 from datetime import timedelta 4 5 # Retrieve config_id by listing Virtual MCP servers filtered by name 6 list_response = actions.mcp.list_configs(filter_name="gmail-user-tools") 7 mcp_server_url = list_response.configs[0].mcp_server_url 8 mcp_id = list_response.configs[0].id 9 10 token_response = actions.mcp.create_session_token( 11 mcp_config_id=mcp_id, 12 identifier="user_123", 13 expiry=timedelta(hours=1), 14 ) 15 16 with MCPServerAdapter({ 17 "url": mcp_server_url, 18 "headers": {"Authorization": f"Bearer {token_response.token}"}, 19 "transport": "streamable-http", 20 }) as tools: 21 agent = Agent( 22 role="Email Assistant", 23 goal="Fetch and summarize the user's unread emails", 24 backstory="You are a helpful assistant with access to the user's Gmail inbox.", 25 tools=tools, 26 llm=LLM( 27 model=os.getenv("LLM_MODEL", "gpt-4o"), 28 base_url=os.getenv("OPENAI_BASE_URL"), 29 api_key=os.getenv("OPENAI_API_KEY"), 30 ), 31 verbose=True, 32 ) 33 34 task = Task( 35 description="Fetch the last 5 unread emails and provide a brief summary of each.", 36 expected_output="A list of 5 unread emails with subject, sender, and a one-sentence summary.", 37 agent=agent, 38 ) 39 40 result = Crew(agents=[agent], tasks=[task]).kickoff() 41 print(result) ``` Nullable schema fields Some Scalekit tool schemas include nullable types (`{"type": ["string", "null"]}`) that CrewAI’s schema converter doesn’t handle out of the box. If you see a `TypeError` during tool parsing, apply the [schema patch](https://github.com/scalekit-developers/crewai-scalekit-example/blob/main/agent.py) at the top of your script. ## Multi-agent crew [Section titled “Multi-agent crew”](#multi-agent-crew) CrewAI’s real strength is multi-agent orchestration. For a full example that splits email triage across three specialized agents (scanner, prioritizer, drafter), see the [CrewAI email triage cookbook](/cookbooks/crewai-agentkit-email-triage/). ## Get the MCP server URL [Section titled “Get the MCP server URL”](#get-the-mcp-server-url) The code above reads `mcp_server_url` from a Virtual MCP server. Create one in the Scalekit dashboard under **AgentKit** > **Virtual MCP Servers**, or with the API. See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/). ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # Google ADK > Build a Google ADK agent with Scalekit-authenticated Gmail tools. Scalekit returns native ADK tool objects; no schema reshaping needed. Build a Google ADK agent that reads a user’s Gmail inbox. Scalekit handles OAuth, token storage, and returns tools as native ADK tool objects compatible with any ADK agent. [Full code on GitHub](https://github.com/scalekit-inc/google-adk-agent-example) ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your Gemini API key as `GOOGLE_API_KEY` to it. * The samples load `.env` with `load_dotenv()` from `python-dotenv`. ## Install [Section titled “Install”](#install) ```sh pip install scalekit-sdk-python python-dotenv "google-adk>=2,<3" ``` ## Initialize [Section titled “Initialize”](#initialize) ```python import os import asyncio import scalekit.client from dotenv import load_dotenv load_dotenv() scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) ```python 1 # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 2 connection_name = "gmail" 3 identifier = "user_123" # your app's unique user ID 4 5 response = actions.get_or_create_connected_account( 6 connection_name=connection_name, identifier=identifier 7 ) 8 if response.connected_account.status != "ACTIVE": 9 link = actions.get_authorization_link( 10 connection_name=connection_name, identifier=identifier 11 ) 12 print("Authorize Gmail:", link.link) 13 input("Press Enter after authorizing...") 14 # Fetch the account again to pick up the new status 15 response = actions.get_or_create_connected_account( 16 connection_name=connection_name, identifier=identifier 17 ) 18 19 if response.connected_account.status != "ACTIVE": 20 raise RuntimeError( 21 f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." 22 ) ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Build and run the agent [Section titled “Build and run the agent”](#build-and-run-the-agent) `actions.google.get_tools()` returns native ADK tool objects. Pass them directly to a Google ADK `Agent`: ```python 1 from google.adk.agents import Agent 2 from google.adk.runners import Runner 3 from google.adk.sessions import InMemorySessionService 4 from google.genai import types 5 6 tools = actions.google.get_tools( 7 identifier="user_123", 8 connection_names=["gmail"], 9 page_size=100, # avoid missing tools when a connector has more than the default page 10 ) 11 12 agent = Agent( 13 name="gmail_assistant", 14 model="gemini-2.0-flash", 15 instruction="You are a helpful Gmail assistant.", 16 tools=tools, 17 ) 18 19 async def main(): 20 session_service = InMemorySessionService() 21 runner = Runner(agent=agent, app_name="gmail_app", session_service=session_service) 22 session = await session_service.create_session(app_name="gmail_app", user_id="user_123") 23 24 message = types.Content( 25 role="user", 26 parts=[types.Part(text="Fetch my last 5 unread emails and summarize them")], 27 ) 28 async for event in runner.run_async( 29 user_id="user_123", 30 session_id=session.id, 31 new_message=message, 32 ): 33 if event.is_final_response() and event.content and event.content.parts: 34 print(event.content.parts[0].text) 35 36 asyncio.run(main()) ``` Multiple Gmail accounts If a user has multiple Gmail connections, pass the specific `connection_names` value from your Scalekit dashboard to scope tools to the right one. ## Use MCP instead [Section titled “Use MCP instead”](#use-mcp-instead) Google ADK connects to MCP servers with `McpToolset`. Pass the Virtual MCP server URL and a session token for this user: ```python 1 import os 2 from datetime import timedelta 3 from google.adk.agents import Agent 4 from google.adk.tools.mcp_tool import McpToolset, StreamableHTTPConnectionParams 5 6 # Returned by create_config when you created the Virtual MCP server 7 config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] 8 mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] 9 10 # Mint a fresh session token for this user before each agent run 11 mcp_token = actions.mcp.create_session_token( 12 mcp_config_id=config_id, 13 identifier="user_123", 14 expiry=timedelta(hours=1), 15 ).token 16 17 agent = Agent( 18 name="gmail_assistant", 19 model="gemini-2.0-flash", 20 instruction="You are a helpful Gmail assistant.", 21 tools=[ 22 McpToolset( 23 connection_params=StreamableHTTPConnectionParams( 24 url=mcp_url, 25 headers={"Authorization": f"Bearer {mcp_token}"}, 26 ) 27 ) 28 ], 29 ) ``` Run this agent with the same `Runner` loop as above. See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](/agentkit/mcp/session-tokens/) to create the server, check the user’s connections and mint a token. ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # LangChain > Build a LangChain agent with Scalekit-authenticated Gmail tools. Scalekit returns native LangChain tool objects; no schema reshaping needed. Build a LangChain agent that reads a user’s Gmail inbox. Scalekit handles OAuth, token storage, and returns tools in native LangChain format. Your agent code needs no Scalekit-specific logic beyond initialization. [Full code on GitHub](https://github.com/scalekit-inc/sample-langchain-agent) ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. * The samples load `.env` with `load_dotenv()` from `python-dotenv`. ## Install [Section titled “Install”](#install) ```sh pip install scalekit-sdk-python python-dotenv langchain-openai ``` ## Initialize [Section titled “Initialize”](#initialize) ```python import os import scalekit.client from dotenv import load_dotenv load_dotenv() scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) ```python 1 # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 2 connection_name = "gmail" 3 identifier = "user_123" # your app's unique user ID 4 5 response = actions.get_or_create_connected_account( 6 connection_name=connection_name, identifier=identifier 7 ) 8 if response.connected_account.status != "ACTIVE": 9 link = actions.get_authorization_link( 10 connection_name=connection_name, identifier=identifier 11 ) 12 print("Authorize Gmail:", link.link) 13 input("Press Enter after authorizing...") 14 # Fetch the account again to pick up the new status 15 response = actions.get_or_create_connected_account( 16 connection_name=connection_name, identifier=identifier 17 ) 18 19 if response.connected_account.status != "ACTIVE": 20 raise RuntimeError( 21 f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." 22 ) ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Build and run the agent [Section titled “Build and run the agent”](#build-and-run-the-agent) `actions.langchain.get_tools()` returns native `StructuredTool` objects. Bind them to your LLM and run the tool-calling loop: ```python 1 from langchain_openai import ChatOpenAI 2 from langchain_core.messages import HumanMessage, ToolMessage 3 4 tools = actions.langchain.get_tools( 5 identifier="user_123", 6 connection_names=["gmail"], 7 page_size=100, # avoid missing tools when a connector has more than the default page 8 ) 9 tool_map = {t.name: t for t in tools} 10 11 llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) 12 messages = [HumanMessage("Fetch my last 5 unread emails and summarize them")] 13 14 while True: 15 response = llm.invoke(messages) 16 messages.append(response) 17 if not response.tool_calls: 18 print(response.content) 19 break 20 for tc in response.tool_calls: 21 result = tool_map[tc["name"]].invoke(tc["args"]) 22 messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"])) ``` Multiple Gmail accounts If a user has multiple Gmail connections, pass the specific `connection_names` value from your Scalekit dashboard to scope tools to the right one. ## Use MCP instead [Section titled “Use MCP instead”](#use-mcp-instead) LangChain connects to MCP servers with `langchain-mcp-adapters`. Pass the Virtual MCP server URL and a session token for this user: ```sh pip install "langchain-mcp-adapters>=0.3,<1" ``` ```python 1 import asyncio 2 import os 3 from datetime import timedelta 4 from langchain_mcp_adapters.client import MultiServerMCPClient 5 from langchain_openai import ChatOpenAI 6 from langchain_core.messages import HumanMessage, ToolMessage 7 8 # Returned by create_config when you created the Virtual MCP server 9 config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] 10 mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] 11 12 # Mint a fresh session token for this user before each agent run 13 mcp_token = actions.mcp.create_session_token( 14 mcp_config_id=config_id, 15 identifier="user_123", 16 expiry=timedelta(hours=1), 17 ).token 18 19 async def run(): 20 client = MultiServerMCPClient( 21 { 22 "scalekit": { 23 "transport": "streamable_http", 24 "url": mcp_url, 25 "headers": {"Authorization": f"Bearer {mcp_token}"}, 26 } 27 } 28 ) 29 tools = await client.get_tools() 30 tool_map = {t.name: t for t in tools} 31 llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) 32 messages = [HumanMessage("Fetch my last 5 unread emails and summarize them")] 33 34 while True: 35 response = await llm.ainvoke(messages) 36 messages.append(response) 37 if not response.tool_calls: 38 print(response.content) 39 break 40 for tc in response.tool_calls: 41 result = await tool_map[tc["name"]].ainvoke(tc["args"]) 42 messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"])) 43 44 asyncio.run(run()) ``` See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](/agentkit/mcp/session-tokens/) to create the server, check the user’s connections and mint a token. ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # LangGraph > Build a LangGraph agent that reads Gmail as the user with AgentKit: wire Scalekit tools into a ToolNode graph, or connect to a Virtual MCP server. Build a LangGraph agent that reads a user’s Gmail inbox. The graph has two nodes: the model, and a `ToolNode` that runs the Scalekit tools the model asks for. In Python, Scalekit returns tools as LangChain tool objects. In Node.js, you wrap each tool with LangChain’s `tool()` helper. Using LangChain’s agent loop without a graph? See [LangChain](/agentkit/examples/langchain/). ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. * The samples load `.env` with `load_dotenv()` from `python-dotenv` in Python, and `import 'dotenv/config'` in Node.js. ## Install [Section titled “Install”](#install) * Python ```sh pip install scalekit-sdk-python python-dotenv langgraph langchain-openai ``` * Node.js ```sh npm install @scalekit-sdk/node dotenv @langchain/langgraph @langchain/core @langchain/openai ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize [Section titled “Initialize”](#initialize) * Python ```python import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` * Node.js ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` Later blocks reuse this setup. ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) * Python ```python 1 # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 2 connection_name = "gmail" 3 identifier = "user_123" # your app's unique user ID 4 5 response = actions.get_or_create_connected_account( 6 connection_name=connection_name, identifier=identifier 7 ) 8 if response.connected_account.status != "ACTIVE": 9 link = actions.get_authorization_link( 10 connection_name=connection_name, identifier=identifier 11 ) 12 print("Authorize Gmail:", link.link) 13 input("Press Enter after authorizing...") 14 # Fetch the account again to pick up the new status 15 response = actions.get_or_create_connected_account( 16 connection_name=connection_name, identifier=identifier 17 ) 18 19 if response.connected_account.status != "ACTIVE": 20 raise RuntimeError( 21 f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." 22 ) ``` * Node.js ```typescript 1 import { createInterface } from 'node:readline/promises'; 2 import { ConnectorStatus } from '@scalekit-sdk/node'; 3 4 // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 5 const connectionName = 'gmail'; 6 const identifier = 'user_123'; // your app's unique user ID 7 8 let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 9 connectionName, 10 identifier, 11 }); 12 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 13 const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); 14 console.log('Authorize Gmail:', link); 15 const rl = createInterface({ input: process.stdin, output: process.stdout }); 16 await rl.question('Press Enter after authorizing...'); 17 rl.close(); 18 // Fetch the account again to pick up the new status 19 ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 20 connectionName, 21 identifier, 22 })); 23 } 24 25 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 26 throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); 27 } ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Build and run the graph [Section titled “Build and run the graph”](#build-and-run-the-graph) `build_graph` (`buildGraph` in Node.js) takes a list of tools and returns a compiled graph. The model node calls the LLM. When the LLM asks for tools, `tools_condition` routes to the `ToolNode`, which runs them and returns their results to the model. When the LLM answers without tool calls, the graph ends. Fetch the tools scoped to this user and run the graph. [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. * Python ```python 1 from langchain_openai import ChatOpenAI 2 from langgraph.graph import START, MessagesState, StateGraph 3 from langgraph.prebuilt import ToolNode, tools_condition 4 5 6 def build_graph(tools): 7 llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) 8 9 def call_model(state: MessagesState): 10 return {"messages": [llm.invoke(state["messages"])]} 11 12 builder = StateGraph(MessagesState) 13 builder.add_node("agent", call_model) 14 builder.add_node("tools", ToolNode(tools)) 15 builder.add_edge(START, "agent") 16 # Run the tools the model asked for, or end when it answers without tool calls 17 builder.add_conditional_edges("agent", tools_condition) 18 builder.add_edge("tools", "agent") 19 return builder.compile() 20 21 22 tools = actions.langchain.get_tools( 23 identifier=identifier, 24 connection_names=[connection_name], 25 page_size=100, # up to 100 tools per page; page with page_token for more 26 ) 27 graph = build_graph(tools) 28 result = graph.invoke( 29 {"messages": [("user", "Fetch my last 5 unread emails and summarize them")]} 30 ) 31 print(result["messages"][-1].content) ``` * Node.js ```typescript 1 import { ChatOpenAI } from '@langchain/openai'; 2 import { tool, type StructuredToolInterface } from '@langchain/core/tools'; 3 import { MessagesAnnotation, START, StateGraph } from '@langchain/langgraph'; 4 import { ToolNode, toolsCondition } from '@langchain/langgraph/prebuilt'; 5 6 function buildGraph(tools: StructuredToolInterface[]) { 7 const llm = new ChatOpenAI({ model: 'gpt-4o' }).bindTools(tools); 8 return new StateGraph(MessagesAnnotation) 9 .addNode('agent', async (state) => ({ messages: [await llm.invoke(state.messages)] })) 10 .addNode('tools', new ToolNode(tools)) 11 .addEdge(START, 'agent') 12 // Run the tools the model asked for, or end when it answers without tool calls 13 .addConditionalEdges('agent', toolsCondition) 14 .addEdge('tools', 'agent') 15 .compile(); 16 } 17 18 const { tools: scopedTools } = await scalekit.tools.listScopedTools(identifier, { 19 filter: { connectionNames: [connectionName] }, 20 pageSize: 100, // up to 100 tools per page; page with nextPageToken for more 21 }); 22 // Tool definitions are typed as generic JSON, so read each field explicitly 23 const definitions = scopedTools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : [])); 24 const tools = definitions.map((definition) => 25 tool( 26 async (input) => { 27 const result = await scalekit.actions.executeTool({ 28 toolName: String(definition.name), 29 identifier, 30 connector: connectionName, 31 toolInput: input as Record, 32 }); 33 return JSON.stringify(result.data); 34 }, 35 { 36 name: String(definition.name), 37 description: String(definition.description ?? ''), 38 schema: definition.input_schema as Record, 39 }, 40 ), 41 ); 42 43 const graph = buildGraph(tools); 44 const result = await graph.invoke({ 45 messages: [{ role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }], 46 }); 47 console.log(result.messages.at(-1)?.content); ``` When the app itself rejects a request, the tool doesn’t raise: the app’s error response comes back as the tool’s result, and the model reads it like any other result. If a tool raises an error instead, such as a network failure, the two languages differ: * **Node.js:** `ToolNode` returns the error to the model. * **Python:** `ToolNode` returns only invalid-argument errors to the model, and raises the rest. To return every error to the model, create the node with `ToolNode(tools, handle_tool_errors=True)`. ## Check it worked [Section titled “Check it worked”](#check-it-worked) The script prints a summary of the user’s unread emails. `result["messages"]` (`result.messages` in Node.js) holds the whole run: the user message, the model’s tool calls, a tool message with each Gmail response, and the final answer. ## Use MCP instead [Section titled “Use MCP instead”](#use-mcp-instead) To give the graph the tools of a [Virtual MCP server](/agentkit/mcp/overview/), load them with LangChain’s MCP adapters and pass them to the same `build_graph`. Pass the server URL and a session token for this user. * Python ```sh pip install "langchain-mcp-adapters>=0.3,<1" ``` ```python 1 import asyncio 2 import os 3 from datetime import timedelta 4 5 from langchain_mcp_adapters.client import MultiServerMCPClient 6 7 # Returned by create_config when you created the Virtual MCP server 8 config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] 9 mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] 10 11 # Mint a fresh session token for this user before each agent run 12 mcp_token = actions.mcp.create_session_token( 13 mcp_config_id=config_id, 14 identifier=identifier, 15 expiry=timedelta(hours=1), 16 ).token 17 18 19 async def run(): 20 client = MultiServerMCPClient( 21 { 22 "scalekit": { 23 "transport": "streamable_http", 24 "url": mcp_url, 25 "headers": {"Authorization": f"Bearer {mcp_token}"}, 26 } 27 } 28 ) 29 graph = build_graph(await client.get_tools()) 30 result = await graph.ainvoke( 31 {"messages": [("user", "Fetch my last 5 unread emails and summarize them")]} 32 ) 33 print(result["messages"][-1].content) 34 35 36 asyncio.run(run()) ``` * Node.js ```sh npm install @langchain/mcp-adapters ``` ```typescript 1 import { MultiServerMCPClient } from '@langchain/mcp-adapters'; 2 3 // Returned by createConfig when you created the Virtual MCP server 4 const configId = process.env.SCALEKIT_MCP_CONFIG_ID!; 5 const mcpUrl = process.env.SCALEKIT_MCP_SERVER_URL!; 6 7 // Mint a fresh session token for this user before each agent run 8 const session = await scalekit.actions.mcp.createSessionToken({ 9 mcpConfigId: configId, 10 identifier, 11 expirySeconds: 60 * 60, 12 }); 13 14 const mcpClient = new MultiServerMCPClient({ 15 servers: { 16 scalekit: { 17 transport: 'http', 18 url: mcpUrl, 19 headers: { Authorization: `Bearer ${session.token}` }, 20 }, 21 }, 22 }); 23 try { 24 const mcpGraph = buildGraph(await mcpClient.getTools()); 25 const mcpResult = await mcpGraph.invoke({ 26 messages: [{ role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }], 27 }); 28 console.log(mcpResult.messages.at(-1)?.content); 29 } finally { 30 await mcpClient.close(); 31 } ``` See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](/agentkit/mcp/session-tokens/) to create the server, check the user’s connections and mint a token. ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # Mastra > Connect a Mastra agent to Scalekit tools over MCP, all in Node.js. Create a Virtual MCP server, mint a session token for the user, and run the agent. Connect a Mastra agent to Scalekit tools using MCP. Mastra has native MCP support via `@mastra/mcp`. Pass a Scalekit Virtual MCP server URL and a session token, and Mastra handles tool discovery automatically. Why MCP for Mastra Mastra’s tool system uses Zod schemas internally. The MCP path skips manual schema conversion. Mastra discovers tools and their schemas directly from the Scalekit MCP server. The whole example runs in Node.js on your server, in two files. `setup.mts` creates the Virtual MCP server once. `agent.mts` connects the user, mints a session token and runs the agent, every run. Run each with `npx tsx`, for example `npx tsx agent.mts`. ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. * The samples load `.env` with `import 'dotenv/config'`. ## Install [Section titled “Install”](#install) ```sh npm install @scalekit-sdk/node @mastra/core@1 @mastra/mcp@2 @ai-sdk/openai@4 dotenv ``` ## Initialize the Scalekit client [Section titled “Initialize the Scalekit client”](#initialize-the-scalekit-client) Start both files with the client: ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` ## Create the Virtual MCP server [Section titled “Create the Virtual MCP server”](#create-the-virtual-mcp-server) Create the server once per agent role, not once per user. Every user and every session reuses its `mcpServerUrl`. Server names are unique in an environment, so run this file once: a second run fails with `config with name 'gmail-user-tools' already exists`. setup.mts ```typescript // Run once per agent role, not once per user const { config } = await scalekit.actions.mcp.createConfig({ name: 'gmail-user-tools', connectionToolMappings: [{ connectionName: 'gmail', tools: ['gmail_fetch_mails'] }], }); console.log('Virtual MCP server:', config!.id, config!.mcpServerUrl); ``` [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) covers choosing tools, and listing, updating and deleting servers. ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) The rest of the code goes in `agent.mts` and runs before each agent run. The Virtual MCP server only calls tools for users who have authorized the connection. Before each agent run, make sure the user’s Gmail account is `ACTIVE`: ```typescript 1 import { createInterface } from 'node:readline/promises'; 2 import { ConnectorStatus } from '@scalekit-sdk/node'; 3 4 // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 5 const connectionName = 'gmail'; 6 const identifier = 'user_123'; // your app's unique user ID 7 8 let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 9 connectionName, 10 identifier, 11 }); 12 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 13 const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); 14 console.log('Authorize Gmail:', link); 15 const rl = createInterface({ input: process.stdin, output: process.stdout }); 16 await rl.question('Press Enter after authorizing...'); 17 rl.close(); 18 // Fetch the account again to pick up the new status 19 ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 20 connectionName, 21 identifier, 22 })); 23 } 24 25 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 26 throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); 27 } ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Mint a session token for the user [Section titled “Mint a session token for the user”](#mint-a-session-token-for-the-user) The server URL is the same for everyone. The **session token** carries the user’s identity. Mint a new token before each agent run, after the connect step: agent.mts ```typescript 1 import { ScalekitNotFoundException, ScalekitUnauthorizedException } from '@scalekit-sdk/node'; 2 3 // Run before each agent session, after the connect step 4 let mcpServerUrl: string; 5 let sessionToken: string; 6 try { 7 const { configs } = await scalekit.actions.mcp.listConfigs({ search: 'gmail-user-tools' }); 8 const server = configs.find((c) => c.name === 'gmail-user-tools'); 9 if (!server) throw new Error('Run setup.mts to create the gmail-user-tools server first'); 10 mcpServerUrl = server.mcpServerUrl; 11 12 const session = await scalekit.actions.mcp.createSessionToken({ 13 mcpConfigId: server.id, 14 identifier, // the user who authorized Gmail above 15 expirySeconds: 60 * 60, 16 }); 17 sessionToken = session.token; 18 } catch (err) { 19 if (err instanceof ScalekitUnauthorizedException) { 20 // Scalekit client credentials are wrong or expired: fix the environment variables 21 } else if (err instanceof ScalekitNotFoundException) { 22 // The server was deleted: create it again, then retry 23 } 24 throw err; // don't start the agent without a token 25 } ``` Don’t start the agent when minting fails: every tool call would return `401`. See [Errors and rate limits](/agentkit/reference/errors/) for what an error response contains. Set `expirySeconds` above the expected run time; [Mint session tokens](/agentkit/mcp/session-tokens/) covers when to mint again. Do not share one session token across users The server URL is safe to share, because every user gets the same one. The session token is not. Each token is scoped to one identifier, and any request carrying it runs as that user. Mint a token per user on your server, and never send it to client-side code. ## Build the agent [Section titled “Build the agent”](#build-the-agent) Pass the server URL to `MCPClient`, and the user’s session token as a bearer header in `requestInit`. Mastra fetches the tool list and schemas automatically. It names each tool after the server key, so `gmail_fetch_mails` appears to the model as `scalekit_gmail_fetch_mails`. agent.mts ```typescript 1 import { Agent } from '@mastra/core/agent'; 2 import { MCPClient } from '@mastra/mcp'; 3 import { openai } from '@ai-sdk/openai'; 4 5 const mcp = new MCPClient({ 6 servers: { 7 scalekit: { 8 url: new URL(mcpServerUrl), 9 requestInit: { headers: { Authorization: `Bearer ${sessionToken}` } }, 10 }, 11 }, 12 }); 13 14 try { 15 const tools = await mcp.listTools(); 16 17 const agent = new Agent({ 18 id: 'gmail-assistant', 19 name: 'Gmail assistant', 20 instructions: 'You are a helpful Gmail assistant.', 21 model: openai('gpt-4o'), 22 tools, 23 }); 24 25 const result = await agent.generate('Fetch my last 5 unread emails and summarize them'); 26 console.log(result.text); 27 } finally { 28 await mcp.disconnect(); 29 } ``` ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # OpenAI > Build an OpenAI agent with Scalekit-authenticated tools. Convert Scalekit's tool schemas to OpenAI's function calling format in one step. Build an agent using OpenAI’s GPT models that reads a user’s Gmail inbox. Scalekit’s tool schemas use `input_schema`: rename it to `parameters` and wrap it in OpenAI’s function format. ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. * The samples load `.env` with `load_dotenv()` from `python-dotenv` in Python, and `import 'dotenv/config'` in Node.js. ## Install [Section titled “Install”](#install) * Python ```sh pip install scalekit-sdk-python python-dotenv openai ``` * Node.js ```sh npm install @scalekit-sdk/node openai dotenv ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize [Section titled “Initialize”](#initialize) * Python ```python import os, json import scalekit.client from dotenv import load_dotenv from openai import OpenAI from google.protobuf.json_format import MessageToDict load_dotenv() scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions client = OpenAI() ``` * Node.js ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; import OpenAI from 'openai'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const openai = new OpenAI(); ``` ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) * Python ```python 1 # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 2 connection_name = "gmail" 3 identifier = "user_123" # your app's unique user ID 4 5 response = actions.get_or_create_connected_account( 6 connection_name=connection_name, identifier=identifier 7 ) 8 if response.connected_account.status != "ACTIVE": 9 link = actions.get_authorization_link( 10 connection_name=connection_name, identifier=identifier 11 ) 12 print("Authorize Gmail:", link.link) 13 input("Press Enter after authorizing...") 14 # Fetch the account again to pick up the new status 15 response = actions.get_or_create_connected_account( 16 connection_name=connection_name, identifier=identifier 17 ) 18 19 if response.connected_account.status != "ACTIVE": 20 raise RuntimeError( 21 f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." 22 ) ``` * Node.js ```typescript 1 import { createInterface } from 'node:readline/promises'; 2 import { ConnectorStatus } from '@scalekit-sdk/node'; 3 4 // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 5 const connectionName = 'gmail'; 6 const identifier = 'user_123'; // your app's unique user ID 7 8 let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 9 connectionName, 10 identifier, 11 }); 12 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 13 const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); 14 console.log('Authorize Gmail:', link); 15 const rl = createInterface({ input: process.stdin, output: process.stdout }); 16 await rl.question('Press Enter after authorizing...'); 17 rl.close(); 18 // Fetch the account again to pick up the new status 19 ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 20 connectionName, 21 identifier, 22 })); 23 } 24 25 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 26 throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); 27 } ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Run the agent [Section titled “Run the agent”](#run-the-agent) Fetch tools scoped to this user, convert to OpenAI’s function format, then run the tool-calling loop. [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. * Python ```python 1 # Fetch and convert tools to OpenAI format 2 scoped_response, _ = actions.tools.list_scoped_tools( 3 identifier="user_123", 4 filter={"connection_names": ["gmail"]}, 5 page_size=100, # up to 100 tools per page; page with next_page_token for more 6 ) 7 llm_tools = [ 8 { 9 "type": "function", 10 "function": { 11 "name": MessageToDict(t.tool).get("definition", {}).get("name"), 12 "description": MessageToDict(t.tool).get("definition", {}).get("description", ""), 13 "parameters": MessageToDict(t.tool).get("definition", {}).get("input_schema", {}), 14 }, 15 } 16 for t in scoped_response.tools 17 ] 18 19 # Run the agent loop 20 messages = [{"role": "user", "content": "Fetch my last 5 unread emails and summarize them"}] 21 22 while True: 23 response = client.chat.completions.create( 24 model="gpt-4o", 25 tools=llm_tools, 26 messages=messages, 27 ) 28 message = response.choices[0].message 29 if not message.tool_calls: 30 print(message.content) 31 break 32 33 messages.append(message) 34 for tc in message.tool_calls: 35 result = actions.execute_tool( 36 tool_name=tc.function.name, 37 identifier="user_123", 38 connection_name="gmail", 39 tool_input=json.loads(tc.function.arguments), 40 ) 41 messages.append({ 42 "role": "tool", 43 "tool_call_id": tc.id, 44 "content": str(result.data), 45 }) ``` * Node.js ```typescript 1 // Fetch and convert tools to OpenAI format 2 const { tools } = await scalekit.tools.listScopedTools('user_123', { 3 filter: { connectionNames: ['gmail'] }, 4 pageSize: 100, // up to 100 tools per page; page with nextPageToken for more 5 }); 6 // Tool definitions are typed as generic JSON, so read each field explicitly 7 const definitions = tools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : [])); 8 const llmTools: OpenAI.ChatCompletionTool[] = definitions.map((definition) => ({ 9 type: 'function', 10 function: { 11 name: String(definition.name), 12 description: String(definition.description ?? ''), 13 parameters: definition.input_schema as OpenAI.FunctionParameters, 14 }, 15 })); 16 17 // Run the agent loop 18 const messages: OpenAI.ChatCompletionMessageParam[] = [ 19 { role: 'user', content: 'Fetch my last 5 unread emails and summarize them' }, 20 ]; 21 22 while (true) { 23 const response = await openai.chat.completions.create({ 24 model: 'gpt-4o', 25 tools: llmTools, 26 messages, 27 }); 28 const message = response.choices[0].message; 29 if (!message.tool_calls?.length) { 30 console.log(message.content); 31 break; 32 } 33 messages.push(message); 34 for (const tc of message.tool_calls) { 35 if (tc.type !== 'function') continue; // only function tools are registered 36 const result = await scalekit.actions.executeTool({ 37 toolName: tc.function.name, 38 identifier: 'user_123', 39 connector: 'gmail', 40 toolInput: JSON.parse(tc.function.arguments), 41 }); 42 messages.push({ role: 'tool', tool_call_id: tc.id, content: JSON.stringify(result.data) }); 43 } 44 } ``` ## Use the Responses API [Section titled “Use the Responses API”](#use-the-responses-api) OpenAI’s [Responses API](https://platform.openai.com/docs/api-reference/responses) is a stateful alternative to Chat Completions. Instead of managing conversation history yourself, you pass `previous_response_id` to continue a session. It takes tools in a flat shape, `{type, name, description, parameters}`, without the `function` wrapper that Chat Completions uses, so convert the tools first. Using a proxy or gateway The Responses API works with an OpenAI API key. Many OpenAI-compatible proxies and gateways support only Chat Completions, so check yours before you switch, or use the Chat Completions loop above. * Python ```python 1 # Responses API tools are flat: no "function" wrapper. 2 # strict=False because Scalekit schemas don't follow strict mode's rules. 3 response_tools = [{"type": "function", **t["function"], "strict": False} for t in llm_tools] 4 5 response = client.responses.create( 6 model="gpt-4o", 7 input="Fetch my last 5 unread emails and summarize them", 8 tools=response_tools, 9 ) 10 11 while any(item.type == "function_call" for item in response.output): 12 tool_results = [ 13 { 14 "type": "function_call_output", 15 "call_id": item.call_id, 16 "output": str(actions.execute_tool( 17 tool_name=item.name, 18 identifier="user_123", 19 connection_name="gmail", 20 tool_input=json.loads(item.arguments), 21 ).data), 22 } 23 for item in response.output 24 if item.type == "function_call" 25 ] 26 response = client.responses.create( 27 model="gpt-4o", 28 previous_response_id=response.id, 29 input=tool_results, 30 tools=response_tools, 31 ) 32 33 for item in response.output: 34 if item.type == "message": 35 print(item.content[0].text) ``` * Node.js ```typescript 1 // Responses API tools are flat: no `function` wrapper. 2 // strict: false because Scalekit schemas don't follow strict mode's rules. 3 const responseTools: OpenAI.Responses.FunctionTool[] = definitions.map((definition) => ({ 4 type: 'function', 5 name: String(definition.name), 6 description: String(definition.description ?? ''), 7 parameters: definition.input_schema as Record, 8 strict: false, 9 })); 10 11 let response = await openai.responses.create({ 12 model: 'gpt-4o', 13 input: 'Fetch my last 5 unread emails and summarize them', 14 tools: responseTools, 15 }); 16 17 while (response.output.some(item => item.type === 'function_call')) { 18 const toolResults = await Promise.all( 19 response.output 20 .filter(item => item.type === 'function_call') 21 .map(async item => { 22 const result = await scalekit.actions.executeTool({ 23 toolName: item.name, 24 identifier: 'user_123', 25 connector: 'gmail', 26 toolInput: JSON.parse(item.arguments), 27 }); 28 return { 29 type: 'function_call_output' as const, 30 call_id: item.call_id, 31 output: JSON.stringify(result.data), 32 }; 33 }) 34 ); 35 response = await openai.responses.create({ 36 model: 'gpt-4o', 37 previous_response_id: response.id, 38 input: toolResults, 39 tools: responseTools, 40 }); 41 } 42 43 const message = response.output.find(item => item.type === 'message'); 44 if (message?.type === 'message') { 45 for (const part of message.content) { 46 if (part.type === 'output_text') console.log(part.text); 47 } 48 } ``` ## Use MCP instead [Section titled “Use MCP instead”](#use-mcp-instead) If you prefer the MCP approach, connect your OpenAI agent via the [Vercel AI SDK + MCP](/agentkit/examples/vercel-ai#use-mcp-instead) or LangChain’s MCP client with a Scalekit-generated URL. See [Virtual MCP servers](/agentkit/mcp/overview/) for the URL setup. ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # OpenAI Agents SDK > Build an OpenAI Agents SDK agent that reads Gmail as the user with AgentKit: turn Scalekit tools into function tools, or connect a Virtual MCP server. Build an agent with the [OpenAI Agents SDK](https://openai.github.io/openai-agents-python/) that reads a user’s Gmail inbox. Each Scalekit tool becomes an Agents SDK function tool that runs the call through Scalekit as the user, and the SDK’s runner handles the tool-calling loop. Calling the Chat Completions or Responses API directly instead? See [OpenAI](/agentkit/examples/openai/). ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. * The samples load `.env` with `load_dotenv()` from `python-dotenv` in Python, and `import 'dotenv/config'` in Node.js. ## Install [Section titled “Install”](#install) * Python ```sh pip install scalekit-sdk-python python-dotenv openai-agents ``` * Node.js ```sh npm install @scalekit-sdk/node dotenv @openai/agents ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize [Section titled “Initialize”](#initialize) * Python ```python import os from dotenv import load_dotenv from scalekit import ScalekitClient load_dotenv() scalekit_client = ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), ) actions = scalekit_client.actions ``` * Node.js ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` Later blocks reuse this setup. ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) * Python ```python 1 # Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 2 connection_name = "gmail" 3 identifier = "user_123" # your app's unique user ID 4 5 response = actions.get_or_create_connected_account( 6 connection_name=connection_name, identifier=identifier 7 ) 8 if response.connected_account.status != "ACTIVE": 9 link = actions.get_authorization_link( 10 connection_name=connection_name, identifier=identifier 11 ) 12 print("Authorize Gmail:", link.link) 13 input("Press Enter after authorizing...") 14 # Fetch the account again to pick up the new status 15 response = actions.get_or_create_connected_account( 16 connection_name=connection_name, identifier=identifier 17 ) 18 19 if response.connected_account.status != "ACTIVE": 20 raise RuntimeError( 21 f"Gmail is {response.connected_account.status}, not ACTIVE. Authorize it and run again." 22 ) ``` * Node.js ```typescript 1 import { createInterface } from 'node:readline/promises'; 2 import { ConnectorStatus } from '@scalekit-sdk/node'; 3 4 // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 5 const connectionName = 'gmail'; 6 const identifier = 'user_123'; // your app's unique user ID 7 8 let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 9 connectionName, 10 identifier, 11 }); 12 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 13 const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); 14 console.log('Authorize Gmail:', link); 15 const rl = createInterface({ input: process.stdin, output: process.stdout }); 16 await rl.question('Press Enter after authorizing...'); 17 rl.close(); 18 // Fetch the account again to pick up the new status 19 ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 20 connectionName, 21 identifier, 22 })); 23 } 24 25 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 26 throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); 27 } ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Run the agent [Section titled “Run the agent”](#run-the-agent) Fetch the tools scoped to this user and turn each one into a function tool. The tool’s JSON Schema is passed through as is, with strict mode off. When the model calls a tool, the function runs it with `execute_tool` (`executeTool` in Node.js) for this user and returns the app’s response. [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. * Python ```python 1 import asyncio 2 import json 3 4 from agents import Agent, FunctionTool, Runner 5 from google.protobuf.json_format import MessageToDict 6 7 scoped_response, _ = actions.tools.list_scoped_tools( 8 identifier=identifier, 9 filter={"connection_names": [connection_name]}, 10 page_size=100, # up to 100 tools per page; page with next_page_token for more 11 ) 12 definitions = [MessageToDict(t.tool).get("definition", {}) for t in scoped_response.tools] 13 14 15 def scalekit_tool(definition): 16 async def run_tool(ctx, args_json: str) -> str: 17 # execute_tool is a blocking call, so run it off the event loop 18 result = await asyncio.to_thread( 19 actions.execute_tool, 20 tool_name=definition["name"], 21 identifier=identifier, 22 connection_name=connection_name, 23 tool_input=json.loads(args_json or "{}"), 24 ) 25 return json.dumps(result.data) 26 27 return FunctionTool( 28 name=definition["name"], 29 description=definition.get("description", ""), 30 params_json_schema=definition.get("input_schema", {"type": "object", "properties": {}}), 31 on_invoke_tool=run_tool, 32 strict_json_schema=False, # Scalekit schemas don't follow strict mode's rules 33 ) 34 35 36 agent = Agent( 37 name="Gmail assistant", 38 instructions="Answer questions about the user's Gmail inbox.", 39 model="gpt-4o", 40 tools=[scalekit_tool(definition) for definition in definitions], 41 ) 42 result = Runner.run_sync(agent, "Fetch my last 5 unread emails and summarize them") 43 print(result.final_output) ``` * Node.js ```typescript 1 import { Agent, run, tool } from '@openai/agents'; 2 3 const { tools: scopedTools } = await scalekit.tools.listScopedTools(identifier, { 4 filter: { connectionNames: [connectionName] }, 5 pageSize: 100, // up to 100 tools per page; page with nextPageToken for more 6 }); 7 // Tool definitions are typed as generic JSON, so read each field explicitly 8 const definitions = scopedTools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : [])); 9 const tools = definitions.map((definition) => 10 tool({ 11 name: String(definition.name), 12 description: String(definition.description ?? ''), 13 parameters: definition.input_schema as any, 14 strict: false, // Scalekit schemas don't follow strict mode's rules 15 execute: async (input) => { 16 const result = await scalekit.actions.executeTool({ 17 toolName: String(definition.name), 18 identifier, 19 connector: connectionName, 20 toolInput: input as Record, 21 }); 22 return JSON.stringify(result.data); 23 }, 24 }), 25 ); 26 27 const agent = new Agent({ 28 name: 'Gmail assistant', 29 instructions: "Answer questions about the user's Gmail inbox.", 30 model: 'gpt-4o', 31 tools, 32 }); 33 const result = await run(agent, 'Fetch my last 5 unread emails and summarize them'); 34 console.log(result.finalOutput); ``` If a tool function raises an error, the Agents SDK tells the model the call failed, so the model can retry or explain the failure. When the app itself rejects a request, `execute_tool` doesn’t raise: the app’s error response comes back in `data`, and the model reads it like any other result. ## Check it worked [Section titled “Check it worked”](#check-it-worked) The script prints a summary of the user’s unread emails. To see each tool call and the Gmail response it returned, read `result.new_items` (`result.newItems` in Node.js). ## Use MCP instead [Section titled “Use MCP instead”](#use-mcp-instead) The Agents SDK connects to MCP servers natively. Point it at a [Virtual MCP server](/agentkit/mcp/overview/) with a session token for this user, and the agent discovers the server’s tools on its own. Add this below the code above. It reuses the setup and, in Node.js, the `Agent` and `run` imports. * Python ```python 1 import asyncio 2 import os 3 from datetime import timedelta 4 5 from agents import Agent, Runner 6 from agents.mcp import MCPServerStreamableHttp 7 8 # Returned by create_config when you created the Virtual MCP server 9 config_id = os.environ["SCALEKIT_MCP_CONFIG_ID"] 10 mcp_url = os.environ["SCALEKIT_MCP_SERVER_URL"] 11 12 # Mint a fresh session token for this user before each agent run 13 mcp_token = actions.mcp.create_session_token( 14 mcp_config_id=config_id, 15 identifier=identifier, 16 expiry=timedelta(hours=1), 17 ).token 18 19 20 async def run(): 21 async with MCPServerStreamableHttp( 22 name="scalekit", 23 params={"url": mcp_url, "headers": {"Authorization": f"Bearer {mcp_token}"}}, 24 ) as server: 25 agent = Agent( 26 name="Gmail assistant", 27 instructions="Answer questions about the user's Gmail inbox.", 28 model="gpt-4o", 29 mcp_servers=[server], 30 ) 31 result = await Runner.run(agent, "Fetch my last 5 unread emails and summarize them") 32 print(result.final_output) 33 34 35 asyncio.run(run()) ``` * Node.js ```typescript 1 import { MCPServerStreamableHttp } from '@openai/agents'; 2 3 // Returned by createConfig when you created the Virtual MCP server 4 const configId = process.env.SCALEKIT_MCP_CONFIG_ID!; 5 const mcpUrl = process.env.SCALEKIT_MCP_SERVER_URL!; 6 7 // Mint a fresh session token for this user before each agent run 8 const session = await scalekit.actions.mcp.createSessionToken({ 9 mcpConfigId: configId, 10 identifier, 11 expirySeconds: 60 * 60, 12 }); 13 14 const mcpServer = new MCPServerStreamableHttp({ 15 name: 'scalekit', 16 url: mcpUrl, 17 requestInit: { headers: { Authorization: `Bearer ${session.token}` } }, 18 }); 19 await mcpServer.connect(); 20 try { 21 const mcpAgent = new Agent({ 22 name: 'Gmail assistant', 23 instructions: "Answer questions about the user's Gmail inbox.", 24 model: 'gpt-4o', 25 mcpServers: [mcpServer], 26 }); 27 const mcpResult = await run(mcpAgent, 'Fetch my last 5 unread emails and summarize them'); 28 console.log(mcpResult.finalOutput); 29 } finally { 30 await mcpServer.close(); 31 } ``` See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) and [Mint session tokens](/agentkit/mcp/session-tokens/) to create the server, check the user’s connections and mint a token. ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # Vercel AI SDK > Build a Vercel AI SDK agent that reads Gmail as the user with AgentKit: wrap Scalekit tools with tool() and jsonSchema(), or connect to a Virtual MCP server. Build an agent using the Vercel AI SDK that reads a user’s Gmail inbox. Wrap each Scalekit tool with `tool()` from the `ai` package, and pass its JSON Schema to the model with `jsonSchema()`. ## Before you start [Section titled “Before you start”](#before-you-start) * Create a Gmail connection in **AgentKit** > **Connections**. The samples use the connection name `gmail`, so change it if yours differs. See [Set up a connection](/agentkit/connections/). * Have the `.env` file from the [Quickstart](/agentkit/quickstart/), and add your OpenAI API key as `OPENAI_API_KEY` to it. * The samples load `.env` with `import 'dotenv/config'`. ## Install [Section titled “Install”](#install) ```sh npm install @scalekit-sdk/node ai@7 @ai-sdk/openai@4 dotenv ``` For Node.js, save the code in a `.mts` file, such as `agent.mts`, and run it with `npx tsx agent.mts`. The `.mts` extension makes the file an ES module, so it can use top-level `await`. ## Initialize [Section titled “Initialize”](#initialize) ```typescript import 'dotenv/config'; import { ScalekitClient } from '@scalekit-sdk/node'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); ``` ## Connect the user to Gmail [Section titled “Connect the user to Gmail”](#connect-the-user-to-gmail) ```typescript 1 import { createInterface } from 'node:readline/promises'; 2 import { ConnectorStatus } from '@scalekit-sdk/node'; 3 4 // Connect the user's Gmail account, and wait until it's ACTIVE before calling tools 5 const connectionName = 'gmail'; 6 const identifier = 'user_123'; // your app's unique user ID 7 8 let { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 9 connectionName, 10 identifier, 11 }); 12 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 13 const { link } = await scalekit.actions.getAuthorizationLink({ connectionName, identifier }); 14 console.log('Authorize Gmail:', link); 15 const rl = createInterface({ input: process.stdin, output: process.stdout }); 16 await rl.question('Press Enter after authorizing...'); 17 rl.close(); 18 // Fetch the account again to pick up the new status 19 ({ connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 20 connectionName, 21 identifier, 22 })); 23 } 24 25 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 26 throw new Error(`Gmail is not ACTIVE (status ${connectedAccount?.status}). Authorize it and run again.`); 27 } ``` See [Authorize a user](/agentkit/tools/authorize/) for production auth handling. ## Run the agent [Section titled “Run the agent”](#run-the-agent) Fetch the tools scoped to this user, wrap each one with the AI SDK’s `tool()` helper, then let `generateText` run the tool-calling loop. [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/) explains the scoped tool list, paging past 100 tools, and how to read `data` when the app returns an error. ```typescript 1 import { generateText, jsonSchema, stepCountIs, tool } from 'ai'; 2 import { openai } from '@ai-sdk/openai'; 3 4 const { tools: scopedTools } = await scalekit.tools.listScopedTools('user_123', { 5 filter: { connectionNames: ['gmail'] }, 6 pageSize: 100, // up to 100 tools per page; page with nextPageToken for more 7 }); 8 9 // Tool definitions are typed as generic JSON, so read each field explicitly 10 const definitions = scopedTools.flatMap((t) => (t.tool?.definition ? [t.tool.definition] : [])); 11 12 const tools = Object.fromEntries( 13 definitions.map((definition) => { 14 const name = String(definition.name); 15 const schema = definition.input_schema ?? { type: 'object', properties: {} }; 16 return [ 17 name, 18 tool({ 19 description: String(definition.description ?? ''), 20 inputSchema: jsonSchema>(schema as Parameters[0]), 21 execute: async (args) => { 22 const result = await scalekit.actions.executeTool({ 23 toolName: name, 24 identifier: 'user_123', 25 connector: 'gmail', 26 toolInput: args, 27 }); 28 return result.data; 29 }, 30 }), 31 ]; 32 }), 33 ); 34 35 const { text } = await generateText({ 36 model: openai('gpt-4o'), 37 tools, 38 stopWhen: stepCountIs(5), 39 prompt: 'Fetch my last 5 unread emails and summarize them', 40 }); 41 console.log(text); ``` ## Use MCP instead [Section titled “Use MCP instead”](#use-mcp-instead) The Vercel AI SDK connects to MCP servers with `createMCPClient` from `@ai-sdk/mcp`. Pass the Virtual MCP server URL and a session token, and the client loads the server’s tools for you. ```sh npm install @ai-sdk/mcp@2 ``` Mint the session token on your backend for each agent session with `scalekit.actions.mcp.createSessionToken`, as shown in [Mint session tokens](/agentkit/mcp/session-tokens/#mint-the-token), and pass it to this code. ```typescript 1 import { createMCPClient } from '@ai-sdk/mcp'; 2 import { generateText, stepCountIs } from 'ai'; 3 import { openai } from '@ai-sdk/openai'; 4 5 const mcpUrl = process.env.SCALEKIT_MCP_SERVER_URL!; // mcp_server_url from your Virtual MCP server 6 const mcpToken = process.env.SCALEKIT_MCP_SESSION_TOKEN!; // minted on your backend for this user 7 8 const mcpClient = await createMCPClient({ 9 transport: { 10 type: 'http', 11 url: mcpUrl, 12 headers: { Authorization: `Bearer ${mcpToken}` }, 13 }, 14 }); 15 16 const tools = await mcpClient.tools(); 17 18 const { text } = await generateText({ 19 model: openai('gpt-4o'), 20 tools, 21 stopWhen: stepCountIs(5), 22 prompt: 'Fetch my last 5 unread emails and summarize them', 23 }); 24 await mcpClient.close(); 25 console.log(text); ``` See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/) to create the server, and [Mint session tokens](/agentkit/mcp/session-tokens/) to check the user’s connections and mint a token. ## Next [Section titled “Next”](#next) [Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect. [AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect. --- # DOCUMENT BOUNDARY --- # Add Enterprise SSO to Next.js with Auth.js > Wire Scalekit's OIDC interface into Auth.js to ship per-tenant enterprise SSO in Next.js without touching SAML or IdP-specific code. Enterprise customers don’t want to hand over their employees’ credentials to your app — they want SSO through their own IdP. Auth.js handles sessions well, but it has no concept of per-tenant SAML connections or routing by organization. Scalekit fills that gap: it exposes a single OIDC-compliant endpoint that sits in front of every IdP your customers use. This cookbook wires those two pieces together so your app gets enterprise SSO without writing a line of SAML code. ## The problem [Section titled “The problem”](#the-problem) Adding enterprise SSO to a Next.js app sounds simple until you start building it: * **SAML complexity** — every IdP (Okta, Azure AD, Google Workspace, Ping) uses different metadata, certificate rotation schedules, and attribute mappings. You end up maintaining per-IdP configuration forever. * **Per-tenant routing** — each sign-in attempt needs to resolve to the right connection for that customer. A single `clientId` in Auth.js doesn’t model this. * **Duplicate boilerplate** — Okta setup is not Azure AD setup. You write the integration N times, once per IdP your enterprise customers use. * **Session ownership** — SAML assertions and OIDC tokens are not app sessions. Bridging them correctly (handling expiry, attribute claims, refresh) is error-prone without a clear seam. ## Who needs this [Section titled “Who needs this”](#who-needs-this) This cookbook is for you if: * ✅ You’re building a multi-tenant B2B SaaS app * ✅ You already use Auth.js for session management and want to keep it * ✅ You have enterprise customers who require SSO through their own IdP * ✅ You want to avoid ripping out Auth.js to adopt a fully managed auth platform You **don’t** need this if: * ❌ You’re building a consumer app with no enterprise requirements * ❌ Your app has no concept of organizations or tenants * ❌ You don’t have customers asking for Okta/Azure AD/Google Workspace integration ## The solution [Section titled “The solution”](#the-solution) Scalekit exposes a single OIDC-compliant authorization endpoint. Auth.js treats it like any other OIDC provider and manages the session after the callback. You never write SAML code — Scalekit handles the protocol translation, certificate rotation, and attribute normalization for every IdP your customers connect. The routing params (`connection_id`, `organization_id`, `domain`) let you target the right enterprise connection at sign-in time. ## Implementation [Section titled “Implementation”](#implementation) ### 1. Set up Scalekit [Section titled “1. Set up Scalekit”](#1-set-up-scalekit) Create an environment in the [Scalekit dashboard](https://app.scalekit.com/): 1. Copy your **Issuer URL** (e.g. `https://yourenv.scalekit.dev`), **Client ID** (`skc_...`), and **Client Secret** from **API Keys**. 2. Register your redirect URI: `http://localhost:3000/auth/callback/scalekit` > This guide sets `basePath: "/auth"` in `auth.ts` — a custom override. The Auth.js v5 default is `/api/auth`. Register your redirect URI to match whatever `basePath` you configure or the OAuth flow will fail. 3. Create an **Organization** and add an **SSO Connection** for your test IdP. 4. Copy the **Connection ID** (`conn_...`) — you’ll use it to route sign-in attempts during development. ### 2. Install dependencies [Section titled “2. Install dependencies”](#2-install-dependencies) ```bash pnpm add next-auth ``` Auth.js v5 (`next-auth@5`) ships as a single package. No separate adapter is needed for JWT sessions. ### 3. Add the Scalekit provider [Section titled “3. Add the Scalekit provider”](#3-add-the-scalekit-provider) Native provider coming soon PR [#13392](https://github.com/nextauthjs/next-auth/pull/13392) adds `next-auth/providers/scalekit` natively to Auth.js. Until it merges, copy the provider file below into your project as `providers/scalekit.ts`. providers/scalekit.ts ```typescript 1 import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers" 2 3 export interface ScalekitProfile extends Record { 4 sub: string 5 email: string 6 email_verified: boolean 7 name: string 8 given_name: string 9 family_name: string 10 picture: string 11 oid: string // organization_id 12 } 13 14 export default function Scalekit

( 15 options: OAuthUserConfig

& { 16 issuer: string 17 organizationId?: string 18 connectionId?: string 19 domain?: string 20 } 21 ): OAuthConfig

{ 22 const { issuer, organizationId, connectionId, domain } = options 23 24 return { 25 id: "scalekit", 26 name: "Scalekit", 27 type: "oidc", 28 issuer, 29 authorization: { 30 params: { 31 scope: "openid email profile", 32 ...(connectionId && { connection_id: connectionId }), 33 ...(organizationId && { organization_id: organizationId }), 34 ...(domain && { domain }), 35 }, 36 }, 37 profile(profile) { 38 return { 39 id: profile.sub, 40 name: profile.name ?? `${profile.given_name} ${profile.family_name}`, 41 email: profile.email, 42 image: profile.picture ?? null, 43 } 44 }, 45 style: { bg: "#6f42c1", text: "#fff" }, 46 options, 47 } 48 } ``` After PR #13392 merges, replace the local import with: ```typescript import Scalekit from "next-auth/providers/scalekit" ``` ### 4. Configure `auth.ts` [Section titled “4. Configure auth.ts”](#4-configure-authts) Create `auth.ts` in your project root: ```typescript 1 import NextAuth from "next-auth" 2 import Scalekit from "./providers/scalekit" // → "next-auth/providers/scalekit" after PR #13392 3 4 export const { handlers, auth, signIn, signOut } = NextAuth({ 5 providers: [ 6 Scalekit({ 7 issuer: process.env.AUTH_SCALEKIT_ISSUER!, 8 clientId: process.env.AUTH_SCALEKIT_ID!, 9 clientSecret: process.env.AUTH_SCALEKIT_SECRET!, 10 // Routing: set one of these (see step 7 for strategy) 11 connectionId: process.env.AUTH_SCALEKIT_CONNECTION_ID, 12 }), 13 ], 14 basePath: "/auth", 15 session: { strategy: "jwt" }, 16 }) ``` `basePath: "/auth"` is required to match the redirect URI you registered in step 1. Without it, Auth.js uses `/api/auth` and the Scalekit callback will fail. ### 5. Set environment variables [Section titled “5. Set environment variables”](#5-set-environment-variables) .env.local ```bash # Generate with: npx auth secret AUTH_SECRET= # From Scalekit dashboard → API Keys AUTH_SCALEKIT_ISSUER=https://yourenv.scalekit.dev AUTH_SCALEKIT_ID=skc_... AUTH_SCALEKIT_SECRET= # Connection ID for development routing (conn_...) # In production, resolve this dynamically per tenant — see step 7 AUTH_SCALEKIT_CONNECTION_ID=conn_... ``` `AUTH_SECRET` is not optional. Auth.js uses it to sign JWTs and encrypt session cookies. Missing it causes sign-in to fail silently. ### 6. Wire up route handlers [Section titled “6. Wire up route handlers”](#6-wire-up-route-handlers) Create `app/auth/[...nextauth]/route.ts`: ```typescript import { handlers } from "@/auth" export const { GET, POST } = handlers ``` This exposes `GET /auth/callback/scalekit` and `POST /auth/signout` — the endpoints Auth.js needs. The directory must be `app/auth/` (not `app/api/auth/`) to match the `basePath` you configured. ### 7. SSO routing strategies [Section titled “7. SSO routing strategies”](#7-sso-routing-strategies) Scalekit resolves which IdP connection to activate using these params (highest to lowest precedence): ```typescript Scalekit({ issuer: process.env.AUTH_SCALEKIT_ISSUER!, clientId: process.env.AUTH_SCALEKIT_ID!, clientSecret: process.env.AUTH_SCALEKIT_SECRET!, // Option A — exact connection (dev / single-tenant use) connectionId: "conn_...", // Option B — org's active connection (multi-tenant: look up org from user's DB record) organizationId: "org_...", // Option C — resolve org from email domain (useful at login prompt) domain: "acme.com", }) ``` In production, don’t hardcode these values. Store `organizationId` or `connectionId` per tenant in your database, then construct the `signIn()` call dynamically based on the authenticated user’s org: ```typescript // Example: look up org at sign-in time const org = await db.organizations.findByDomain(emailDomain) await signIn("scalekit", { organizationId: org.scalekitOrgId, redirectTo: "/dashboard", }) ``` ### 8. Trigger sign-in and read the session [Section titled “8. Trigger sign-in and read the session”](#8-trigger-sign-in-and-read-the-session) A server component reads the session, and a sign-in form triggers the flow: app/page.tsx ```typescript 1 import { auth, signIn } from "@/auth" 2 3 export default async function Home() { 4 const session = await auth() 5 6 if (session) { 7 return ( 8

9

Signed in as {session.user?.email}

10
11 ) 12 } 13 14 return ( 15
{ 17 "use server" 18 await signIn("scalekit", { redirectTo: "/dashboard" }) 19 }} 20 > 21 22
23 ) 24 } ``` `session.user` includes `name`, `email`, and `image` normalized from the Scalekit OIDC profile. ## Testing [Section titled “Testing”](#testing) 1. Run `pnpm dev` and visit `http://localhost:3000`. 2. Click **Sign in with SSO** — you should be redirected to your IdP’s login page. 3. Complete authentication and confirm you land back on your app. 4. Check the session at `http://localhost:3000/api/auth/session` or read it from a server component — you should see `user.email` populated. If the redirect fails immediately, enable debug logging to trace the OIDC callback: ```bash AUTH_DEBUG=true pnpm dev ``` ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) 1. **Wrong redirect URI** — registering `/api/auth/callback/scalekit` instead of `/auth/callback/scalekit`. This guide sets `basePath: "/auth"` (a custom override, not the v5 default — the default remains `/api/auth`). The URI in Scalekit’s dashboard must match the callback path Auth.js actually uses. 2. **Missing `AUTH_SECRET`** — sign-in appears to start but fails on the callback with no visible error. Always set `AUTH_SECRET`. Generate one with `npx auth secret`. 3. **Hardcoding `connectionId` in production** — works in development, breaks for every other tenant. Store connection identifiers per-organization in your database and resolve them at runtime. 4. **Missing `basePath` in `auth.ts`** — if you omit `basePath: "/auth"`, Auth.js defaults to `/api/auth`. Your route handler must be at `app/api/auth/[...nextauth]/route.ts` and your redirect URI must use `/api/auth/callback/scalekit`. Pick one and be consistent. 5. **Using the wrong import path** — `next-auth/providers/scalekit` only resolves after PR #13392 merges. Until then, the local file at `./providers/scalekit` is the correct import. ## Production notes [Section titled “Production notes”](#production-notes) * **Rotate secrets without code changes** — update `AUTH_SCALEKIT_SECRET` in your environment configuration; Scalekit handles IdP certificate rotation automatically. * **Dynamic connection routing** — store `organizationId` or `connectionId` per tenant in your database. Resolve at sign-in time based on the user’s email domain or their existing tenant membership. * **Debug OIDC callback issues** — set `AUTH_DEBUG=true` temporarily in production to emit detailed callback traces. Remove it after diagnosing. * **Session persistence** — JWT sessions (the default) work without a database. If you need server-side session invalidation, add an Auth.js adapter (e.g. Prisma, Drizzle) and switch to `strategy: "database"`. * **Scalekit handles IdP complexity** — certificate rotation, SAML metadata updates, and attribute mapping changes happen in the Scalekit dashboard without touching your code. ## Next steps [Section titled “Next steps”](#next-steps) * [scalekit-developers/scalekit-authjs-example](https://github.com/scalekit-developers/scalekit-authjs-example) — full working repo for this cookbook * [Auth.js PR #13392](https://github.com/nextauthjs/next-auth/pull/13392) — track native Scalekit provider availability * [Scalekit SSO routing documentation](https://docs.scalekit.com/sso/quickstart) — full reference for `connection_id`, `organization_id`, and `domain` routing params * [Auth.js adapters](https://authjs.dev/getting-started/database) — add database-backed sessions for server-side invalidation * [Scalekit organization management API](https://docs.scalekit.com/apis) — look up `organizationId` dynamically from your tenant records --- # DOCUMENT BOUNDARY --- # Add Scalekit hosted auth to a Next.js app > Wire Scalekit hosted login into the Next.js App Router with server-side sessions, transparent token refresh, and logout. Scalekit’s [Full Stack Auth journey](/authenticate/fsa/quickstart/) shows the hosted-login flow with Express, Flask, Gin, and Spring. None of those map cleanly onto the Next.js App Router, where there is no long-lived `req`/`res` pair: authentication runs across Route Handlers, Server Components, and middleware, and sessions live in cookies you set from the server. This cookbook ports the complete flow to Next.js 15 (App Router): redirect users to Scalekit’s hosted login, exchange the authorization code on a callback Route Handler, store tokens in `HttpOnly` cookies, validate and refresh them on every request, and sign users out cleanly. You get enterprise SSO, social login, and passwordless out of the box, because the hosted page handles every method you enable in the dashboard. ## The problem [Section titled “The problem”](#the-problem) You want production-grade authentication in a Next.js App Router app, and you have decided to use Scalekit’s hosted login page so you don’t build or maintain login UI. Three things make this non-trivial: * **No `req`/`res` lifecycle.** The Express examples set cookies on a response object. In the App Router you set cookies through the `cookies()` API and `NextResponse`, in different files for different stages of the flow. * **The Edge runtime can’t run the Node SDK.** Middleware runs on the Edge runtime by default. The Scalekit Node SDK depends on Node APIs, so token validation belongs in the Node.js runtime, not in default middleware. * **Refresh tokens rotate.** Scalekit issues a new refresh token every time you redeem one. If you store tokens carelessly, a refresh races itself and logs the user out. ## The approach [Section titled “The approach”](#the-approach) Keep every token operation on the server and give each stage of the flow its own file: | Stage | File | Runtime | | ------------------------------------ | --------------------------------- | ------- | | Build the Scalekit client once | `lib/scalekit.ts` | Node.js | | Read and write session cookies | `lib/session.ts` | Node.js | | Start login (redirect to Scalekit) | `app/login/route.ts` | Node.js | | Handle the callback (code exchange) | `app/api/callback/route.ts` | Node.js | | Validate and refresh on each request | `lib/session.ts` → `getSession()` | Node.js | | Sign out | `app/logout/route.ts` | Node.js | Validate the session inside Server Components and Route Handlers — both run on the Node.js runtime — instead of inside Edge middleware. Use middleware only as a lightweight gate that checks for the presence of a session cookie. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Next.js 15 app using the App Router. * A Scalekit account with an **Environment URL**, **Client ID**, and **Client Secret** from **Dashboard > Developers > API Credentials**. * `http://localhost:3000/api/callback` registered under **Dashboard > Authentication > Redirects > Allowed callback URLs**, and `http://localhost:3000/login` registered as a **Post logout URL**. Install the SDK: Terminal ```bash pnpm add @scalekit-sdk/node ``` Add your credentials to `.env.local`: .env.local ```bash SCALEKIT_ENVIRONMENT_URL="https://your-subdomain.scalekit.com" SCALEKIT_CLIENT_ID="skc_..." SCALEKIT_CLIENT_SECRET="..." # Never expose this to the browser. Server-only. SESSION_COOKIE_SECRET="a-32-byte-random-string-for-cookie-encryption" ``` ## Create the Scalekit client [Section titled “Create the Scalekit client”](#create-the-scalekit-client) Instantiate the client once and reuse it. Reading credentials from the environment keeps the secret out of your bundle, and a module-level singleton avoids reconnecting on every request. lib/scalekit.ts ```ts import { Scalekit } from '@scalekit-sdk/node'; // Security: credentials come from server-only env vars. The client secret must // never reach the browser, so this module is only ever imported in server code. export const scalekit = new Scalekit( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); export const REDIRECT_URI = 'http://localhost:3000/api/callback'; ``` ## Start the login flow [Section titled “Start the login flow”](#start-the-login-flow) Generate a `state` value, store it in a short-lived cookie to defend against CSRF, and redirect the browser to Scalekit’s hosted login page. Include `offline_access` in the scopes so Scalekit returns a refresh token. app/login/route.ts ```ts 1 import { randomBytes } from 'node:crypto'; 2 import { cookies } from 'next/headers'; 3 import { redirect } from 'next/navigation'; 4 import { scalekit, REDIRECT_URI } from '@/lib/scalekit'; 5 6 export async function GET() { 7 // Security: a random state ties the callback back to this browser. Without it, 8 // an attacker could replay a callback and complete login as someone else (CSRF). 9 const state = randomBytes(32).toString('hex'); 10 11 const cookieStore = await cookies(); 12 cookieStore.set('sk_oauth_state', state, { 13 httpOnly: true, // Block JavaScript access to mitigate XSS token theft. 14 secure: process.env.NODE_ENV === 'production', // HTTPS-only outside local dev. 15 sameSite: 'lax', 16 maxAge: 60 * 10, // The state is only needed for the next 10 minutes. 17 path: '/', 18 }); 19 20 const authorizationUrl = scalekit.getAuthorizationUrl(REDIRECT_URI, { 21 scopes: ['openid', 'profile', 'email', 'offline_access'], 22 state, 23 }); 24 25 redirect(authorizationUrl); 26 } ``` ## Handle the callback [Section titled “Handle the callback”](#handle-the-callback) After the user authenticates, Scalekit redirects back with a `code` and your `state`. Validate the `state`, exchange the code for tokens with `authenticateWithCode`, then store the tokens in `HttpOnly` cookies. app/api/callback/route.ts ```ts 1 import { cookies } from 'next/headers'; 2 import { NextRequest, NextResponse } from 'next/server'; 3 import { scalekit, REDIRECT_URI } from '@/lib/scalekit'; 4 import { setSessionCookies } from '@/lib/session'; 5 6 export async function GET(request: NextRequest) { 7 const { searchParams } = request.nextUrl; 8 const code = searchParams.get('code'); 9 const state = searchParams.get('state'); 10 const error = searchParams.get('error'); 11 12 const cookieStore = await cookies(); 13 const storedState = cookieStore.get('sk_oauth_state')?.value; 14 cookieStore.delete('sk_oauth_state'); // Use the state only once. 15 16 if (error) { 17 return NextResponse.redirect(new URL('/login?error=auth_failed', request.url)); 18 } 19 20 // Security: reject the callback unless the returned state matches the one we 21 // issued. A mismatch means the response did not originate from our redirect. 22 if (!code || !state || state !== storedState) { 23 return NextResponse.redirect(new URL('/login?error=invalid_state', request.url)); 24 } 25 26 try { 27 const { user, idToken, accessToken, refreshToken } = 28 await scalekit.authenticateWithCode(code, REDIRECT_URI); 29 30 const response = NextResponse.redirect(new URL('/dashboard', request.url)); 31 setSessionCookies(response, { idToken, accessToken, refreshToken }); 32 return response; 33 } catch { 34 return NextResponse.redirect(new URL('/login?error=exchange_failed', request.url)); 35 } 36 } ``` A refresh token requires offline_access Scalekit only returns `refreshToken` when the authorization request includes the `offline_access` scope. If you omit it, sessions end as soon as the access token expires because there is nothing to refresh. ## Store and read the session [Section titled “Store and read the session”](#store-and-read-the-session) Centralize cookie handling so login, refresh, and logout stay consistent. Keep tokens in `HttpOnly`, `Secure` cookies, and scope the refresh token to a narrow path so it is sent only when you need it. lib/session.ts ```ts 1 import { cookies } from 'next/headers'; 2 import type { NextResponse } from 'next/server'; 3 import { scalekit } from '@/lib/scalekit'; 4 5 type Tokens = { idToken: string; accessToken: string; refreshToken: string }; 6 7 const COOKIE_BASE = { 8 httpOnly: true, // Tokens are never readable from client-side JavaScript (XSS defense). 9 secure: process.env.NODE_ENV === 'production', 10 sameSite: 'lax' as const, 11 }; 12 13 export function setSessionCookies(response: NextResponse, tokens: Tokens) { 14 response.cookies.set('sk_id_token', tokens.idToken, { ...COOKIE_BASE, path: '/' }); 15 rotateTokens(response, tokens); 16 } 17 18 // Refresh returns a new access and refresh token but not a new ID token, so this 19 // updates only those two cookies and leaves the existing ID token in place. 20 export function rotateTokens( 21 response: NextResponse, 22 tokens: { accessToken: string; refreshToken: string }, 23 ) { 24 response.cookies.set('sk_access_token', tokens.accessToken, { ...COOKIE_BASE, path: '/' }); 25 // Security: scope the refresh token to the refresh endpoint only, so it is not 26 // attached to every request. This shrinks the window for token exfiltration. 27 response.cookies.set('sk_refresh_token', tokens.refreshToken, { 28 ...COOKIE_BASE, 29 path: '/api/refresh', 30 }); 31 } 32 33 /** 34 * Returns the authenticated user, or null. Call this from Server Components and 35 * Route Handlers (Node.js runtime) — never from Edge middleware, because the 36 * Scalekit SDK needs Node APIs that the Edge runtime does not provide. 37 */ 38 export async function getSession() { 39 const cookieStore = await cookies(); 40 const accessToken = cookieStore.get('sk_access_token')?.value; 41 if (!accessToken) return null; 42 43 const isValid = await scalekit.validateAccessToken(accessToken); 44 if (!isValid) return null; 45 46 // validateToken returns the decoded claims once the signature and expiry pass. 47 const claims = await scalekit.validateToken(accessToken); 48 return { sub: claims.sub, email: claims.email }; 49 } ``` ## Refresh tokens transparently [Section titled “Refresh tokens transparently”](#refresh-tokens-transparently) When the access token expires, redeem the refresh token for a new pair. Because Scalekit rotates refresh tokens, write the new refresh token back immediately and discard the old one. app/api/refresh/route.ts ```ts 1 import { cookies } from 'next/headers'; 2 import { NextResponse } from 'next/server'; 3 import { scalekit } from '@/lib/scalekit'; 4 import { rotateTokens } from '@/lib/session'; 5 6 export async function POST() { 7 const cookieStore = await cookies(); 8 const refreshToken = cookieStore.get('sk_refresh_token')?.value; 9 if (!refreshToken) { 10 return NextResponse.json({ error: 'no_session' }, { status: 401 }); 11 } 12 13 try { 14 const tokens = await scalekit.refreshAccessToken(refreshToken); 15 const response = NextResponse.json({ ok: true }); 16 // Security: persist the rotated refresh token. Replaying the old one fails, 17 // which is how Scalekit detects a stolen, reused token. 18 rotateTokens(response, tokens); 19 return response; 20 } catch { 21 return NextResponse.json({ error: 'refresh_failed' }, { status: 401 }); 22 } 23 } ``` ## Protect routes [Section titled “Protect routes”](#protect-routes) Read the session in a Server Component and redirect unauthenticated visitors. This runs on the Node.js runtime, so the SDK validation works. app/dashboard/page.tsx ```tsx import { redirect } from 'next/navigation'; import { getSession } from '@/lib/session'; export default async function DashboardPage() { const session = await getSession(); if (!session) redirect('/login'); return

Welcome, {session.email}

; } ``` For a coarse, fast gate across many routes, add middleware that only checks whether a session cookie exists. Keep real validation in the page or Route Handler. middleware.ts ```ts import { NextRequest, NextResponse } from 'next/server'; export function middleware(request: NextRequest) { // Presence check only — middleware runs on the Edge runtime and cannot call the // Scalekit SDK. getSession() does the cryptographic validation downstream. const hasSession = request.cookies.has('sk_access_token'); if (!hasSession) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); } export const config = { matcher: ['/dashboard/:path*'] }; ``` ## Sign out [Section titled “Sign out”](#sign-out) Build the Scalekit logout URL, clear your cookies, and redirect the browser to Scalekit so the server-side session ends too. Pass the ID token as `idTokenHint` before you clear it. app/logout/route.ts ```ts 1 import { cookies } from 'next/headers'; 2 import { NextResponse } from 'next/server'; 3 import { scalekit } from '@/lib/scalekit'; 4 5 export async function GET() { 6 const cookieStore = await cookies(); 7 const idToken = cookieStore.get('sk_id_token')?.value; 8 9 const logoutUrl = scalekit.getLogoutUrl({ 10 idTokenHint: idToken, 11 postLogoutRedirectUri: 'http://localhost:3000/login', 12 }); 13 14 const response = NextResponse.redirect(logoutUrl); 15 // Clear local cookies after building the logout URL, so the ID token is still 16 // available to tell Scalekit which session to end. 17 response.cookies.delete('sk_access_token'); 18 response.cookies.delete('sk_id_token'); 19 response.cookies.delete('sk_refresh_token'); 20 return response; 21 } ``` Logout must be a browser redirect Redirect the browser to the logout URL rather than calling it from server code. The redirect carries Scalekit’s session cookie, which lets Scalekit identify and end the correct session. ## Verify it works [Section titled “Verify it works”](#verify-it-works) 1. Start the app with `pnpm dev` and open `http://localhost:3000/dashboard`. The middleware redirects you to `/login`. 2. Visit `http://localhost:3000/login`. The browser lands on Scalekit’s hosted login page showing every method you enabled in the dashboard. 3. Sign in. Scalekit returns to `/api/callback`, which sets the session cookies and forwards you to `/dashboard`, where your email renders. 4. Inspect cookies in your browser devtools. Confirm `sk_access_token`, `sk_id_token`, and `sk_refresh_token` are present and marked `HttpOnly`. 5. Open `http://localhost:3000/logout`. Your cookies clear, Scalekit ends the session, and you return to `/login`. ## Production notes [Section titled “Production notes”](#production-notes) * **Encrypt cookie values.** This recipe stores raw tokens for clarity. In production, encrypt them with `SESSION_COOKIE_SECRET` (for example with [`jose`](https://github.com/panva/jose)) before writing, and decrypt on read. * **Drive refresh from the client.** Call `POST /api/refresh` from a client effect shortly before the access token expires, or retry once on a `401`, so sessions renew without a full re-login. * **Use absolute redirect URLs per environment.** Replace the hard-coded `localhost` URLs with an environment variable, and register each environment’s callback and post-logout URLs in the dashboard. When you are ready to ship, walk the [production readiness checklist](/authenticate/launch-checklist/). To inspect what the access token carries, see [ID token claims](/guides/idtoken-claims/). --- # DOCUMENT BOUNDARY --- # Apify Actor with per-user OAuth via Scalekit > Build an Apify Actor that uses Scalekit AgentKit so each user connects their OAuth accounts, keyed by Apify userId. An Apify Actor is a stateless serverless container. Every run starts cold — no session, no cookies, no “current user.” When you want each person who runs your Actor to access their own Notion workspace, their own Gmail, or their own GitHub account, you need to map Apify’s identity model onto an OAuth token store that persists across runs. Scalekit solves this with a connected-accounts model: for each `(connector, identifier)` pair, it stores one OAuth session and refreshes it automatically. This recipe builds an Actor that connects to Notion per-user and to YouTube via a shared account, using Apify’s native `userId` as the per-user identifier. It also shows how to surface the OAuth consent step as a live interactive page inside the Actor run, instead of a raw link buried in JSON output. **What this recipe covers:** * **Per-user identity without input fields** — derive the connected-account identifier from `Actor.getEnv().userId` so users never type an email or ID * **Shared vs per-user connectors** — hardcode a single identifier for connectors shared across all users; derive one per user for private accounts * **Interactive auth UX** — serve a branded OAuth consent page on Apify’s live-view port so users click a button rather than hunting for a raw URL * **Input schema design** — expose only the `task` field to end users; keep all auth and config internal The complete source is available in the [notion-youtube-agent](https://github.com/scalekit-developers/agentkit-apify-actor-example) repository. ## Before you start [Section titled “Before you start”](#before-you-start) You need working OAuth credentials for each third-party API your Actor will connect to. Scalekit manages the token lifecycle, but the underlying API must be enabled and the OAuth client must exist first. **For YouTube (or any Google API):** 1. Open the [Google Cloud Console](https://console.cloud.google.com/) and select your project. 2. Go to **APIs & Services → Enabled APIs & services** and enable **YouTube Data API v3**. Without this, every tool call returns `permission_denied` even if the OAuth token is valid. 3. Go to **APIs & Services → OAuth consent screen** and add the Google accounts that will authorize the Actor under **Test users**. While the app is in “Testing” publishing status, only accounts listed here can complete the OAuth flow — all others see `Error 403: org_internal`. 4. Create an OAuth 2.0 client (**APIs & Services → Credentials → + Create credentials → OAuth client ID**). Set the application type to **Web application**. Leave the redirect URI blank for now — you’ll add it from Scalekit in the next section. **For Notion:** 1. Go to [notion.so/my-integrations](https://www.notion.so/my-integrations) and create a new integration, or use Notion’s OAuth setup if your Actor uses Scalekit’s Notion OAuth connector. **For both connectors:** * A [Scalekit](https://app.scalekit.com) environment with API credentials (`SCALEKIT_ENVIRONMENT_URL`, `SCALEKIT_CLIENT_ID`, `SCALEKIT_CLIENT_SECRET`). * [Apify CLI](https://docs.apify.com/cli) installed: `npm install -g @apify/cli` * Node.js 18+ ### 1. Set up connections in Scalekit [Section titled “1. Set up connections in Scalekit”](#1-set-up-connections-in-scalekit) In the [Scalekit Dashboard](https://app.scalekit.com), go to **AgentKit → Connections** and create two connections: **YouTube connection (shared)** 1. Search for **YouTube** and click **Create**. 2. Copy the **Redirect URI** from the connection panel (it looks like `https:///sso/v1/oauth//callback`). 3. Paste it into your Google Cloud OAuth client under **Authorized redirect URIs** and save. 4. Back in Scalekit, enter the **Client ID** and **Client Secret** from your Google Cloud OAuth client. 5. Under **Scopes**, select at least `youtube.readonly`. Add `youtube` if your Actor needs write access (playlists, subscriptions). Add `yt-analytics.readonly` if you query analytics data. 6. Click **Save**. Note the **Connection name** (e.g., `youtube`) — your code must match it exactly. **Notion connection (per-user)** 1. Search for **Notion** and click **Create**. 2. Enter the **Client ID** and **Client Secret** from your Notion integration or OAuth app. 3. Scalekit pre-configures the redirect URI and scopes for Notion. Click **Save**. 4. Note the **Connection name** (e.g., `notion`). Scopes are locked at authorization time Scopes are locked in at authorization time. If you add scopes to a connection after a user has already authorized, their existing token does not gain the new scopes. Delete the connected account in Scalekit and have the user re-authorize to pick up the updated scopes. ### 2. Create the Apify Actor project [Section titled “2. Create the Apify Actor project”](#2-create-the-apify-actor-project) ```bash apify create notion-youtube-agent -t project_empty cd notion-youtube-agent npm install @scalekit-sdk/node openai apify ``` Set your Scalekit credentials as Actor environment variables in the Apify Console under **Settings → Environment variables**: ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=your-secret ``` If your Actor creates new Notion pages (not just writing to existing ones), also set a default parent location. Without this, the Actor can only write to pages that already exist by exact title match. ```bash NOTION_DEFAULT_PARENT_PAGE_ID=1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d ``` To find a Notion page ID: open the page in Notion, click **Share → Copy link**. The 32-character hex string at the end of the URL is the page ID. ### 3. Derive user identity from Apify’s runtime [Section titled “3. Derive user identity from Apify’s runtime”](#3-derive-user-identity-from-apifys-runtime) Apify exposes the identity of the account running the Actor through `Actor.getEnv()`. Use `userId` directly as the Scalekit connected-account identifier — no email field, no manual input. src/main.js ```js import { Actor } from 'apify'; await Actor.init(); const { userId } = Actor.getEnv(); // userId is stable per Apify account — the same user always gets the same token. const notionIdentifier = userId; ``` `userId` is a stable opaque string that Apify sets for the account running the Actor. It persists across runs, so the first run that completes OAuth will find an active token on every subsequent run. Local development: userId is undefined `Actor.getEnv().userId` is `undefined` when you run the Actor locally with `apify run`. Use a hardcoded fallback for local development: ```js const { userId } = Actor.getEnv(); const notionIdentifier = userId ?? 'local-dev-user'; ``` ### 4. Choose shared vs per-user identifiers [Section titled “4. Choose shared vs per-user identifiers”](#4-choose-shared-vs-per-user-identifiers) Not every connector needs per-user isolation. A YouTube data connection used for research can be shared across all Actor runs with a hardcoded identifier. Only connectors that access private user data need per-user identifiers. src/main.js ```js // Per-user: each Apify account connects their own Notion workspace. const notionIdentifier = userId; // Shared: one YouTube OAuth session used by all runs. const youtubeIdentifier = 'shared-youtube'; ``` Hardcode the shared identifier in code — do not expose it as an input field. End users should not need to know it exists. ### 5. Ensure each connector is authorized [Section titled “5. Ensure each connector is authorized”](#5-ensure-each-connector-is-authorized) Before calling any API, check whether the connected account is active. If not, generate an authorization link and wait for the user to complete the OAuth flow. src/notionAuth.js ```js 1 import { Actor } from 'apify'; 2 import { ConnectorStatus } from '@scalekit-sdk/node'; 3 4 export async function ensureNotionConnected(scalekitActions, identifier, { 5 pollIntervalMs = 5_000, 6 timeoutMs = 300_000, 7 onMagicLink = async () => {}, 8 } = {}) { 9 const resp = await scalekitActions.getOrCreateConnectedAccount({ 10 connectionName: 'notion', 11 identifier, 12 }); 13 const account = resp.connectedAccount ?? resp; 14 15 if (account.status === ConnectorStatus.ACTIVE) { 16 return account.id; 17 } 18 19 const { link } = await scalekitActions.getAuthorizationLink({ 20 connectionName: 'notion', 21 identifier, 22 }); 23 24 const markDone = await onMagicLink(link); 25 26 const deadline = Date.now() + timeoutMs; 27 28 while (Date.now() < deadline) { 29 await sleep(pollIntervalMs); 30 31 const pollResp = await scalekitActions.getOrCreateConnectedAccount({ 32 connectionName: 'notion', 33 identifier, 34 }); 35 const polled = pollResp.connectedAccount ?? pollResp; 36 37 if (polled.status === ConnectorStatus.ACTIVE) { 38 markDone?.(); 39 await Actor.setStatusMessage('Notion authorized — proceeding.'); 40 return polled.id; 41 } 42 } 43 44 throw new Error(`Timed out waiting for Notion authorization.`); 45 } 46 47 function sleep(ms) { 48 return new Promise(resolve => setTimeout(resolve, ms)); 49 } ``` The same pattern applies to every connector. Copy the function, change `connectionName`, and pass in the appropriate identifier. ### 6. Surface auth as a live interactive page [Section titled “6. Surface auth as a live interactive page”](#6-surface-auth-as-a-live-interactive-page) Printing a raw authorization link to the console or burying it in JSON output creates a poor experience. Apify Actors can start an HTTP server on `ACTOR_WEB_SERVER_PORT` (default `4321`), and Apify automatically exposes it as a public URL while the run is active. Use this to serve a branded OAuth consent page. src/authServer.js ```js 1 import http from 'http'; 2 import { Actor } from 'apify'; 3 4 const PORT = parseInt(process.env.ACTOR_WEB_SERVER_PORT ?? '4321', 10); 5 6 let server = null; 7 8 export function getLiveViewUrl() { 9 const { actorId, actorRunId } = Actor.getEnv(); 10 return `https://${actorId}--${actorRunId}-${PORT}.runs.apify.net`; 11 } 12 13 export async function serveAuthPage(link, serviceName) { 14 let html = buildAuthPage(link, serviceName); 15 16 if (server) server.close(); 17 server = http.createServer((_req, res) => { 18 res.writeHead(200, { 'Content-Type': 'text/html' }); 19 res.end(html); 20 }); 21 server.listen(PORT); 22 23 return { 24 liveViewUrl: getLiveViewUrl(), 25 markDone: () => { html = buildDonePage(serviceName); }, 26 }; 27 } 28 29 function buildAuthPage(link, serviceName) { 30 return ` 31 32 33 34 Authorize ${serviceName} 35 43 44 45
46

🔐 Connect ${serviceName}

47

Click below to authorize access to your ${serviceName} account. 48 The actor will continue automatically once you complete authorization.

49 Authorize ${serviceName} → 50
51 52 `; 53 } 54 55 function buildDonePage(serviceName) { 56 return ` 57 58 ${serviceName} Authorized 59 60

✅ ${serviceName} Authorized

61

Returning to task — you can close this tab.

62 63 `; 64 } ``` The live view URL follows this pattern: ```text https://{actorId}--{actorRunId}-{PORT}.runs.apify.net ``` Both `actorId` and `actorRunId` come from `Actor.getEnv()` — the same call that gives you `userId`. ### 7. Wire auth into the Actor entry point [Section titled “7. Wire auth into the Actor entry point”](#7-wire-auth-into-the-actor-entry-point) Pass the live view callback into `ensureNotionConnected`. The callback starts the HTTP server, stores the `markDone` function, and returns it so the polling loop can update the page when auth completes. src/main.js ```js 1 import { Actor } from 'apify'; 2 import { ScalekitClient } from '@scalekit-sdk/node'; 3 import { ensureNotionConnected } from './notionAuth.js'; 4 import { serveAuthPage } from './authServer.js'; 5 6 await Actor.init(); 7 8 const input = await Actor.getInput(); 9 const { task } = input; 10 11 const { userId } = Actor.getEnv(); 12 const notionIdentifier = userId; 13 const youtubeIdentifier = 'shared-youtube'; 14 15 const scalekit = new ScalekitClient( 16 process.env.SCALEKIT_ENVIRONMENT_URL, 17 process.env.SCALEKIT_CLIENT_ID, 18 process.env.SCALEKIT_CLIENT_SECRET, 19 ); 20 21 await ensureNotionConnected(scalekit.actions, notionIdentifier, { 22 onMagicLink: async (link) => { 23 const { liveViewUrl, markDone } = await serveAuthPage(link, 'Notion'); 24 25 // Store the live view URL in OUTPUT so the Apify UI shows a clickable link. 26 await Actor.setValue('OUTPUT', { 27 status: 'AWAITING_NOTION_AUTH', 28 authPageUrl: liveViewUrl, 29 message: 'Open authPageUrl in your browser to authorize Notion.', 30 }); 31 32 await Actor.setStatusMessage(`ACTION REQUIRED: Authorize Notion → ${liveViewUrl}`); 33 34 return markDone; 35 }, 36 }); 37 38 // ... run the agent, push results ``` End-user experience after this change: 1. User starts the Actor run and types their task 2. **Output** panel immediately shows a clickable `authPageUrl` 3. User opens the URL and sees a branded “Authorize Notion →” button 4. After completing OAuth, the page updates to ”✅ Notion Authorized” 5. The Actor continues automatically — no re-run needed Apify web view does not auto-refresh The Actor’s live web view in the Apify Console does not refresh automatically. After completing the OAuth flow, the page may still show the “Authorize” button. Click the **auto-refresh** toggle in the web view toolbar, or open the URL in a new tab to see the updated state. The Actor itself continues regardless — it polls the account status server-side and proceeds as soon as authorization completes. ### 8. Design the input schema for end users [Section titled “8. Design the input schema for end users”](#8-design-the-input-schema-for-end-users) The Actor’s input form should show only what the end user actually needs to provide. All auth identifiers, LLM config, and internal settings stay out of the form. .actor/input_schema.json ```json { "title": "Notion + YouTube AI Agent", "type": "object", "schemaVersion": 1, "properties": { "task": { "title": "Task", "type": "string", "description": "Natural language task for the agent. Examples: 'List the 5 most recently edited pages in my Notion workspace' or 'Search YouTube for React tutorial channels and append the top 10 to my Research page'.", "editor": "textarea" } }, "required": ["task"] } ``` By default the Actor uses [Apify’s OpenRouter proxy](https://apify.com/apify/openrouter) for LLM inference, authenticated via `APIFY_TOKEN` (which Apify sets automatically). No external API key is needed — LLM costs are billed to the user’s Apify credits. If your Actor needs to support a custom LLM endpoint, add an optional `llmApiKey` field and detect the endpoint at runtime. Everything else — `notionIdentifier`, `youtubeIdentifier`, timeouts, model name, base URL — is either derived at runtime (`userId`) or hardcoded and deployed as an Actor environment variable. Apify input schema does not support placeholder The Apify input schema spec does not allow a `placeholder` property on fields. Put example values in `description` instead — they appear as helper text below the field label in the Console. ### 9. Testing [Section titled “9. Testing”](#9-testing) Run locally: ```bash apify run ``` Provide input in `storage/key_value_stores/default/INPUT.json`: ```json { "task": "List the 5 most recently edited pages in my Notion workspace" } ``` Because `Actor.getEnv().userId` is `undefined` locally, the `notionIdentifier` falls back to your local development value. After you confirm the flow works, deploy to Apify: ```bash apify push ``` On the first cloud run, the Actor outputs an `authPageUrl`. Open it, click **Authorize Notion**, and complete the OAuth flow. The Actor polls and continues automatically. On every subsequent run for the same Apify account, the token is already active and the auth step is skipped entirely. ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) Tool calls fail with `permission_denied` even though the account is ACTIVE * **Symptom**: `[permission_denied] tool execution failed - forbidden access` in logs. The connected account status is ACTIVE and authorization completed successfully. * **Cause**: The underlying API is not enabled in the cloud provider console. An ACTIVE connected account means the OAuth token exists — it does not mean the API accepts calls. For YouTube, this happens when **YouTube Data API v3** is not enabled in the Google Cloud project. * **Fix**: Go to [Google Cloud Console → APIs & Services → Enabled APIs](https://console.cloud.google.com/apis/dashboard) and enable **YouTube Data API v3**. No code change or re-authorization needed — existing tokens work once the API is enabled. Authorization fails with `Error 403: org_internal` * **Symptom**: Google shows “Access blocked: \[App name] can only be used within its organization” when a user tries to authorize. * **Cause**: The Google account attempting to authorize is not listed as a test user. While the OAuth app is in “Testing” publishing status, only accounts explicitly added as test users can complete the OAuth flow. * **Fix**: Go to [Google Cloud Console → APIs & Services → OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) and add the Google account under **Test users**. No need to change the app’s publishing status or user type — just add the account and retry. Tool calls fail with `permission_denied` after adding scopes * **Symptom**: Same `permission_denied` error. The connection has the right scopes configured, but you added them after the user already authorized. * **Cause**: OAuth tokens carry the scopes that were configured at authorization time. Adding scopes to a connection does not retroactively update existing tokens. * **Fix**: Delete the connected account in the Scalekit dashboard (or via API) and have the user re-authorize. The new token will include the updated scopes. Notion page creation fails with “no parent_page_id/database_id provided” * **Symptom**: The agent finds no page with the requested title and throws `Notion page "X" was not found and cannot be created because no parent_page_id/database_id was provided`. * **Cause**: Notion’s API requires a parent location for every new page. The Actor checks for a default parent in the input, then in environment variables, and throws if neither is set. * **Fix**: Set `NOTION_DEFAULT_PARENT_PAGE_ID` or `NOTION_DEFAULT_DATABASE_ID` as an Actor environment variable. To find a page ID, open the page in Notion, click **Share → Copy link**, and extract the 32-character hex string from the URL. Using email as the identifier * **Symptom**: Input form asks for user email, or the identifier is passed in as an input field * **Cause**: Treating the connected-account identifier as a user-facing concept * **Fix**: Use `Actor.getEnv().userId` as the identifier. It is stable, unique per Apify account, and requires no input from the user. Scalekit does not require an email — it accepts any unique string as an identifier. `placeholder` property causes build failure * **Symptom**: `apify push` fails with `Property schema.properties.task.placeholder is not allowed.` * **Cause**: `placeholder` is not part of the Apify input schema specification * **Fix**: Move example text into the `description` field. It appears as helper text in the Apify Console input form. `userId` is `undefined` locally * **Symptom**: Actor crashes with `Could not determine Apify user ID` during `apify run` * **Cause**: `Actor.getEnv()` does not populate `userId` in local runs * **Fix**: Fall back to a local dev value: `const notionIdentifier = userId ?? 'local-dev-user'` Stale variable name causes `ReferenceError` at runtime * **Symptom**: Actor fails with `notionUserEmail is not defined` even though you removed that field * **Cause**: The variable was renamed in some places but left in others — console logs, OUTPUT payloads, or `runAgent` arguments * **Fix**: Search the entire codebase for the old variable name before deploying. One missed reference fails at runtime, not at build time. Live view URL not available in local runs * **Symptom**: `getLiveViewUrl()` returns a broken URL during `apify run` * **Cause**: `actorId` and `actorRunId` are also `undefined` locally * **Fix**: Guard the live view server behind a check: `if (actorId && actorRunId) { ... }`. Fall back to logging the raw authorization link to the console for local development. ## Production notes [Section titled “Production notes”](#production-notes) **Token persistence across runs** — Scalekit stores the OAuth token server-side keyed by `(connectionName, identifier)`. As long as `userId` is stable (it is), the user only completes the OAuth flow once. Subsequent runs call `getOrCreateConnectedAccount` and get an active account back immediately. **Token refresh** — Scalekit refreshes expired tokens automatically before returning them. You do not need to track expiry or call a refresh endpoint. **Re-authorization** — If a user revokes access in Notion’s settings, `getOrCreateConnectedAccount` returns a non-active account. The Actor generates a new magic link automatically. No code change required — the polling loop handles it the same way as a first-time auth. **Shared connectors** — The `shared-youtube` identifier works because YouTube access is the same for all users (e.g., read-only public data). Any connector where all users share the same OAuth session can use a hardcoded identifier. Private data connectors — Notion, Gmail, GitHub — should always use a per-user identifier. **Input schema changes require a redeploy** — The Apify Console reads the input schema from the deployed build. Changes to `.actor/input_schema.json` only take effect after `apify push`. ## Next steps [Section titled “Next steps”](#next-steps) * **Add more per-user connectors** — The same `ensureConnected` + `onMagicLink` pattern works for any Scalekit connector. Add a `src/githubAuth.js` following the same structure as `notionAuth.js`. * **Use built-in actions** — For connectors with Scalekit built-in tools, replace manual API calls with `scalekit.actions.executeTool`. See [all supported connectors](/agentkit/connectors/). * **Extend the input schema** — Add optional fields like `maxIterations` or `llmModel` with defaults, so power users can tune the Actor without the defaults getting in the way for casual users. * **Review the AgentKit quickstart** — For a broader overview of the connected-accounts model, see the [AgentKit quickstart](/agentkit/quickstart/). --- # DOCUMENT BOUNDARY --- # Build a Vapi voice assistant with Scalekit > Use Vapi + Scalekit Virtual MCP for voice assistants to securely access any tool from large catalogs. Voice interfaces shine for hands-free, conversational access to data and actions. But giving a voice agent access to real tools (Gmail, Calendar, Slack, GitHub, Drive, CRM, Notion, and [many other connectors](/agentkit/connectors/)) introduces two hard problems: per-user authentication without leaking tokens to the LLM, and managing a large tool surface without overwhelming context windows or exposing everything to every user. Scalekit lets you: * Register built-in tools from connections and custom tools directly in the UI or code (see “Registering tools in the Scalekit UI” below). * Associate them with specific users via connections. * Use **Virtual MCP** to bundle hundreds or thousands of those registered tools into a single, scoped, per-user MCP endpoint that Vapi can discover dynamically. This cookbook walks through building a [Vapi](https://vapi.ai) voice assistant that can safely discover and use any tool (from a catalog of hundreds or thousands) using Scalekit’s tool registration + Virtual MCP. You can also call specific registered tools directly via Vapi Function tools if you don’t need dynamic discovery of many. The complete source is available in the [vapi-scalekit-voice-demo](https://github.com/scalekit-developers/vapi-scalekit-voice-demo) repository. ## What you are building [Section titled “What you are building”](#what-you-are-building) * Register and manage individual tools (built-in from connections or custom) directly in the Scalekit UI, and associate them with users/connections. * A **Vapi voice assistant** that understands natural language requests (e.g. “List my meetings this week”, “Find emails from Acme about the renewal”, “Summarize the last thread in #eng-updates on Slack”, “Show my open GitHub PRs”, “Find the latest proposal in Drive and email it to the team”). * **Dynamic tool discovery** via a Scalekit Virtual MCP — the assistant only sees the tools you explicitly scoped for that role (selected from the ones you registered). Vapi discovers these at call start via the MCP protocol. * **Per-user OAuth without custom code** — Scalekit handles authorization links, token storage, and refresh. Your app passes the user’s identifier. * **Access to any tool** — scoping + per-user tokens keep context small and access least-privilege even when the full catalog is huge. * A pattern you can extend to other voice platforms or agent frameworks by swapping the client. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Scalekit account with AgentKit enabled ([create one](https://app.scalekit.com)). * At least one connection configured (e.g. Google Calendar, Gmail) under **AgentKit → Connections**. See [Configure a connection](/agentkit/connections/). * A Vapi account and at least one assistant. * Node.js 18+ and the demo dependencies. * ngrok (or equivalent) for local webhook exposure during development. * Familiarity with basic voice agent concepts and OAuth flows. ## Environment variables [Section titled “Environment variables”](#environment-variables) The demo is configured via `.env.local` (copy it from `.env.example`). ```bash cp .env.example .env.local ``` ### Variables present in the demo [Section titled “Variables present in the demo”](#variables-present-in-the-demo) | Variable | Required | Client (browser) | Purpose | | ----------------------------------------- | --------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | | `NEXT_PUBLIC_VAPI_PUBLIC_KEY` | Yes | Yes | Initializes the Vapi Web SDK in the browser | | `NEXT_PUBLIC_VAPI_ASSISTANT_ID` | Yes | Yes | ID of the assistant to start voice calls against | | `VAPI_PRIVATE_KEY` | For API updates | No | Used server-side only to call Vapi APIs (create/update tools programmatically) | | `SCALEKIT_ENVIRONMENT_URL` | Yes | No | Base URL of your Scalekit environment | | `SCALEKIT_CLIENT_ID` | Yes | No | Scalekit API credential (public part) | | `SCALEKIT_CLIENT_SECRET` | Yes | No | Scalekit secret. Never ship to browser or commit | | `TEST_IDENTIFIER` | Yes | No (server) | Your app’s opaque ID (not an email) for the test user whose connections the demo uses, such as `demo_user_1` | | `NEXT_PUBLIC_TEST_SCALEKIT_CONNECTION_ID` | Demo only | Yes | The identifier the demo browser sends in Vapi call `metadata`. Anyone can change it, so don’t carry this pattern into production (see below) | | `SCALEKIT_MCP_CONFIG_ID` | For MCP | No | `cfg_...` ID of your Virtual MCP server (created in Scalekit dashboard) | | `NEXT_PUBLIC_SCALEKIT_MCP_SERVER_URL` | For MCP | Yes (url only) | The static base MCP server URL. Token is added at runtime via the `Authorization` header | **MCP-specific vars** (`SCALEKIT_MCP_CONFIG_ID` + `NEXT_PUBLIC_SCALEKIT_MCP_SERVER_URL`) are only needed when using the dynamic discovery MCP tool path (recommended when you want to expose many tools). They are not required for pure Function tool + `executeTool` mode. ### Running the token helper (demo) [Section titled “Running the token helper (demo)”](#running-the-token-helper-demo) After filling the MCP variables, generate a fresh token for Vapi: ```bash python scripts/generate-mcp-token.py ``` (or `node --env-file=.env.local scripts/generate-mcp-token.js`) The script outputs the exact `server` object (url + Authorization header) you paste into the Vapi MCP tool form. ### What changes when you go live (production) [Section titled “What changes when you go live (production)”](#what-changes-when-you-go-live-production) In a real application you do **not** rely on a static `.env.local` for user identity or tokens: * **Identifier comes from runtime context** — After a real user authenticates in your app and authorizes their connections (Gmail etc.), you obtain their `identifier` from your session / database. Never use a hardcoded `TEST_IDENTIFIER`. * **Tokens are minted on the fly and short-lived** — When the user starts a voice call, your backend calls Scalekit to create a fresh session token for that specific identifier + config (see `app/api/scalekit/mcp-session/route.ts` and the Python script). Tokens typically live for minutes to an hour. * **MCP server configuration is injected dynamically** — * Demo: you manually copy-paste `server.url` and the `Authorization: Bearer ` header into the Vapi dashboard UI. * Production: your backend uses the **Vapi API** (authenticated with `VAPI_PRIVATE_KEY`) to create or patch the MCP tool on the assistant with the fresh per-call `server` object (containing the just-minted token). You can do this immediately before calling `vapi.start(...)` or via Vapi’s assistant update endpoints. The token never sits in your dashboard. * **Secrets stay server-side** — `SCALEKIT_CLIENT_SECRET` and `VAPI_PRIVATE_KEY` remain in your production environment variables / secret manager. Only non-sensitive values (`NEXT_PUBLIC_*` public keys, config IDs, base URLs) may be exposed to the client when truly required. * **For Function tools, the webhook works out the user on the server.** Vapi call `metadata` set in the browser is under the caller’s control, so a webhook that runs `executeTool` for whatever identifier arrives there lets any caller act as any user. Instead, confirm the request came from Vapi (check the server secret you configured on the tool), then map the call to the signed-in user your backend recorded when it started the call, and use that user’s identifier. Never take the identifier itself from browser-supplied metadata. This design means the same Virtual MCP server works for every user—you only swap the short-lived token and the acting identifier at call time. ## Registering tools in the Scalekit UI [Section titled “Registering tools in the Scalekit UI”](#registering-tools-in-the-scalekit-ui) You register and associate tools centrally in the Scalekit dashboard (no code required for built-ins): * **Set up a connection** (AgentKit → Connections): Choose Gmail, Google Calendar, GitHub, etc. Scalekit registers its built-in tools automatically (e.g. `googlecalendar_list_events`). You can browse and search the full catalog under **AgentKit → Tools** or inside the connection page. See the [full list of connectors](/agentkit/connectors/). * **Authorize for users**: Go to **AgentKit → Connected Accounts** (or during first use), authorize using your app’s `identifier` (for example, `demo_user_1`). This associates the tools with that user. The connection must show **Active**. * **Custom tools**: Define additional tools in your code (see [Build custom tools](/agentkit/tools/custom-tools)) using `actions.request` to call any provider API. They can be associated with connections and included in scopes. Once registered and associated, these tools are available for: * Direct calls via `executeTool` (e.g. from a Vapi Function tool webhook). * Inclusion in a **Virtual MCP** config so a single MCP endpoint can surface hundreds or thousands safely. This is key when you have many tools: you register/maintain them in the UI or code, then use vMCP to selectively expose subsets to your voice assistant without token bloat or over-privileging. ## Architecture overview [Section titled “Architecture overview”](#architecture-overview) ```plaintext User (voice) │ ▼ Vapi assistant (MCP tool configured) │ (connects with per-user Bearer token) ▼ Scalekit Virtual MCP (scoped to role + user) │ (only exposes allowed tools) ▼ Scalekit AgentKit (token vault + execute) │ ▼ Real connectors (Gmail, Calendar, Slack, GitHub, ... — [many connectors](/agentkit/connectors/)) ``` Key differences from a naive “give the LLM every tool” approach: * Tool surface is defined once in the Virtual MCP server (least privilege). * Context size stays manageable (scoping reduces tokens). * Identity is enforced at the token level (no raw OAuth secrets reach the model or Vapi). ## Step 1: Create a scoped Virtual MCP server in Scalekit [Section titled “Step 1: Create a scoped Virtual MCP server in Scalekit”](#step-1-create-a-scoped-virtual-mcp-server-in-scalekit) 1. In the Scalekit dashboard go to **AgentKit** > **Virtual MCP Servers** and create a server. 2. Give it a name (e.g. “voice-personal-assistant”). 3. Add connection tool mappings for the connectors you want. See the [full list of connectors](/agentkit/connectors/). Example for calendar + email (expand later with Slack, GitHub, Drive, etc.): ```yaml connection_tool_mappings: - connection_name: googlecalendar tools: [googlecalendar_list_events, googlecalendar_create_event] - connection_name: gmail tools: [gmail_fetch_mails, gmail_send_message] # start small! ``` 4. Save. Copy the **config ID** (e.g. `cfg_...`) and the generated **mcp_server_url**. ![Creating a Virtual MCP in the Scalekit dashboard](/.netlify/images?url=_astro%2Fvmcp-scalekit.DzqPxAS8.png\&w=3012\&h=1090\&dpl=6ae471fc143c9a4c27550463) The screenshot above shows the Scalekit dashboard flow for creating the scoped Virtual MCP. This single config definition is reused for every user. You only change the token you mint at runtime. The scoping here is what lets you safely expose many tools without the LLM seeing everything. ## Step 2: Authorize connections for your test user [Section titled “Step 2: Authorize connections for your test user”](#step-2-authorize-connections-for-your-test-user) For the identifier you will use in the demo (for example, `demo_user_1`): 1. Go to **AgentKit → Connected Accounts**. 2. Authorize the connections you mapped above. 3. Confirm they show **Active**. If any are inactive, the token mint will fail with a clear error and an auth link. ## Step 3: Wire the Vapi assistant [Section titled “Step 3: Wire the Vapi assistant”](#step-3-wire-the-vapi-assistant) You have two main options in Vapi, depending on whether you want dynamic discovery of many scoped tools or direct calls to specific ones. ### Option A: MCP tool for dynamic discovery (recommended when you have many tools) [Section titled “Option A: MCP tool for dynamic discovery (recommended when you have many tools)”](#option-a-mcp-tool-for-dynamic-discovery-recommended-when-you-have-many-tools) 1. In Vapi, create or edit an assistant. 2. Create a new **MCP** tool (not Function). 3. Configure it with the Scalekit Virtual MCP details: * **Server URL**: the `mcp_server_url` from Step 1 * **HTTP Headers** (look for the **Headers**, **Add Header**, or **Custom Headers** / `server.headers` section in the tool form in the Vapi dashboard): * Key: `Authorization` * Value: `Bearer ` 4. Attach the MCP tool to the assistant. 5. Update the system prompt to tell the model when and how to use tools (example in the demo repo). ![Registering the MCP tool in the Vapi dashboard](/.netlify/images?url=_astro%2Ftool-registration-scalekit.DMPxf1_w.png\&w=2988\&h=1182\&dpl=6ae471fc143c9a4c27550463) The screenshot above illustrates where to configure the server URL and add the Authorization HTTP header in Vapi’s MCP tool form. ### Option B: Function tool for specific registered tools [Section titled “Option B: Function tool for specific registered tools”](#option-b-function-tool-for-specific-registered-tools) Create a **Function** tool in Vapi pointing to your webhook URL. The tool name must exactly match a tool you have registered or available in Scalekit (e.g. `googlecalendar_list_events`). Your webhook then calls `executeTool` for it. You now have a voice agent that can discover tools dynamically from the scoped MCP (or call specific ones directly). ## Step 4: Mint per-user tokens at runtime (the demo) [Section titled “Step 4: Mint per-user tokens at runtime (the demo)”](#step-4-mint-per-user-tokens-at-runtime-the-demo) See the dedicated [Environment variables](#environment-variables) section above for the full list and demo vs. production guidance. The demo (Next.js + Vapi Web SDK) shows the complete loop for testing: * User clicks “Start Voice Call” and passes `scalekitConnectionId` (the identifier) via Vapi call `metadata`. This is a local-testing shortcut: in production the identifier never comes from the browser (see [What changes when you go live](#what-changes-when-you-go-live-production)). * The backend (or the helper script) mints a fresh short-lived session token for that identifier + your Virtual MCP server. * You supply the token via the `Authorization: Bearer ...` header so Vapi can connect to the scoped MCP endpoint with the correct identity. For quick local testing use the provided scripts (prominently shown in the demo UI): ```bash python scripts/generate-mcp-token.py ``` See `scripts/generate-mcp-token.py` (recommended), `scripts/generate-mcp-token.js`, `app/api/scalekit/mcp-session/route.ts`, and the demo’s Virtual MCP panel for exact implementation and copyable output. Key pattern (what the script / route produces): ```json { "server": { "url": "https://...scalekit.../mcp/v3/servers/...", "headers": { "Authorization": "Bearer " } } } ``` In the demo you paste the `url` into Vapi’s MCP tool **Server URL** field and add the header manually (see Step 3). The UI and scripts make this easy to copy. ## Production note: tokens and config are injected at runtime [Section titled “Production note: tokens and config are injected at runtime”](#production-note-tokens-and-config-are-injected-at-runtime) In a real application you never manually edit the Vapi dashboard for each user or call: * Your backend mints the token **on the fly** (using the Scalekit SDK or direct call with management token) exactly when the user initiates the voice session. * You then use the Vapi API (authenticated with your `VAPI_PRIVATE_KEY`) to dynamically set or override the MCP tool’s `server` (url + Authorization header) on the assistant before starting the call. * The identifier always comes from the logged-in user context rather than a `TEST_IDENTIFIER` env var. Mint a session token (and build the `mcpConfig`) in your backend before each call. * Node.js mint-token.ts ```ts 1 // Mint via the REST API with a client-credentials token, as in the demo route. 2 try { 3 // Security: mint a short-lived, per-user token server-side so the credential 4 // never reaches the browser, Vapi dashboard, or the LLM. 5 const managementToken = await scalekit.getClientAccessToken(); 6 const base = process.env.SCALEKIT_ENVIRONMENT_URL!.replace(/\/$/, ''); 7 const tokenRes = await fetch( 8 `${base}/api/v1/mcp/configs/${mcpConfigId}/tokens`, 9 { 10 method: 'POST', 11 headers: { 12 Authorization: `Bearer ${managementToken}`, 13 'Content-Type': 'application/json', 14 }, 15 body: JSON.stringify({ 16 identifier: userIdentifier, 17 expiry: '3600s', // a protobuf Duration: seconds with an "s" suffix, 60s to 24h 18 }), 19 }, 20 ); 21 if (!tokenRes.ok) { 22 throw new Error(await tokenRes.text()); 23 } 24 const tokenData = await tokenRes.json(); 25 const token = tokenData.token; 26 27 const mcpConfig = { 28 url: mcpServerUrl, 29 headers: { Authorization: `Bearer ${token}` }, 30 }; 31 32 // Pass mcpConfig to Vapi (via API or call start) 33 } catch (err) { 34 console.error('Token mint failed:', err); 35 } ``` * Python mint_token.py ```python 1 from datetime import timedelta 2 3 try: 4 # Security: mint a short-lived, per-user token server-side so the credential 5 # never reaches the browser, Vapi dashboard, or the LLM. 6 token_response = scalekit_client.actions.mcp.create_session_token( 7 mcp_config_id=mcp_id, 8 identifier=user_identifier, 9 expiry=timedelta(hours=1), 10 ) 11 12 mcp_config = { 13 "url": mcp_server_url, 14 "headers": {"Authorization": f"Bearer {token_response.token}"}, 15 } 16 17 # use mcp_config with Vapi 18 except Exception as e: 19 print(f"Token mint failed: {e}") ``` The same Virtual MCP server (the `cfg_...` you created) is reused for everyone. Only the short-lived token and acting identifier change per user / per call. This is exactly what the demo’s `/api/scalekit/mcp-session` endpoint and the “In a real app” callout in the demo UI demonstrate. ## Step 5: Test the voice flow [Section titled “Step 5: Test the voice flow”](#step-5-test-the-voice-flow) 1. Start the demo: `npm run dev` + `ngrok http 3000`. 2. Update Vapi tool Server URL to your ngrok + `/api/vapi/webhook` (for the Function fallback) or point the MCP tool at the Scalekit URL + token. 3. In the browser: click **Start Voice Call**. 4. Speak natural requests. A few examples that work well with common scoped tools: **Calendar** * “What do I have on my calendar this week?” * “Find a 30-minute slot tomorrow afternoon for a sync with Priya” * “Add a meeting with the design team on Friday at 2pm” **Email (Gmail)** * “Find emails from Acme Corp” * “Summarize the latest thread with the legal team” * “Draft a polite reply to the last message from Sarah” **Slack** * “Summarize the latest messages in #product channel” * “Any mentions of the launch in Slack this morning?” **GitHub + Drive + cross-tool** * “Show my open pull requests” * “Find the Q3 roadmap in Drive and email a summary to the team” * “Check my calendar for tomorrow and email the attendees the agenda doc from Drive” The assistant should use the discovered tools, execute them via Scalekit (with your identity), and speak the results. Mix and match connectors (see [full list](/agentkit/connectors/)) that you’ve included in your Virtual MCP server. Watch the terminal for `[Vapi Webhook]` or MCP connection logs. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause & fix | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Vapi can’t discover tools | Token expired or missing `Authorization: Bearer ` header. Regenerate and paste fresh. | | “No connected account” when minting | User hasn’t authorized the connections in the Virtual MCP. Authorize in Scalekit → Connected Accounts. | | Agent ignores tools or hallucinates | System prompt doesn’t instruct tool use, or too many tools in scope. Tighten the tools in the Virtual MCP server and strengthen the prompt. | | 401 from Scalekit | Wrong identifier or connection not active for the config. Verify with `list_mcp_connected_accounts`. | See the demo repo for more. ## How this gives voice assistants access to any tool [Section titled “How this gives voice assistants access to any tool”](#how-this-gives-voice-assistants-access-to-any-tool) * **Scoping at config time** — the Virtual MCP only advertises the tools you mapped. The LLM never sees the rest of the catalog. * **Per-user tokens** — even when hundreds or thousands of tools exist, each call only carries the user’s authorized subset. * **Dynamic discovery** — Vapi fetches the current allowed tools at the start of the conversation. No static tool list to maintain. * **Token cost control** — fewer tools = dramatically smaller context. A huge catalog would be unusable; a small scoped set (e.g. 5–15 tools) is practical. **Real use case examples that become feasible only with scoping**: A sales rep might get “Gmail + Calendar + Salesforce + Slack” (8-12 tools). An engineer might get “GitHub + Drive + Slack + Gmail”. Both use the *same* underlying catalog, but each voice session only sees what that person is allowed to do. The same pattern works for any voice or chat platform that supports MCP clients. ## Security & compliance notes [Section titled “Security & compliance notes”](#security--compliance-notes) * Raw OAuth tokens never leave Scalekit. * Every tool call is audited with the acting user’s identity. * You can rotate or revoke access per connection without touching the agent. * Virtual MCP gives you an explicit allow-list instead of “all tools the user has ever connected”. ## Next steps & variations [Section titled “Next steps & variations”](#next-steps--variations) * Add more connectors (see the [full list](/agentkit/connectors/)) by extending the Virtual MCP mapping. * Switch voice platforms (replace Vapi with another MCP-capable voice or chat client). * Add a real user login flow so the identifier comes from your session instead of an env var. * Expose the same Virtual MCP to web, mobile, and voice clients from one config. * Combine with Scalekit’s user verification for stronger identity assurance. ## References [Section titled “References”](#references) * [Scalekit Virtual MCP docs](/agentkit/mcp/overview/) * [Vapi](https://vapi.ai) (see [MCP integration](https://docs.vapi.ai/tools/mcp)) * [AgentKit tool calling](/agentkit/tools/scalekit-optimized-tools/) * Demo repo + recording linked above. See the [full demo source](https://github.com/scalekit-developers/vapi-scalekit-voice-demo) and the [Virtual MCP server guide](/agentkit/mcp/configure-mcp-server/). --- # DOCUMENT BOUNDARY --- # Building a Custom Organization Switcher > Learn how to build your own organization switcher UI for complete control over multi-tenant user experiences. When users belong to multiple organizations, the default Scalekit organization switcher handles most use cases. However, some applications require deeper integration—a custom switcher embedded directly in your app’s navigation, or a specialized UI that matches your design system. This guide shows you how to build your own organization switcher using Scalekit’s APIs. ## Why build a custom switcher? [Section titled “Why build a custom switcher?”](#why-build-a-custom-switcher) The default Scalekit-hosted switcher works well for most scenarios. Build a custom switcher when you need: * **In-app navigation**: Users switch organizations without leaving your application * **Custom branding**: The switcher matches your application’s design language * **Specialized workflows**: Your app needs org-specific logic during switches * **Reduced redirects**: Avoid sending users through the authentication flow for every switch ## How the custom switcher works [Section titled “How the custom switcher works”](#how-the-custom-switcher-works) Your application handles the entire switching flow: 1. User authenticates through Scalekit and receives a session 2. Your app fetches the user’s organizations via the User Sessions API 3. You render your own organization selector UI 4. When a user selects an organization, your app updates the active context This approach gives you full control over the UI and routing, but requires you to manage session state and organization context within your application. ## Fetch user organizations [Section titled “Fetch user organizations”](#fetch-user-organizations) The User Sessions API returns the `authenticated_organizations` field containing all organizations the user can access. Use this data to populate your switcher UI. * Node.js Express.js ```javascript // Use case: Get user's organizations for your switcher UI // Security: Always validate session ownership before returning org data const session = await scalekit.session.getSession(sessionId); // Extract organizations from the session response const organizations = session.authenticated_organizations || []; // Render your organization switcher with this data res.json({ organizations }); ``` * Python Flask ```python # Use case: Get user's organizations for your switcher UI # Security: Always validate session ownership before returning org data session = scalekit_client.sessions.get_session(session_id) # Extract organizations from the session response organizations = session.get('authenticated_organizations', []) # Render your organization switcher with this data return jsonify({'organizations': organizations}) ``` * Go Gin ```go // Use case: Get user's organizations for your switcher UI // Security: Always validate session ownership before returning org data session, err := scalekitClient.Session().GetSession(ctx, sessionId) if err != nil { return err } // Extract organizations from the session response organizations := session.AuthenticatedOrganizations // Render your organization switcher with this data c.JSON(http.StatusOK, gin.H{"organizations": organizations}) ``` * Java Spring ```java // Use case: Get user's organizations for your switcher UI // Security: Always validate session ownership before returning org data Session session = scalekitClient.sessions().getSession(sessionId); // Extract organizations from the session response List organizations = session.getAuthenticatedOrganizations(); // Render your organization switcher with this data return ResponseEntity.ok(Map.of("organizations", organizations)); ``` The response includes organization IDs, names, and metadata for each organization the user can access. ## Add domain context [Section titled “Add domain context”](#add-domain-context) Enhance your switcher by displaying which domains are associated with each organization. Use the Domains API to fetch this information. ```javascript // Example: Fetch domains for an organization const domains = await scalekit.domain.listDomains('org_123'); // Display "@acme.com" next to the organization name in your UI ``` This helps users quickly identify the correct organization, especially when they belong to organizations with similar names. ## Handle organization selection [Section titled “Handle organization selection”](#handle-organization-selection) When a user selects an organization in your custom switcher, update your application’s context. Store the active organization ID in session storage or a cookie, then use it for subsequent API calls. * Node.js Express.js ```javascript 1 // Use case: Store selected organization and fetch org-specific data 2 app.post('/api/select-organization', async (req, res) => { 3 const { organizationId } = req.body; 4 const sessionId = req.session.scalekitSessionId; 5 6 // Security: Verify the user belongs to this organization 7 const session = await scalekit.session.getSession(sessionId); 8 const hasAccess = session.authenticated_organizations.some( 9 org => org.id === organizationId 10 ); 11 12 if (!hasAccess) { 13 return res.status(403).json({ error: 'Unauthorized' }); 14 } 15 16 // Store the active organization in the user's session 17 req.session.activeOrganizationId = organizationId; 18 19 res.json({ success: true }); 20 }); ``` * Python Flask ```python 1 # Use case: Store selected organization and fetch org-specific data 2 @app.route('/api/select-organization', methods=['POST']) 3 def select_organization(): 4 data = request.get_json() 5 organization_id = data.get('organizationId') 6 session_id = session.get('scalekit_session_id') 7 8 # Security: Verify the user belongs to this organization 9 user_session = scalekit_client.sessions.get_session(session_id) 10 has_access = any( 11 org['id'] == organization_id 12 for org in user_session.get('authenticated_organizations', []) 13 ) 14 15 if not has_access: 16 return jsonify({'error': 'Unauthorized'}), 403 17 18 # Store the active organization in the user's session 19 session['active_organization_id'] = organization_id 20 21 return jsonify({'success': True}) ``` * Go Gin ```go 1 // Use case: Store selected organization and fetch org-specific data 2 func SelectOrganization(c *gin.Context) { 3 var req struct { 4 OrganizationID string `json:"organizationId"` 5 } 6 if err := c.BindJSON(&req); err != nil { 7 c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) 8 return 9 } 10 11 sessionID := c.GetString("scalekitSessionID") 12 13 // Security: Verify the user belongs to this organization 14 session, err := scalekitClient.Session().GetSession(ctx, sessionID) 15 if err != nil { 16 c.JSON(http.StatusInternalServerError, gin.H{"error": "Session error"}) 17 return 18 } 19 20 hasAccess := false 21 for _, org := range session.AuthenticatedOrganizations { 22 if org.ID == req.OrganizationID { 23 hasAccess = true 24 break 25 } 26 } 27 28 if !hasAccess { 29 c.JSON(http.StatusForbidden, gin.H{"error": "Unauthorized"}) 30 return 31 } 32 33 // Store the active organization in the user's session 34 c.SetCookie("activeOrganizationID", req.OrganizationID, 3600, "/", "", true, true) 35 36 c.JSON(http.StatusOK, gin.H{"success": true}) 37 } ``` * Java Spring ```java 1 // Use case: Store selected organization and fetch org-specific data 2 @PostMapping("/api/select-organization") 3 public ResponseEntity selectOrganization( 4 @RequestBody Map request, 5 HttpSession httpSession 6 ) { 7 String organizationId = request.get("organizationId"); 8 String sessionId = (String) httpSession.getAttribute("scalekitSessionId"); 9 10 // Security: Verify the user belongs to this organization 11 Session session = scalekitClient.sessions().getSession(sessionId); 12 boolean hasAccess = session.getAuthenticatedOrganizations().stream() 13 .anyMatch(org -> org.getId().equals(organizationId)); 14 15 if (!hasAccess) { 16 return ResponseEntity.status(HttpStatus.FORBIDDEN) 17 .body(Map.of("error", "Unauthorized")); 18 } 19 20 // Store the active organization in the user's session 21 httpSession.setAttribute("activeOrganizationId", organizationId); 22 23 return ResponseEntity.ok(Map.of("success", true)); 24 } ``` Always verify that the user actually belongs to the organization they’re attempting to switch to. The `authenticated_organizations` array from the session is your source of truth for access control. ## When to use the hosted switcher instead [Section titled “When to use the hosted switcher instead”](#when-to-use-the-hosted-switcher-instead) The default Scalekit-hosted switcher is the right choice when: * You want the quickest implementation with minimal code * Your application doesn’t require in-app organization switching * You’re okay with users navigating through the authentication flow to switch organizations Build a custom switcher when user experience requirements demand deeper integration with your application’s UI and routing. You may refer to our [Sample Org Swithcer ](https://github.com/scalekit-inc/Nextjs-Django-Org-Switcher-Example/tree/main)application to better understand how the API calls enable this custom org switcher that is embedded inside your application. --- # DOCUMENT BOUNDARY --- # Build a multi-agent email triage crew with CrewAI > Use CrewAI multi-agent orchestration with Scalekit-authenticated Gmail tools to scan, classify, and draft replies to emails. CrewAI’s strength is multi-agent orchestration — you define specialized agents and let them collaborate on a shared workflow. But the moment those agents need to call Gmail, Slack, or GitHub on behalf of a real user, you’re stuck managing OAuth tokens, refresh cycles, and per-user credential storage before you write any agent logic. Scalekit eliminates that plumbing. It stores OAuth sessions per user, refreshes tokens automatically, and exposes authenticated tools over MCP. Your CrewAI code never touches a token — it connects to a Scalekit MCP URL and gets back ready-to-use tools. This cookbook builds a three-agent email triage crew: one agent scans unread emails, another classifies them by priority, and a third drafts replies for the high-priority items. All Gmail access goes through Scalekit. **What this recipe covers:** * **Scalekit MCP integration** — get a Virtual MCP server URL and mint a session token that authenticates Gmail tools for a specific user * **CrewAI MCPServerAdapter** — connect CrewAI to the MCP server so agents can discover and call Gmail tools * **Multi-agent pipeline** — define three agents with distinct roles that run in sequence * **First-run authorization** — handle the OAuth flow when a user hasn’t connected Gmail yet The complete source is available in the [crewai-scalekit-example](https://github.com/scalekit-developers/crewai-scalekit-example) repository. ### 1. Set up Gmail and a Virtual MCP server [Section titled “1. Set up Gmail and a Virtual MCP server”](#1-set-up-gmail-and-a-virtual-mcp-server) New environments do not ship with a Virtual MCP server. Create the Gmail connection and the Virtual MCP server **before** you run the sample — otherwise `list_configs` returns an empty list. In the [Scalekit Dashboard](https://app.scalekit.com): 1. Go to **AgentKit** → **Connections** → **Create Connection** and select **Gmail**. 2. Note the **Connection name** — your code references it by this exact string (for example `gmail`). 3. Go to **AgentKit** > **Virtual MCP Servers** and create a Virtual MCP server (for example `gmail-user-tools`). 4. Attach the Gmail connection and include the tools the crew needs (at least `gmail_fetch_mails`). 5. Copy the **config name** into `SCALEKIT_MCP_CONFIG_NAME` below. For the full API path (create config, mint session tokens, connect agents), see [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/). ### 2. Install dependencies [Section titled “2. Install dependencies”](#2-install-dependencies) ```bash pip install crewai "crewai-tools[mcp]" scalekit-sdk-python python-dotenv ``` `crewai-tools` provides `MCPServerAdapter`, which connects CrewAI to any MCP server. `scalekit-sdk-python` generates the authenticated MCP URL for each user. ### 3. Configure credentials [Section titled “3. Configure credentials”](#3-configure-credentials) ```bash cp .env.example .env ``` .env ```bash 1 # Scalekit — get these at app.scalekit.com → Settings → API Credentials 2 SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev 3 SCALEKIT_CLIENT_ID=skc_... 4 SCALEKIT_CLIENT_SECRET=your-secret 5 6 # User identifier from your application 7 SCALEKIT_USER_IDENTIFIER=user_123 8 9 # Exact Connection name from AgentKit → Connections 10 GMAIL_CONNECTION_NAME=gmail 11 12 # Virtual MCP server name — must match the server created in step 1 13 SCALEKIT_MCP_CONFIG_NAME=gmail-user-tools 14 15 # LLM — any OpenAI-compatible endpoint 16 OPENAI_API_KEY=sk-... ``` ### 4. Initialize Scalekit and ensure authorization [Section titled “4. Initialize Scalekit and ensure authorization”](#4-initialize-scalekit-and-ensure-authorization) ```python import os from scalekit import ScalekitClient from dotenv import find_dotenv, load_dotenv load_dotenv(find_dotenv()) # Constructor: env_url, client_id, client_secret scalekit_client = ScalekitClient( os.environ["SCALEKIT_ENVIRONMENT_URL"], os.environ["SCALEKIT_CLIENT_ID"], os.environ["SCALEKIT_CLIENT_SECRET"], ) actions = scalekit_client.actions USER_ID = os.getenv("SCALEKIT_USER_IDENTIFIER", "user_123") ``` Before calling any Gmail tool, check whether the user has an active connected account. If not, print an authorization link and wait for them to complete OAuth in the browser: ```python 1 # Use the exact Connection name from AgentKit → Connections (not a guessed slug). 2 CONNECTION_NAME = os.getenv("GMAIL_CONNECTION_NAME", "gmail") 3 4 response = actions.get_or_create_connected_account( 5 connection_name=CONNECTION_NAME, 6 identifier=USER_ID, 7 ) 8 connected_account = response.connected_account 9 10 # Do not start the crew until status is ACTIVE. 11 if connected_account.status != "ACTIVE": 12 link = actions.get_authorization_link( 13 connection_name=CONNECTION_NAME, 14 identifier=USER_ID, 15 ) 16 print(f"\n[{CONNECTION_NAME}] Authorization required.") 17 print(f"Open this link:\n\n {link.link}\n") 18 input("Press Enter after authorizing...") 19 response = actions.get_or_create_connected_account( 20 connection_name=CONNECTION_NAME, 21 identifier=USER_ID, 22 ) 23 connected_account = response.connected_account 24 25 if connected_account.status != "ACTIVE": 26 raise RuntimeError( 27 f"{CONNECTION_NAME} is still not ACTIVE. Complete authorization and try again." 28 ) ``` After the first successful authorization, `get_or_create_connected_account` returns an active account on all subsequent runs. Scalekit refreshes expired tokens automatically. ### 5. Connect to Gmail tools via MCP [Section titled “5. Connect to Gmail tools via MCP”](#5-connect-to-gmail-tools-via-mcp) Look up the Virtual MCP server you created in step 1, mint a session token, then pass both to `MCPServerAdapter`. If `list_configs` returns no results, stop and create the config first (dashboard or [configure-mcp-server](/agentkit/mcp/configure-mcp-server/)). Do not assume a default Virtual MCP server exists. ```python 1 from crewai_tools import MCPServerAdapter 2 from datetime import timedelta 3 4 mcp_config_name = os.getenv("SCALEKIT_MCP_CONFIG_NAME", "gmail-user-tools") 5 6 # Retrieve config_id by listing Virtual MCP servers filtered by name 7 list_response = actions.mcp.list_configs(filter_name=mcp_config_name) 8 if not list_response.configs: 9 raise RuntimeError( 10 f"No Virtual MCP server named '{mcp_config_name}'. " 11 "Create one under AgentKit > Virtual MCP Servers (include Gmail tools), " 12 "or follow https://docs.scalekit.com/agentkit/mcp/configure-mcp-server/" 13 ) 14 15 mcp_server_url = list_response.configs[0].mcp_server_url 16 mcp_id = list_response.configs[0].id 17 18 token_response = actions.mcp.create_session_token( 19 mcp_config_id=mcp_id, 20 identifier=USER_ID, 21 expiry=timedelta(hours=1), 22 ) ``` Mint a fresh session token before each agent run. The Virtual MCP server URL is static — it stays the same across all sessions. CrewAI’s `MCPServerAdapter` connects to the MCP server and discovers all available tools. It connects as soon as you create it, and the tools work only until you call `stop()`, so keep it open until the crew finishes in step 7: ```python mcp_adapter = MCPServerAdapter({ "url": mcp_server_url, "headers": {"Authorization": f"Bearer {token_response.token}"}, "transport": "streamable-http", }) # `tools` is a list of CrewAI-compatible tool objects tools = mcp_adapter.tools print(f"Discovered {len(tools)} Gmail tools") ``` ### 6. Define the agents [Section titled “6. Define the agents”](#6-define-the-agents) Three agents, each with a specific role. Only the Inbox Scanner needs direct access to Gmail tools — the other agents work with the data it produces: ```python 1 from crewai import Agent, LLM 2 3 llm = LLM( 4 model=os.getenv("LLM_MODEL", "gpt-4o"), 5 base_url=os.getenv("OPENAI_BASE_URL"), 6 api_key=os.getenv("OPENAI_API_KEY"), 7 ) 8 9 scanner = Agent( 10 role="Inbox Scanner", 11 goal="Fetch the user's latest unread emails and extract key metadata.", 12 backstory=( 13 "You are an efficient assistant that reads a Gmail inbox and " 14 "returns a structured summary of unread messages including " 15 "subject, sender, date, and a one-line preview." 16 ), 17 tools=tools, # Gmail tools from MCPServerAdapter 18 llm=llm, 19 verbose=True, 20 ) 21 22 prioritizer = Agent( 23 role="Email Prioritizer", 24 goal="Classify each email by urgency: high, medium, or low.", 25 backstory=( 26 "You are an expert at triaging incoming messages. You consider " 27 "sender importance, subject keywords, and time sensitivity to " 28 "assign a priority level to each email." 29 ), 30 llm=llm, 31 verbose=True, 32 ) 33 34 drafter = Agent( 35 role="Reply Drafter", 36 goal="Draft short, professional replies for high-priority emails.", 37 backstory=( 38 "You are a concise writer who drafts polite, on-point email " 39 "replies. You focus only on high-priority items and keep each " 40 "draft under 100 words." 41 ), 42 llm=llm, 43 verbose=True, 44 ) ``` Tool assignment Only assign Gmail tools to agents that need them. The Prioritizer and Drafter operate on data from the Scanner’s output — they don’t need direct Gmail access. This keeps the agent scopes clean and reduces unnecessary tool-calling overhead. ### 7. Define tasks and run the crew [Section titled “7. Define tasks and run the crew”](#7-define-tasks-and-run-the-crew) Each task describes what the agent should do and what output to expect. CrewAI runs them in sequence — each task receives the output of the previous one: ```python 1 from crewai import Crew, Process, Task 2 3 scan_task = Task( 4 description=( 5 "Fetch the last 5 unread emails from Gmail. For each email, " 6 "return: subject, sender name, sender email, date, and a " 7 "one-sentence preview of the body." 8 ), 9 expected_output=( 10 "A numbered list of 5 emails with subject, sender, date, " 11 "and preview for each." 12 ), 13 agent=scanner, 14 ) 15 16 prioritize_task = Task( 17 description=( 18 "Take the list of emails from the Inbox Scanner and classify " 19 "each one as high, medium, or low priority. Consider sender " 20 "importance, urgency cues in the subject, and whether the email " 21 "requires a response." 22 ), 23 expected_output=( 24 "The same list of emails, each now tagged with a priority " 25 "level (high / medium / low) and a brief reason." 26 ), 27 agent=prioritizer, 28 ) 29 30 draft_task = Task( 31 description=( 32 "For each email marked as high priority by the Prioritizer, " 33 "draft a short, professional reply (under 100 words). Skip " 34 "medium and low priority emails." 35 ), 36 expected_output=( 37 "A list of draft replies, one per high-priority email, " 38 "including the original subject line and the draft text." 39 ), 40 agent=drafter, 41 ) 42 43 crew = Crew( 44 agents=[scanner, prioritizer, drafter], 45 tasks=[scan_task, prioritize_task, draft_task], 46 process=Process.sequential, 47 verbose=True, 48 ) 49 50 try: 51 result = crew.kickoff() 52 print(result) 53 finally: 54 # Close the MCP connection only after the crew is done with the tools 55 mcp_adapter.stop() ``` ### 8. Run and test [Section titled “8. Run and test”](#8-run-and-test) ```bash python agent.py ``` On first run, you see an authorization prompt: ```text [gmail] Authorization required. Open this link: https://auth.scalekit.dev/connect/... Press Enter after authorizing... ``` After completing OAuth in the browser and pressing Enter, the crew runs: ```text 1 [ok] Session token minted 2 Discovered 15 Gmail tools 3 4 [Inbox Scanner] Fetching unread emails... 5 [Email Prioritizer] Classifying 5 emails... 6 [Reply Drafter] Drafting replies for 2 high-priority emails... 7 8 ============================================================ 9 CREW RESULT 10 ============================================================ 11 ## High-Priority Emails — Draft Replies 12 13 1. Subject: "Q1 roadmap feedback needed" 14 From: Sarah Chen 15 Priority: HIGH 16 Draft: "Hi Sarah, thanks for flagging this. I'll review the 17 roadmap doc this afternoon and share my comments by EOD." 18 19 2. Subject: "Production incident — action required" 20 From: PagerDuty 21 Priority: HIGH 22 Draft: "Acknowledged. I'm looking into the alert now and will 23 update the incident channel within 15 minutes." ``` On subsequent runs, the authorization step is skipped entirely. ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) Connection name mismatch * **Symptom**: `get_or_create_connected_account` returns an error or creates a new connection instead of finding the existing one * **Cause**: The connection name in your code does not match the name in the Scalekit Dashboard exactly * **Fix**: Copy the connection name from **AgentKit → Connections** in the dashboard and paste it into your code. Case and spacing matter. Virtual MCP server not found * **Symptom**: `list_configs` returns an empty list * **Cause**: No Virtual MCP server exists yet, or `SCALEKIT_MCP_CONFIG_NAME` does not match the dashboard name * **Fix**: Create a Virtual MCP server under **AgentKit** > **Virtual MCP Servers** that includes Gmail tools, then set `SCALEKIT_MCP_CONFIG_NAME` to that exact name. See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/). Nullable schema fields crash CrewAI * **Symptom**: `TypeError` or `ValidationError` when CrewAI parses tool schemas containing `{"type": ["string", "null"]}` * **Cause**: CrewAI’s built-in JSON Schema converter does not handle nullable union types * **Fix**: Apply the monkey-patch from the [sample repo](https://github.com/scalekit-developers/crewai-scalekit-example/blob/main/agent.py) at the top of your script. This adds nullable type handling to `crewai.utilities.pydantic_schema_utils`. Missing or wrong LLM API key * **Symptom**: `AuthenticationError` or `401` from the LLM provider * **Cause**: `OPENAI_API_KEY` is not set, or points to the wrong provider * **Fix**: Verify your API key is valid. If using a LiteLLM proxy or custom endpoint, set both `OPENAI_API_KEY` and `OPENAI_BASE_URL`. The `LLM_MODEL` variable defaults to `gpt-4o` — change it to match your provider. ## Production notes [Section titled “Production notes”](#production-notes) **User ID from session** — The sample hardcodes `USER_ID = "user_123"`. In production, replace this with the real user identifier from your application’s session or JWT. A mismatch means Scalekit looks up the wrong user’s Gmail connection. **Token freshness** — Scalekit refreshes expired OAuth tokens before returning them. Mint a fresh session token before each agent run — session tokens are short-lived and scoped to a single run. **MCP server URL is static** — The Virtual MCP server URL (`mcp_server_url`) is stable and the same for all users. Cache it once per config. Only the session token is per-run. **Rate limits** — Gmail API has per-user daily quotas. If your crew runs frequently, add rate-limiting logic or use Scalekit’s built-in tool pagination to limit the number of emails fetched per run. **Error handling** — In production, wrap `crew.kickoff()` in a try/except to handle LLM failures, MCP connection errors, and tool execution failures gracefully. Log the raw error for debugging. ## Next steps [Section titled “Next steps”](#next-steps) * **Add more connectors** — extend the crew with Slack, GitHub, or Calendar tools. Create additional connections in the dashboard, add their tools to your Virtual MCP server, and pass the expanded tool set to the Scanner agent. See [all supported connectors](/agentkit/connectors/). * **Try the AgentKit CrewAI example** — for a shorter, single-agent version of this pattern, see the [CrewAI example page](/agentkit/examples/crewai/). * **Explore other frameworks** — Scalekit works with LangChain, Google ADK, Vercel AI SDK, and more. See [AgentKit code samples](/agentkit/examples/) for the full list. * **Handle re-authorization** — if a user revokes Gmail access, `get_or_create_connected_account` returns an inactive account. Add a re-authorization path to recover gracefully. * **Review the AgentKit quickstart** — for a broader overview of connections, tools, and MCP, see the [AgentKit quickstart](/agentkit/quickstart/). --- # DOCUMENT BOUNDARY --- # Build a daily briefing agent with Vercel AI SDK and Scalekit AgentKit > Connect a TypeScript or Python agent via Vercel AI SDK and Scalekit AgentKit to Google Calendar and Gmail with authenticated tool calls. A daily briefing agent needs two things: today’s calendar events and the latest unread emails. Both live behind OAuth-protected APIs, and each requires its own token, its own authorization flow, and its own refresh logic. Before you write any scheduling logic, you’re already maintaining two parallel token lifecycles. Scalekit eliminates that overhead. It stores one OAuth session per connector per user, refreshes tokens automatically, and exposes **built-in tools** such as `googlecalendar_list_events` and `gmail_fetch_mails`. Your agent calls those tools through Scalekit; it never talks to the Google Calendar or Gmail REST APIs directly. **What this recipe covers:** * **Authorize once per connector** — create connected accounts for Calendar and Gmail, open the OAuth link if needed, and wait until status is `ACTIVE` * **Built-in tool calls** — `execute_tool("googlecalendar_list_events")` and `execute_tool("gmail_fetch_mails")` so Scalekit runs the provider call and returns structured data * **Wire both tools into an agent** — Vercel AI SDK (TypeScript) or Anthropic messages (Python) The complete source used here is available in the [vercel-ai-agent-toolkit](https://github.com/scalekit-developers/vercel-ai-agent-toolkit) repository, with a TypeScript implementation using the Vercel AI SDK and a Python implementation using the Anthropic SDK directly. ### 1. Set up connections in Scalekit [Section titled “1. Set up connections in Scalekit”](#1-set-up-connections-in-scalekit) In the [Scalekit Dashboard](https://app.scalekit.com), create two connections under **AgentKit** > **Connections** > **Create Connection**: * `googlecalendar` — Google Calendar OAuth connection * `gmail` — Gmail OAuth connection The connection names are identifiers your code references directly. They must match exactly. ### 2. Install dependencies [Section titled “2. Install dependencies”](#2-install-dependencies) * TypeScript ```bash cd typescript pnpm install ``` The `typescript/package.json` includes: ```json { "dependencies": { "ai": "^4.3.15", "@ai-sdk/anthropic": "^1.2.12", "@scalekit-sdk/node": "^2.18.0", "zod": "^3.0.0", "dotenv": "^16.0.0" } } ``` * Python ```bash cd python uv venv .venv uv pip install -r requirements.txt ``` The `python/requirements.txt` includes: ```text scalekit-sdk-python anthropic python-dotenv ``` ### 3. Configure credentials [Section titled “3. Configure credentials”](#3-configure-credentials) Copy the example env file and fill in your credentials: ```bash cp typescript/.env.example typescript/.env # TypeScript cp typescript/.env.example python/.env # Python (same variables) ``` .env ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=your-secret ANTHROPIC_API_KEY=sk-ant-... ``` Get your Scalekit credentials at **app.scalekit.com → Settings → API Credentials**. ### 4. Initialize the Scalekit client [Section titled “4. Initialize the Scalekit client”](#4-initialize-the-scalekit-client) * TypeScript ```typescript import { ScalekitClient } from '@scalekit-sdk/node'; import { ConnectorStatus } from '@scalekit-sdk/node'; import 'dotenv/config'; // Never hard-code credentials — they would be exposed in source control. // Pull them from environment variables at runtime. const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const actions = scalekit.actions; const USER_ID = 'user_123'; // Replace with the real user ID from your session ``` Import `ConnectorStatus` from `@scalekit-sdk/node` (2.18.0 or later). Compare `connectedAccount.status` against `ConnectorStatus.ACTIVE` rather than the string `'ACTIVE'` — TypeScript’s type system enforces this. * Python ```python 1 import os 2 import json 3 from datetime import datetime, timedelta 4 from dotenv import load_dotenv 5 import anthropic 6 from scalekit import ScalekitClient 7 8 load_dotenv() 9 10 # Never hard-code credentials — they would be exposed in source control. 11 # Pull them from environment variables at runtime. 12 # Constructor: env_url, client_id, client_secret 13 scalekit_client = ScalekitClient( 14 os.environ["SCALEKIT_ENVIRONMENT_URL"], 15 os.environ["SCALEKIT_CLIENT_ID"], 16 os.environ["SCALEKIT_CLIENT_SECRET"], 17 ) 18 actions = scalekit_client.actions 19 20 USER_ID = "user_123" # Replace with the real user ID from your session ``` `scalekit_client.actions` is the entry point for connected-account operations and built-in tool execution. ### 5. Ensure each connector is authorized [Section titled “5. Ensure each connector is authorized”](#5-ensure-each-connector-is-authorized) Before calling any API, check whether the user has an active connected account. If not, print an authorization link and wait for them to complete the browser OAuth flow. * TypeScript ```typescript 1 async function ensureConnected(connectionName: string) { 2 let { connectedAccount } = await actions.getOrCreateConnectedAccount({ 3 connectionName, 4 identifier: USER_ID, 5 }); 6 7 // Do not call tools until the user finishes OAuth and status is ACTIVE. 8 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 9 const { link } = await actions.getAuthorizationLink({ 10 connectionName, 11 identifier: USER_ID, 12 }); 13 console.log(`\n[${connectionName}] Authorization required.`); 14 console.log(`Open this link:\n\n ${link}\n`); 15 console.log('Press Enter once you have completed the OAuth flow...'); 16 await new Promise(resolve => { 17 process.stdin.resume(); 18 process.stdin.once('data', () => { process.stdin.pause(); resolve(); }); 19 }); 20 21 const refreshed = await actions.getOrCreateConnectedAccount({ 22 connectionName, 23 identifier: USER_ID, 24 }); 25 connectedAccount = refreshed.connectedAccount; 26 } 27 28 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 29 throw new Error(`${connectionName} is still not ACTIVE. Complete authorization and try again.`); 30 } 31 32 return connectedAccount; 33 } ``` * Python ```python 1 def ensure_connected(connection_name: str): 2 response = actions.get_or_create_connected_account( 3 connection_name=connection_name, 4 identifier=USER_ID, 5 ) 6 connected_account = response.connected_account 7 8 # Do not call tools until the user finishes OAuth and status is ACTIVE. 9 if connected_account.status != "ACTIVE": 10 link_response = actions.get_authorization_link( 11 connection_name=connection_name, 12 identifier=USER_ID, 13 ) 14 print(f"\n[{connection_name}] Authorization required.") 15 print(f"Open this link:\n\n {link_response.link}\n") 16 input("Press Enter once you have completed the OAuth flow...") 17 response = actions.get_or_create_connected_account( 18 connection_name=connection_name, 19 identifier=USER_ID, 20 ) 21 connected_account = response.connected_account 22 23 if connected_account.status != "ACTIVE": 24 raise RuntimeError( 25 f"{connection_name} is still not ACTIVE. Complete authorization and try again." 26 ) 27 28 return connected_account ``` After the first successful authorization, `getOrCreateConnectedAccount` / `get_or_create_connected_account` returns an active account on all subsequent calls. Scalekit refreshes expired tokens automatically — your code never calls a token-refresh endpoint. ### 6. Fetch calendar events with a built-in tool [Section titled “6. Fetch calendar events with a built-in tool”](#6-fetch-calendar-events-with-a-built-in-tool) Call `execute_tool` with `googlecalendar_list_events`. Scalekit uses the stored OAuth session, calls Google Calendar, and returns structured event data. Your agent never handles a Google access token or the Calendar REST API. * TypeScript ```typescript 1 import { tool } from 'ai'; 2 import { z } from 'zod'; 3 4 const getCalendarEvents = tool({ 5 description: "Fetch today's events from Google Calendar via Scalekit", 6 parameters: z.object({ 7 maxResults: z.number().optional().default(5), 8 }), 9 execute: async ({ maxResults }) => { 10 // Today in the machine's local time zone, as RFC 3339 timestamps 11 const startOfDay = new Date(); 12 startOfDay.setHours(0, 0, 0, 0); 13 const endOfDay = new Date(startOfDay); 14 endOfDay.setDate(endOfDay.getDate() + 1); 15 16 const response = await actions.executeTool({ 17 toolName: 'googlecalendar_list_events', 18 connectedAccountId: calendarAccount?.id, 19 toolInput: { 20 time_min: startOfDay.toISOString(), 21 time_max: endOfDay.toISOString(), 22 single_events: true, // list each occurrence of a recurring event 23 max_results: maxResults, 24 }, 25 }); 26 // Tool output lives under data — log once when integrating a new tool. 27 return response.data ?? {}; 28 }, 29 }); ``` * Python ```python 1 def fetch_calendar_events(connected_account_id: str, max_results: int = 5) -> dict: 2 # Today in the machine's local time zone, as RFC 3339 timestamps 3 start_of_day = datetime.now().astimezone().replace(hour=0, minute=0, second=0, microsecond=0) 4 end_of_day = start_of_day + timedelta(days=1) 5 6 response = actions.execute_tool( 7 tool_name="googlecalendar_list_events", 8 connected_account_id=connected_account_id, 9 tool_input={ 10 "time_min": start_of_day.isoformat(), 11 "time_max": end_of_day.isoformat(), 12 "single_events": True, # list each occurrence of a recurring event 13 "max_results": max_results, 14 }, 15 ) 16 # Tool output lives under data — log once when integrating a new tool. 17 return response.data ``` ### 7. Fetch emails with a built-in tool [Section titled “7. Fetch emails with a built-in tool”](#7-fetch-emails-with-a-built-in-tool) Use the same `execute_tool` pattern for Gmail with `gmail_fetch_mails`. Scalekit runs the Gmail API call and returns structured data. * TypeScript ```typescript 1 const getUnreadEmails = tool({ 2 description: 'Fetch top unread emails from Gmail via Scalekit actions', 3 parameters: z.object({ 4 maxResults: z.number().optional().default(5), 5 }), 6 execute: async ({ maxResults }) => { 7 const response = await actions.executeTool({ 8 toolName: 'gmail_fetch_mails', 9 connectedAccountId: gmailAccount?.id, 10 toolInput: { 11 query: 'is:unread', 12 max_results: maxResults, 13 }, 14 }); 15 return response.data ?? {}; 16 }, 17 }); ``` * Python ```python def fetch_unread_emails(connected_account_id: str, max_results: int = 5) -> dict: response = actions.execute_tool( tool_name="gmail_fetch_mails", connected_account_id=connected_account_id, tool_input={ "query": "is:unread", "max_results": max_results, }, ) return response.data ``` You do not need Google Calendar or Gmail API docs for the common path — tool names and parameters are consistent across Scalekit connectors. Browse [all supported agent connectors](/agentkit/connectors/) for the full tool list. ### 8. Wire the agent together [Section titled “8. Wire the agent together”](#8-wire-the-agent-together) Pass both tools to the LLM and ask for a daily summary. * TypeScript The TypeScript version uses the Vercel AI SDK’s `generateText` with `maxSteps` to allow the LLM to call multiple tools in sequence before producing the final response. ```typescript 1 import { generateText } from 'ai'; 2 import { anthropic } from '@ai-sdk/anthropic'; 3 4 // Authorize one connector at a time: each waits for its own Enter keypress 5 const calendarAccount = await ensureConnected('googlecalendar'); 6 const gmailAccount = await ensureConnected('gmail'); 7 8 const today = new Date(); 9 10 const { text } = await generateText({ 11 model: anthropic('claude-sonnet-4-6'), 12 prompt: `Give me a summary of my day for ${today.toDateString()}: list today's calendar events and my top 5 unread emails.`, 13 tools: { 14 getCalendarEvents, 15 getUnreadEmails, 16 }, 17 maxSteps: 5, // allow the LLM to call multiple tools before responding 18 }); 19 20 console.log(text); ``` `maxSteps` controls how many tool-call rounds the LLM can make before it must return a final text response. Without it, `generateText` stops after the first tool call. * Python The Python version uses the Anthropic SDK directly with a manual agentic loop. The loop continues until the model returns `stop_reason == "end_turn"` with no pending tool calls. ```python 1 def run_agent(): 2 calendar_account = ensure_connected("googlecalendar") 3 gmail_account = ensure_connected("gmail") 4 5 client = anthropic.Anthropic() 6 today = datetime.now().strftime("%A, %B %d, %Y") 7 8 tools = [ 9 { 10 "name": "get_calendar_events", 11 "description": "Fetch today's events from Google Calendar via Scalekit", 12 "input_schema": { 13 "type": "object", 14 "properties": {"max_results": {"type": "integer", "default": 5}}, 15 }, 16 }, 17 { 18 "name": "get_unread_emails", 19 "description": "Fetch top unread emails from Gmail via Scalekit actions", 20 "input_schema": { 21 "type": "object", 22 "properties": {"max_results": {"type": "integer", "default": 5}}, 23 }, 24 }, 25 ] 26 27 messages = [ 28 { 29 "role": "user", 30 "content": f"Give me a summary of my day for {today}: list today's calendar events and my top 5 unread emails.", 31 } 32 ] 33 34 while True: 35 response = client.messages.create( 36 model="claude-sonnet-4-6", 37 max_tokens=1024, 38 tools=tools, 39 messages=messages, 40 ) 41 messages.append({"role": "assistant", "content": response.content}) 42 43 if response.stop_reason == "end_turn": 44 for block in response.content: 45 if hasattr(block, "text"): 46 print(block.text) 47 break 48 49 tool_results = [] 50 for block in response.content: 51 if block.type == "tool_use": 52 max_results = block.input.get("max_results", 5) 53 if block.name == "get_calendar_events": 54 result = fetch_calendar_events(calendar_account.id, max_results) 55 elif block.name == "get_unread_emails": 56 result = fetch_unread_emails(gmail_account.id, max_results) 57 else: 58 result = {"error": f"Unknown tool: {block.name}"} 59 tool_results.append({ 60 "type": "tool_result", 61 "tool_use_id": block.id, 62 "content": json.dumps(result), 63 }) 64 65 if tool_results: 66 messages.append({"role": "user", "content": tool_results}) 67 else: 68 break 69 70 if __name__ == "__main__": 71 run_agent() ``` ### 9. Testing [Section titled “9. Testing”](#9-testing) Run the agent: * TypeScript ```bash cd typescript && pnpm start ``` * Python ```bash cd python && .venv/bin/python index.py ``` On first run, you see two authorization prompts in sequence: ```text [googlecalendar] Authorization required. Open this link: https://auth.scalekit.dev/connect/... Press Enter once you have completed the OAuth flow... [gmail] Authorization required. Open this link: https://auth.scalekit.dev/connect/... Press Enter once you have completed the OAuth flow... ``` After both connectors are authorized, the agent fetches your data and returns a summary: ```text Here's your day for Friday, March 27, 2026: 📅 Calendar — 3 events today • 9:00 AM Team standup (30 min) • 1:00 PM Product review • 4:00 PM 1:1 with manager 📧 Unread emails — top 5 • "Q1 roadmap feedback needed" — Sarah Chen, 1h ago • "Deploy failed: production" — GitHub Actions, 2h ago • "New PR review requested" — Lin Feng, 3h ago ... ``` On subsequent runs, both authorization prompts are skipped. Scalekit returns the active session directly. ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) Connection name mismatch * **Symptom**: `getOrCreateConnectedAccount` returns an error for `googlecalendar` or `gmail` * **Cause**: The connection name in the Scalekit Dashboard does not match the literal string in your code * **Fix**: Make the dashboard connection name match your code exactly, for example `googlecalendar` instead of `google-calendar` TypeScript status compared to a string * **Symptom**: TypeScript raises `TS2367` for `connectedAccount?.status !== 'ACTIVE'` * **Cause**: The SDK returns a `ConnectorStatus` enum, not a string literal * **Fix**: Import `ConnectorStatus` from `@scalekit-sdk/node` (2.18.0 or later) and compare against `ConnectorStatus.ACTIVE` `maxSteps` missing in the Vercel AI SDK * **Symptom**: `generateText` stops after the first tool call instead of returning a final summary * **Cause**: The model is not allowed to make enough tool-call rounds * **Fix**: Set `maxSteps` to at least `3`, and increase it if your workflow needs more than one tool call plus a final response Tool called before authorization finishes * **Symptom**: First run prints an auth link, then `execute_tool` fails because the account is not `ACTIVE` * **Cause**: The sample continued to the tool call without waiting for the browser OAuth flow * **Fix**: Block until the user completes authorization (for example `input(...)` in Python or a stdin wait in Node), then re-fetch the connected account before calling tools ## Production notes [Section titled “Production notes”](#production-notes) **User ID from session** — Both implementations hardcode `USER_ID = "user_123"`. In production, replace this with the real user identifier from your application’s session. A mismatch means Scalekit looks up the wrong user’s connected accounts. **Token freshness** — Scalekit refreshes OAuth tokens automatically before tool execution. You do not fetch provider tokens or call a refresh endpoint in application code. **First-run blocking** — The authorization prompt blocks the process until the user completes OAuth in the browser. In a web application, redirect the user to `link` instead of printing it, and handle the callback before proceeding. **`execute_tool` response shape** — Tool output lives under `response.data` (Python and Node). Keys inside `data` depend on the tool. Log the raw response once when integrating a new tool, then pass that structure to the LLM. **Rate limits** — Google Calendar and Gmail both enforce per-user quotas. If your agent runs frequently, avoid tight polling loops and cache briefing data where freshness allows. ## Next steps [Section titled “Next steps”](#next-steps) * **Add more connectors** — The same `ensureConnected` + `execute_tool` pattern works for any Scalekit-supported connector. Swap the connection name and tool name. See [all supported connectors](/agentkit/connectors/). * **Need a raw provider call** — Prefer built-in tools first. If a tool does not cover your case, see [Build custom tools](/agentkit/tools/custom-tools/) rather than extracting tokens in app code. * **Stream the response** — Replace `generateText` with `streamText` in the Vercel AI SDK to stream the LLM’s summary token-by-token instead of waiting for the full response. * **Handle re-authorization** — If a user revokes access, `getOrCreateConnectedAccount` returns an inactive account. Add a re-authorization path to recover gracefully instead of crashing. * **Review the AgentKit quickstart** — For a broader overview of the connected-accounts model and supported providers, see the [AgentKit quickstart](/agentkit/quickstart/). --- # DOCUMENT BOUNDARY --- # FastRouter + Scalekit tool calling > Build a Node.js agent that routes LLM calls through FastRouter and uses Scalekit for per-user OAuth tools. Build an agent that routes LLM calls through [FastRouter](https://fastrouter.ai). FastRouter provides an OpenAI-compatible chat completions API, so the integration requires only one configuration change: point the OpenAI SDK’s `baseURL` at FastRouter. Scalekit extends that with per-user OAuth tool access, so your agent can read Gmail, create GitHub issues, or post to Slack on behalf of individual users. You can choose from 614 [connectors](/agentkit/connectors/). Scalekit handles OAuth token storage, tool discovery, and tool execution for every connected service. The sample repository is **[fastrouter-scalekit-demo](https://github.com/scalekit-developers/fastrouter-scalekit-demo)** on GitHub. ## What you are building [Section titled “What you are building”](#what-you-are-building) * **FastRouter as the LLM provider** — All chat completions go through FastRouter’s OpenAI-compatible endpoint. Switch models by changing one environment variable. * **Scalekit for tool access** — `listScopedTools` returns per-user tool schemas ready to pass directly to FastRouter. `executeTool` runs each tool server-side and returns structured results. * **B2B OAuth without custom OAuth code** — Scalekit handles the OAuth flow, token storage, and refresh for each connected service. Your agent gets an auth link, waits for the user to authorize, and receives a verified, active connected account. * **Agentic loop** — The agent calls FastRouter, receives tool calls, executes them through Scalekit, and feeds results back — repeating until FastRouter returns a final answer. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * Scalekit account with AgentKit enabled — [create one at app.scalekit.com](https://app.scalekit.com) * At least one AgentKit connection configured (Gmail, GitHub, or Slack) * FastRouter account and API key — [sign up at fastrouter.ai](https://fastrouter.ai) * Node.js 20 or later * For Python code examples: `pip install google-protobuf` (required for tool schema deserialization) ## Clone and run the sample [Section titled “Clone and run the sample”](#clone-and-run-the-sample) 1. **Clone the repository and install dependencies.** ```sh git clone https://github.com/scalekit-developers/fastrouter-scalekit-demo cd fastrouter-scalekit-demo npm install ``` 2. **Copy the example environment file and fill in your credentials.** ```sh cp .env.example .env ``` Open `.env` and set these values: ```sh # Scalekit — find these in your Scalekit dashboard under API Keys SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev SCALEKIT_CLIENT_ID=your_client_id SCALEKIT_CLIENT_SECRET=your_client_secret # The AgentKit connection to use — must match a connection name in your dashboard SCALEKIT_CONNECTION_NAME=gmail # FastRouter — find your API key at fastrouter.ai/dashboard FASTROUTER_API_KEY=sk-v1-... FASTROUTER_MODEL=openai/gpt-4o-mini ``` `SCALEKIT_CONNECTION_NAME` must match the exact connection name in your Scalekit dashboard under **AgentKit → Connections**. 3. **Run the agent.** ```sh npm start ``` 4. **Authorize the connection on first run.** The agent prints an authorization link if the connected account is not yet active: ```plaintext Authorization required. Open this link and complete the flow: https://your-env.scalekit.dev/magicLink/... Waiting for callback on http://localhost:3000/callback ... ``` Open the link in your browser and complete the OAuth flow. The agent detects the callback automatically and continues — no manual step required. After authorization, the agent loads tools, calls FastRouter, and prints a final answer: ```plaintext Connected account is now active. Loaded 17 scoped tools from Scalekit. Model requested 1 tool call(s). → Executing gmail_list_messages args: {"maxResults":5,"q":"is:unread"} Final answer: Here are your 5 most recent unread emails: ... ``` ## How the agent works [Section titled “How the agent works”](#how-the-agent-works) Three pieces connect FastRouter to Scalekit tools. ### B2B OAuth connects user accounts without custom token code [Section titled “B2B OAuth connects user accounts without custom token code”](#b2b-oauth-connects-user-accounts-without-custom-token-code) Scalekit handles the full OAuth flow. Your agent calls `getOrCreateConnectedAccount` to check whether the user’s account is already connected, then calls `getAuthorizationLink` to get an auth URL if it isn’t. * Node.js ```typescript 1 import crypto from 'node:crypto'; 2 import { ConnectorStatus } from '@scalekit-sdk/node'; 3 4 const connectionName = process.env.SCALEKIT_CONNECTION_NAME; 5 if (!connectionName) { 6 throw new Error('SCALEKIT_CONNECTION_NAME is required'); 7 } 8 9 const userVerifyUrl = 'http://localhost:3000/callback'; 10 11 // Generate a random state value and store it (e.g. in a secure cookie or session) 12 // to validate on the OAuth callback and prevent CSRF / account mix-up attacks. 13 const state = crypto.randomUUID(); 14 15 const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 16 connectionName, 17 identifier: 'user_123', 18 }); 19 20 // userVerifyUrl goes on the authorization link, not on the connected account 21 if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { 22 const { link } = await scalekit.actions.getAuthorizationLink({ 23 connectionName, 24 identifier: 'user_123', 25 userVerifyUrl, 26 state, 27 }); 28 // Show link to user, then wait for the browser redirect callback 29 } ``` * Python ```python 1 import os 2 import secrets 3 4 connection_name = os.environ["SCALEKIT_CONNECTION_NAME"] 5 user_verify_url = "http://localhost:3000/callback" 6 7 # Generate and store a state value (e.g. in a secure, HTTP-only cookie) for CSRF protection 8 state = secrets.token_urlsafe(32) 9 10 response = scalekit_client.actions.get_or_create_connected_account( 11 connection_name=connection_name, 12 identifier="user_123", 13 ) 14 15 # user_verify_url goes on the authorization link, not on the connected account 16 17 if response.connected_account.status != "ACTIVE": 18 link_resp = scalekit_client.actions.get_authorization_link( 19 connection_name=connection_name, 20 identifier="user_123", 21 user_verify_url=user_verify_url, 22 state=state, 23 ) 24 # Show link_resp.link to the user ``` `userVerifyUrl` is where Scalekit redirects the user’s browser after the OAuth flow completes (GET request with `auth_request_id` and `state` query parameters). The sample runs a minimal HTTP server on `localhost:3000` to catch that redirect, validate the `state` against the original value, extract the `auth_request_id`, and call `verifyConnectedAccountUser` to mark the account active: * Node.js ```typescript 1 import http from 'node:http'; 2 3 async function waitForCallback(port: number, expectedState: string): Promise { 4 return new Promise((resolve, reject) => { 5 const server = http.createServer((req, res) => { 6 const url = new URL(req.url ?? '/', `http://localhost:${port}`); 7 const authRequestId = url.searchParams.get('auth_request_id'); 8 const returnedState = url.searchParams.get('state'); 9 10 res.writeHead(200, { 'Content-Type': 'text/html' }); 11 res.end('

Authorization complete — return to your terminal.

'); 12 server.close(); 13 14 if (authRequestId && returnedState === expectedState) { 15 resolve(authRequestId); 16 } else { 17 reject(new Error('Invalid or missing auth_request_id or state in callback')); 18 } 19 }); 20 server.listen(port); 21 }); 22 } 23 24 const authRequestId = await waitForCallback(3000, state); 25 await scalekit.actions.verifyConnectedAccountUser({ 26 authRequestId, 27 identifier: 'user_123', 28 }); ``` * Python ```python # In your web framework callback handler (e.g. FastAPI): # 1. Validate that the "state" query param matches the value you stored earlier # 2. Then exchange the auth_request_id (never trust identity from the URL alone) result = scalekit_client.actions.verify_connected_account_user( auth_request_id=auth_request_id, identifier="user_123", ) # redirect to result.post_user_verify_redirect_url ``` Production callback endpoint In a production web app, replace `localhost:3000/callback` with your server’s callback endpoint. Scalekit redirects the browser to it with `auth_request_id` and `state` query params. Your handler must validate the state before calling `verifyConnectedAccountUser` to complete account activation. ### Tool discovery returns schemas in FastRouter’s expected format [Section titled “Tool discovery returns schemas in FastRouter’s expected format”](#tool-discovery-returns-schemas-in-fastrouters-expected-format) `listScopedTools` returns only the tools the connected account has permission to use. Map each tool’s `input_schema` to the `parameters` field FastRouter expects: ```typescript 1 const { tools } = await scalekit.tools.listScopedTools('user_123', { 2 filter: { connectionNames: [connectionName] }, 3 pageSize: 100, 4 }); 5 6 const fastRouterTools = tools 7 .map((t) => t.tool?.definition) 8 .filter((def): def is NonNullable => Boolean(def?.name)) 9 .map((def) => ({ 10 type: 'function' as const, 11 function: { 12 name: String(def.name), 13 description: String(def.description ?? ''), 14 parameters: def.input_schema ?? { type: 'object', properties: {} }, 15 }, 16 })); ``` FastRouter uses the same function-calling format as OpenAI. No additional schema transformation is needed. ### The agentic loop runs until the model stops requesting tools [Section titled “The agentic loop runs until the model stops requesting tools”](#the-agentic-loop-runs-until-the-model-stops-requesting-tools) Pass the tool list to FastRouter and execute each tool call through Scalekit until the model returns a response with no tool calls: ```typescript 1 const messages: OpenAI.ChatCompletionMessageParam[] = [ 2 { role: 'system', content: 'You are a helpful assistant. Use tools when they help. Do not invent tool results.' }, 3 { role: 'user', content: 'Fetch my last 5 unread emails and summarize them.' }, 4 ]; 5 6 for (let turn = 0; turn < 8; turn++) { 7 const response = await fastRouter.chat.completions.create({ 8 model: 'openai/gpt-4o-mini', 9 messages, 10 tools: fastRouterTools, 11 tool_choice: 'auto', 12 }); 13 14 const message = response.choices[0].message; 15 messages.push(message); 16 17 // No tool calls means a final answer 18 if (!message.tool_calls?.length) { 19 console.log(message.content); 20 break; 21 } 22 23 // Execute each tool call and append the result 24 for (const call of message.tool_calls) { 25 const result = await scalekit.actions.executeTool({ 26 toolName: call.function.name, 27 identifier: 'user_123', 28 connector: connectionName, 29 toolInput: JSON.parse(call.function.arguments), 30 }); 31 32 messages.push({ 33 role: 'tool', 34 tool_call_id: call.id, 35 content: JSON.stringify(result.data ?? {}), 36 }); 37 } 38 } ``` `executeTool` runs the tool server-side using the connected account’s stored OAuth tokens. Your agent never handles raw access tokens. ## Customize the agent [Section titled “Customize the agent”](#customize-the-agent) **Change the connection.** Set `SCALEKIT_CONNECTION_NAME` to any connection configured in your Scalekit dashboard: | Value | What it connects | | -------- | ----------------------------------- | | `gmail` | Gmail read/send | | `github` | Repositories, issues, pull requests | | `slack` | Channels, messages, users | **Change the model.** Set `FASTROUTER_MODEL` in `.env` to any model FastRouter supports. The agent uses the same code regardless of which model you choose. **Change the prompt.** Pass a prompt as a CLI argument to override the default: ```sh npm start "List all GitHub pull requests assigned to me" ``` Or set `USER_PROMPT` in `.env` to change the default. **Support multiple connections.** Call `listScopedTools` with multiple connection names to give the model tools from all of them at once: ```typescript const { tools } = await scalekit.tools.listScopedTools('user_123', { filter: { connectionNames: ['gmail', 'github', 'slack'] }, }); ``` ## Next steps [Section titled “Next steps”](#next-steps) * **[Scalekit overview](/agentkit/connections)** — Understand connected accounts, tool discovery, and tool execution in depth. * **[AgentKit connections](/agentkit/connectors)** — Set up Gmail, GitHub, Slack, and other connections. * **[OpenAI example](/agentkit/examples/openai)** — See the same tool-calling pattern with OpenAI directly. * **[LiteLLM inbox triage cookbook](/cookbooks/litellm-agentkit-inbox-triage)** — A more complex multi-connection agent with a web approval interface. --- # DOCUMENT BOUNDARY --- # Implement passwordless auth in Next.js 15 > Add magic link and OTP authentication to your Next.js application using Scalekit's headless API. Next.js 15’s App Router expects authentication to be server-first: tokens generated on the server, verification happening in Route Handlers or Server Actions, and sessions stored in HttpOnly cookies. If you’re building passwordless authentication (magic links + OTP), traditional client-side SDKs won’t work properly with this model. This cookbook shows you how to implement passwordless auth that works natively with Next.js 15’s architecture using Scalekit’s headless API. ## The problem [Section titled “The problem”](#the-problem) You want passwordless authentication in Next.js 15 but face these challenges: * **Client-side SDKs break App Router patterns** - They expect browser-side token handling, which violates server-first principles * **Vendor UIs don’t match your design** - Pre-built login pages force you to compromise on branding * **DIY is complex** - Building secure token generation, email delivery, verification, and session management from scratch is a significant lift * **Cross-device failures** - Magic links often break when users switch devices or email clients strip parameters ## Who needs this [Section titled “Who needs this”](#who-needs-this) This cookbook is for you if: * ✅ You’re building a Next.js 15 application using App Router * ✅ You want passwordless authentication (magic links, OTP, or both) * ✅ You need full control over your login UI and email design * ✅ You don’t want to migrate your existing user database * ✅ You require server-side security for compliance You **don’t** need this if: * ❌ You’re happy with vendor-hosted login pages * ❌ You’re using Next.js Pages Router (not App Router) * ❌ You prefer traditional username/password authentication ## The solution [Section titled “The solution”](#the-solution) Scalekit’s passwordless API provides three server-side methods that integrate directly with Next.js 15’s architecture: 1. **`sendPasswordlessEmail()`** - Generates and sends magic link/OTP to user’s email 2. **`verifyPasswordlessEmail()`** - Validates the token/code and returns verified identity 3. **`resendPasswordlessEmail()`** - Issues a fresh credential if the first expires All security logic stays server-side, works with Server Actions and Route Handlers, and integrates with Edge Middleware for route protection. ## Implementation [Section titled “Implementation”](#implementation) ### 1. Configure Scalekit dashboard [Section titled “1. Configure Scalekit dashboard”](#1-configure-scalekit-dashboard) Enable passwordless authentication in your [Scalekit dashboard](https://app.scalekit.com/): 1. Navigate to **Authentication → Passwordless** 2. Select **Magic Link + Verification Code** for maximum reliability 3. Set **Expiry Period** (e.g., 600 seconds for 10-minute lifetime) 4. Enable **Enforce same browser origin** to prevent link hijacking 5. (Optional) Enable **Regenerate credentials on resend** to invalidate old links ### 2. Install dependencies and configure environment [Section titled “2. Install dependencies and configure environment”](#2-install-dependencies-and-configure-environment) ```bash npm install @scalekit-sdk/node jsonwebtoken ``` Create `.env.local`: ```bash SCALEKIT_ENVIRONMENT_URL=env_xxxx SCALEKIT_CLIENT_ID=skc_xxx SCALEKIT_CLIENT_SECRET=your_secret APP_URL=http://localhost:3000 JWT_SECRET=your_jwt_secret ``` ### 3. Create session management utilities [Section titled “3. Create session management utilities”](#3-create-session-management-utilities) Create `lib/session-store.ts` to handle server-side session creation: ```typescript 1 import jwt from 'jsonwebtoken'; 2 import { cookies } from 'next/headers'; 3 4 const COOKIE = 'session'; 5 const SECRET = process.env.JWT_SECRET!; 6 7 export function createSession(email: string) { 8 const token = jwt.sign({ email }, SECRET, { expiresIn: '7d' }); 9 cookies().set(COOKIE, token, { 10 httpOnly: true, 11 secure: process.env.NODE_ENV === 'production', 12 sameSite: 'lax', 13 path: '/', 14 maxAge: 60 * 60 * 24 * 7, 15 }); 16 } 17 18 export function readSessionEmail(): string | null { 19 const token = cookies().get(COOKIE)?.value; 20 if (!token) return null; 21 22 try { 23 const decoded = jwt.verify(token, SECRET) as { email: string }; 24 return decoded.email; 25 } catch { 26 return null; 27 } 28 } 29 30 export function clearSession() { 31 cookies().delete(COOKIE); 32 } ``` ### 4. Create send email endpoint [Section titled “4. Create send email endpoint”](#4-create-send-email-endpoint) Create `app/api/auth/send-passwordless/route.ts`: ```typescript 1 import Scalekit from '@scalekit-sdk/node'; 2 import { NextRequest, NextResponse } from 'next/server'; 3 4 const scalekit = new Scalekit( 5 process.env.SCALEKIT_ENVIRONMENT_URL!, 6 process.env.SCALEKIT_CLIENT_ID!, 7 process.env.SCALEKIT_CLIENT_SECRET! 8 ); 9 10 export async function POST(req: NextRequest) { 11 const { email } = await req.json(); 12 13 try { 14 const response = await scalekit.passwordless.sendPasswordlessEmail(email, { 15 template: 'SIGNIN', 16 expiresIn: 600, // 10 minutes 17 state: crypto.randomUUID(), 18 magiclinkAuthUri: `${process.env.APP_URL}/api/auth/verify`, 19 }); 20 21 return NextResponse.json({ 22 authRequestId: response.authRequestId, 23 expiresAt: response.expiresAt, 24 }); 25 } catch (error) { 26 return NextResponse.json( 27 { error: 'Failed to send email' }, 28 { status: 500 } 29 ); 30 } 31 } ``` ### 5. Create verification endpoint [Section titled “5. Create verification endpoint”](#5-create-verification-endpoint) Create `app/api/auth/verify/route.ts` with both GET (magic link) and POST (OTP) handlers: ```typescript 1 import Scalekit from '@scalekit-sdk/node'; 2 import { NextRequest, NextResponse } from 'next/server'; 3 import { createSession } from '@/lib/session-store'; 4 5 const scalekit = new Scalekit( 6 process.env.SCALEKIT_ENVIRONMENT_URL!, 7 process.env.SCALEKIT_CLIENT_ID!, 8 process.env.SCALEKIT_CLIENT_SECRET! 9 ); 10 11 // Magic link verification 12 export async function GET(req: NextRequest) { 13 const url = new URL(req.url); 14 const linkToken = url.searchParams.get('link_token'); 15 const authRequestId = url.searchParams.get('auth_request_id') ?? undefined; 16 17 if (!linkToken) { 18 return NextResponse.redirect( 19 new URL('/login?error=missing_token', req.url) 20 ); 21 } 22 23 try { 24 const verified = await scalekit.passwordless.verifyPasswordlessEmail( 25 { linkToken }, 26 authRequestId 27 ); 28 29 createSession(verified.email); 30 return NextResponse.redirect(new URL('/dashboard', req.url)); 31 } catch { 32 return NextResponse.redirect( 33 new URL('/login?error=verification_failed', req.url) 34 ); 35 } 36 } 37 38 // OTP verification 39 export async function POST(req: NextRequest) { 40 const { code, authRequestId } = await req.json(); 41 42 if (!code || !authRequestId) { 43 return NextResponse.json( 44 { error: 'Missing required fields' }, 45 { status: 400 } 46 ); 47 } 48 49 try { 50 const verified = await scalekit.passwordless.verifyPasswordlessEmail( 51 { code }, 52 authRequestId 53 ); 54 55 createSession(verified.email); 56 return NextResponse.json({ success: true }); 57 } catch { 58 return NextResponse.json( 59 { error: 'Invalid or expired code' }, 60 { status: 400 } 61 ); 62 } 63 } ``` ### 6. Add resend endpoint [Section titled “6. Add resend endpoint”](#6-add-resend-endpoint) Create `app/api/auth/resend-passwordless/route.ts`: ```typescript 1 import Scalekit from '@scalekit-sdk/node'; 2 import { NextRequest, NextResponse } from 'next/server'; 3 4 const scalekit = new Scalekit( 5 process.env.SCALEKIT_ENVIRONMENT_URL!, 6 process.env.SCALEKIT_CLIENT_ID!, 7 process.env.SCALEKIT_CLIENT_SECRET! 8 ); 9 10 export async function POST(req: NextRequest) { 11 const { authRequestId } = await req.json(); 12 13 if (!authRequestId) { 14 return NextResponse.json( 15 { error: 'Missing authRequestId' }, 16 { status: 400 } 17 ); 18 } 19 20 try { 21 const response = await scalekit.passwordless.resendPasswordlessEmail( 22 authRequestId 23 ); 24 25 return NextResponse.json({ 26 authRequestId: response.authRequestId, 27 expiresAt: response.expiresAt, 28 }); 29 } catch { 30 return NextResponse.json( 31 { error: 'Resend failed' }, 32 { status: 400 } 33 ); 34 } 35 } ``` ### 7. Protect routes with middleware [Section titled “7. Protect routes with middleware”](#7-protect-routes-with-middleware) Create `middleware.ts` in your project root: ```typescript 1 import { NextRequest, NextResponse } from 'next/server'; 2 3 export function middleware(req: NextRequest) { 4 const protectedPath = req.nextUrl.pathname.startsWith('/dashboard'); 5 const hasSession = Boolean(req.cookies.get('session')?.value); 6 7 if (protectedPath && !hasSession) { 8 const url = new URL('/login', req.url); 9 url.searchParams.set('next', req.nextUrl.pathname); 10 return NextResponse.redirect(url); 11 } 12 13 return NextResponse.next(); 14 } 15 16 export const config = { 17 matcher: ['/dashboard/:path*'], 18 }; ``` ### 8. Build login UI (example) [Section titled “8. Build login UI (example)”](#8-build-login-ui-example) Create `app/login/page.tsx`: ```typescript 1 'use client'; 2 3 import { useState } from 'react'; 4 import { useRouter } from 'next/navigation'; 5 6 export default function LoginPage() { 7 const [email, setEmail] = useState(''); 8 const [authRequestId, setAuthRequestId] = useState(''); 9 const [showOtp, setShowOtp] = useState(false); 10 const [otp, setOtp] = useState(''); 11 const router = useRouter(); 12 13 async function handleSendEmail(e: React.FormEvent) { 14 e.preventDefault(); 15 16 const res = await fetch('/api/auth/send-passwordless', { 17 method: 'POST', 18 headers: { 'Content-Type': 'application/json' }, 19 body: JSON.stringify({ email }), 20 }); 21 22 const data = await res.json(); 23 setAuthRequestId(data.authRequestId); 24 setShowOtp(true); 25 } 26 27 async function handleVerifyOtp(e: React.FormEvent) { 28 e.preventDefault(); 29 30 const res = await fetch('/api/auth/verify', { 31 method: 'POST', 32 headers: { 'Content-Type': 'application/json' }, 33 body: JSON.stringify({ code: otp, authRequestId }), 34 }); 35 36 if (res.ok) { 37 router.push('/dashboard'); 38 } 39 } 40 41 return ( 42
43 {!showOtp ? ( 44
45 setEmail(e.target.value)} 49 placeholder="Enter your email" 50 required 51 /> 52 53
54 ) : ( 55
56

Check your email for a magic link or enter the code below:

57 setOtp(e.target.value)} 61 placeholder="Enter 6-digit code" 62 maxLength={6} 63 /> 64 65
66 )} 67
68 ); 69 } ``` ## Security features [Section titled “Security features”](#security-features) Scalekit enforces these protections automatically: * **Rate limiting**: 2 emails per minute per address, 5 OTP attempts per 10 minutes * **Short-lived tokens**: Configure expiry from 60 seconds to 1 hour * **Same-browser enforcement**: When enabled, links can only be verified from the originating browser * **HttpOnly sessions**: Tokens never touch client JavaScript ## Error handling [Section titled “Error handling”](#error-handling) Map Scalekit errors to user-friendly messages: ```typescript function getErrorMessage(error: string): string { if (error.includes('expired')) { return 'This link has expired. Request a new one.'; } if (error.includes('rate')) { return 'Too many attempts. Please try again later.'; } if (error.includes('invalid')) { return 'Invalid code. Please check and try again.'; } return 'Verification failed. Please try again.'; } ``` ## Production checklist [Section titled “Production checklist”](#production-checklist) Before deploying: * ✅ Set `secure: true` for session cookies (enforced automatically in production) * ✅ Configure production Scalekit credentials in environment variables * ✅ Verify dashboard settings match your security requirements * ✅ Test magic link + OTP flow on multiple email clients * ✅ Set up monitoring for authentication errors and rate limit hits * ✅ Configure custom email templates with your branding ## Complete example [Section titled “Complete example”](#complete-example) Full working code is available in the [Scalekit GitHub repository](https://github.com/scalekit-developers/blogops-app-examples/tree/main/nextjs-passwordless-auth). ## Why this approach works [Section titled “Why this approach works”](#why-this-approach-works) This implementation: * **Works natively with App Router** - All sensitive operations are server-side * **Maintains full UI control** - No vendor widgets or redirects to hosted pages * **Handles cross-device gracefully** - OTP fallback covers magic link failures * **Requires no user migration** - Works on top of your existing user store * **Stays secure by default** - HttpOnly cookies, server-only verification, automatic rate limiting ## Related resources [Section titled “Related resources”](#related-resources) * [Scalekit Passwordless Auth Documentation](https://docs.scalekit.com/passwordless/) * [Next.js 15 App Router Documentation](https://nextjs.org/docs/app) * [Full tutorial blog post](https://www.scalekit.com/blog/passwordless-authentication-next-js) --- # DOCUMENT BOUNDARY --- # Configuring JWT Validation Timeouts in Spring Boot 4.0+ > Fix connection timeout errors when validating Scalekit JWT tokens in Spring Boot 4.0.0 and later versions. If you’re using Spring Boot 4.0.0 or later and experiencing connection timeout errors when validating JWT tokens from Scalekit, you’ll need to explicitly configure timeout values. This is a known issue affecting Spring Security’s OAuth2 resource server configuration. ## The problem [Section titled “The problem”](#the-problem) Your Spring Boot application successfully configures the `issuer-uri` for JWT validation: ```yaml spring: security: oauth2: resourceserver: jwt: issuer-uri: https://auth.scalekit.com ``` But authentication fails with timeout errors like: ```plaintext java.net.SocketTimeoutException: Connect timed out at org.springframework.security.oauth2.jwt.JwtDecoders.fromIssuerLocation ``` ## Why this happens [Section titled “Why this happens”](#why-this-happens) Starting with Spring Boot 4.0.0, Spring Security changed how it handles HTTP connections during JWT validation: * **Before 4.0.0**: Spring used default system timeouts (often much longer) * **After 4.0.0**: Spring enforces strict, short timeout defaults that can be too aggressive for production When your application starts or validates its first JWT token, Spring Security: 1. Fetches the OpenID Connect discovery document from `issuer-uri` 2. Retrieves the JWKS (JSON Web Key Set) to verify token signatures 3. Caches these for future validations If these initial requests timeout, authentication fails completely. ## Who needs this fix [Section titled “Who needs this fix”](#who-needs-this-fix) This issue specifically affects: * ✅ Spring Boot applications version **4.0.0 or later** * ✅ Using `issuer-uri` for JWT validation (not manual `jwk-set-uri`) * ✅ Production environments with network latency or firewall rules * ✅ Applications experiencing intermittent authentication failures You **don’t** need this if: * ❌ Using Spring Boot 3.x or earlier * ❌ Manually configuring `jwk-set-uri` instead of `issuer-uri` * ❌ Already have custom `RestTemplate` or `WebClient` configurations ## The solution [Section titled “The solution”](#the-solution) For Spring Security servlet resource servers, there are no properties to configure JWT discovery/JWKS HTTP timeouts. Use a custom `JwtDecoder` bean with `RestOperations` (for example, `RestTemplate`) and explicit timeout values: ```java 1 import org.springframework.context.annotation.Bean; 2 import org.springframework.context.annotation.Configuration; 3 import org.springframework.http.client.SimpleClientHttpRequestFactory; 4 import org.springframework.security.oauth2.jwt.JwtDecoder; 5 import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; 6 import org.springframework.web.client.RestTemplate; 7 8 @Configuration 9 public class SecurityConfig { 10 11 @Bean 12 public JwtDecoder jwtDecoder() { 13 // Create a RestTemplate with custom timeouts 14 SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); 15 factory.setConnectTimeout(10000); // 10 seconds 16 factory.setReadTimeout(10000); // 10 seconds 17 18 RestTemplate restTemplate = new RestTemplate(factory); 19 20 // Use the custom RestTemplate for JWT validation 21 return NimbusJwtDecoder 22 .withIssuerLocation("https://auth.scalekit.com") 23 .restOperations(restTemplate) 24 .build(); 25 } 26 } ``` This gives you: * Full control over HTTP client configuration * Ability to add custom headers or interceptors * Environment-specific timeout tuning (development: 5000ms, production: 10000–15000ms) ## Verifying the fix [Section titled “Verifying the fix”](#verifying-the-fix) After applying the configuration: 1. **Restart your application** - Spring Security initializes the JWT decoder on startup 2. **Test authentication** - Make a request with a valid Scalekit JWT token 3. **Check logs** - You should see successful JWKS retrieval: ```plaintext DEBUG o.s.security.oauth2.jwt.JwtDecoder - Retrieved JWKS from https://auth.scalekit.com/.well-known/jwks.json ``` If you still see timeout errors: * Verify network connectivity to `auth.scalekit.com` * Check firewall rules allowing outbound HTTPS * Increase timeout values if your network has high latency ## When to use standard Spring Security instead [Section titled “When to use standard Spring Security instead”](#when-to-use-standard-spring-security-instead) This cookbook addresses a specific Spring Boot 4.0+ timeout issue. For general JWT validation setup: * Follow the [Spring Security OAuth2 Resource Server documentation](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html) * Use Scalekit’s standard Java SDK for token validation if not using Spring Security * Consider the default `issuer-uri` configuration if you’re not experiencing timeouts ## Related resources [Section titled “Related resources”](#related-resources) * [Spring Security OAuth2 Resource Server - JWT Timeouts](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html#oauth2resourceserver-jwt-timeouts) * [Scalekit API reference](/apis/#tag/sessions) * [Spring Boot 4.0 Release Notes](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Release-Notes) --- # DOCUMENT BOUNDARY --- # Trace AgentKit tool calls in LangSmith > Add LangSmith observability to a LangChain agent that uses Scalekit AgentKit tools for Gmail, Slack, GitHub, and the AgentKit connector catalog. When you hand an LLM a set of tools — Gmail, Slack, GitHub, calendar — you need to see what happened. Which tool was called, with what arguments, what came back, and how long it took. Without that visibility, debugging a misbehaving agent means guessing. [LangSmith](https://smith.langchain.com) provides that visibility for LangChain agents. Scalekit AgentKit returns native LangChain `StructuredTool` objects, which means LangSmith traces them automatically — no wrapper code, no custom callbacks. Set two environment variables and every tool call shows up as a span in your trace. This recipe builds a Python agent that fetches Gmail messages through AgentKit and traces the entire run in LangSmith. The same pattern works with any of Scalekit’s 614 connectors. ## What you are building [Section titled “What you are building”](#what-you-are-building) * **A LangChain agent** that uses Scalekit AgentKit tools to read Gmail. * **LangSmith tracing** that captures every LLM call, tool invocation, input/output, and latency as spans in a trace. * **A verification step** confirming traces appear in the LangSmith dashboard. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Scalekit account at [app.scalekit.com](https://app.scalekit.com) with API credentials (**Settings → API Credentials**). * A **Gmail** connection configured under **AgentKit** > **Connections**. See [Configure a connection](/agentkit/connections/). * A [LangSmith account](https://smith.langchain.com) and API key from **Settings → API Keys**. * An OpenAI API key, or a LiteLLM gateway URL with a virtual key. * **Python 3.10+** and **pip** or **uv**. 1. ## Install dependencies [Section titled “Install dependencies”](#install-dependencies) Terminal ```bash pip install scalekit-sdk-python langchain-openai langsmith python-dotenv ``` `scalekit-sdk-python` includes the LangChain adapter. `langsmith` is the tracing client — importing it is enough for LangSmith to pick up traces when the environment variables are set. 2. ## Set environment variables [Section titled “Set environment variables”](#set-environment-variables) Create a `.env` file at the project root: .env ```bash 1 # Scalekit — from app.scalekit.com → Settings → API Credentials 2 # Threat: leaked credentials grant full API access to your Scalekit environment. 3 # Never commit this file to version control; add .env to .gitignore. 4 SCALEKIT_CLIENT_ID=skc_your_client_id 5 SCALEKIT_CLIENT_SECRET=skcs_your_client_secret 6 SCALEKIT_ENVIRONMENT_URL=https://your-subdomain.scalekit.dev 7 8 # LangSmith — from smith.langchain.com → Settings → API Keys 9 # Threat: exposed API key allows unauthorized trace reads and writes. 10 LANGCHAIN_TRACING_V2=true 11 LANGCHAIN_API_KEY=lsv2_your_langsmith_api_key 12 LANGCHAIN_PROJECT=scalekit-agentkit-traces 13 14 # LLM — OpenAI directly, or through a LiteLLM gateway 15 # Threat: exposed key allows unauthorized model usage billed to your account. 16 OPENAI_API_KEY=sk-your-openai-key ``` | Variable | Purpose | | ---------------------- | ------------------------------------------------------------ | | `LANGCHAIN_TRACING_V2` | Must be `true` to enable tracing | | `LANGCHAIN_API_KEY` | Your LangSmith API key (starts with `lsv2_`) | | `LANGCHAIN_PROJECT` | Project name in LangSmith — auto-created if it doesn’t exist | Using a LiteLLM gateway? Replace `OPENAI_API_KEY` with `LITELLM_BASE_URL` and `LITELLM_API_KEY`, then pass them to `ChatOpenAI(openai_api_base=..., openai_api_key=...)`. The tracing behavior is identical — LangSmith traces the LangChain layer, not the transport. 3. ## Connect a user to Gmail [Section titled “Connect a user to Gmail”](#connect-a-user-to-gmail) Initialize the Scalekit client and ensure the user has an active Gmail connection: langsmith_tracing.py ```python 1 import os 2 from dotenv import load_dotenv 3 4 load_dotenv() 5 6 import scalekit.client 7 8 scalekit_client = scalekit.client.ScalekitClient( 9 client_id=os.getenv("SCALEKIT_CLIENT_ID"), 10 client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), 11 env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"), 12 ) 13 actions = scalekit_client.actions 14 15 IDENTIFIER = "user_123" 16 17 response = actions.get_or_create_connected_account( 18 connection_name="gmail", 19 identifier=IDENTIFIER, 20 ) 21 if response.connected_account.status != "ACTIVE": 22 link = actions.get_authorization_link( 23 connection_name="gmail", 24 identifier=IDENTIFIER, 25 ) 26 print("Authorize Gmail:", link.link) 27 input("Press Enter after authorizing...") 28 else: 29 print(f"✅ Gmail connected for {IDENTIFIER}") ``` `get_or_create_connected_account` returns an existing session if one exists. If the user hasn’t authorized yet, `get_authorization_link` returns a URL the user opens in a browser. Scalekit handles the full OAuth exchange, validates the redirect callback, and stores the token. Your application never sees the `client_secret` used in the token exchange — Scalekit manages that server-side, which prevents credential leakage from frontend or agent code. 4. ## Load tools and run the agent [Section titled “Load tools and run the agent”](#load-tools-and-run-the-agent) Python-only recipe The Scalekit LangChain adapter (`actions.langchain.get_tools()`) is Python-specific because LangChain’s `StructuredTool` is a Python class. If you use the Node.js SDK, call `actions.executeTool()` directly and trace with your framework’s own observability. AgentKit is available in the Python and Node.js SDKs. `actions.langchain.get_tools()` returns a list of `StructuredTool` objects. Bind them to a model and run a standard tool-calling loop: langsmith_tracing.py ```python 1 from langchain_core.messages import HumanMessage, ToolMessage 2 from langchain_openai import ChatOpenAI 3 4 tools = actions.langchain.get_tools( 5 identifier=IDENTIFIER, 6 connection_names=["gmail"], 7 ) 8 tool_map = {t.name: t for t in tools} 9 print(f"✅ Loaded {len(tools)} LangChain tools: {[t.name for t in tools[:5]]}") 10 11 llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) 12 messages = [HumanMessage("Fetch my last 3 unread emails and summarize them")] 13 14 while True: 15 response = llm.invoke(messages) 16 messages.append(response) 17 if not response.tool_calls: 18 print(response.content) 19 break 20 for tc in response.tool_calls: 21 print(f" 🔧 Tool call: {tc['name']}") 22 result = tool_map[tc["name"]].invoke(tc["args"]) 23 messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"])) ``` There is no tracing-specific code here. Because `LANGCHAIN_TRACING_V2=true` is set, LangSmith automatically instruments every `invoke` call — LLM requests, tool calls, and the full message chain. 5. ## Run and verify [Section titled “Run and verify”](#run-and-verify) Terminal ```bash python langsmith_tracing.py ``` Expected output (the first line appears only if the account is already `ACTIVE`; on first run you will see the authorization URL instead): Terminal ```text ✅ Gmail connected for user_123 ✅ Loaded 8 LangChain tools: ['gmail_fetch_mails', 'gmail_send_message', ...] 🔧 Tool call: gmail_fetch_mails Here are your 3 most recent unread emails: ... ``` Open [LangSmith](https://smith.langchain.com), select the **scalekit-agentkit-traces** project, and click the latest trace. You should see: * A **ChatOpenAI** span for the LLM call * A **gmail_fetch_mails** tool span showing the input arguments and the structured response from Gmail * Latency, token counts, and the full message chain ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) Traces are not appearing in LangSmith Either `LANGCHAIN_TRACING_V2` is not `true` or `LANGCHAIN_API_KEY` is missing from the environment. **Solution:** Confirm both variables are set *before* importing any LangChain module. If you are using a `.env` file, call `load_dotenv()` at the top of the script before any other imports. You can verify with: Terminal check ```python import os print(os.getenv("LANGCHAIN_TRACING_V2")) # Should print "true" print(os.getenv("LANGCHAIN_API_KEY")) # Should print "lsv2_..." ``` Connected account stays in PENDING_AUTH The user did not complete the OAuth flow in the browser. AgentKit waits for the user to authorize through the URL returned by `get_authorization_link`. **Solution:** Open the printed URL in a browser, complete the Google OAuth consent, and return to the terminal. The connected account status updates to `ACTIVE` after a successful callback. Tool call fails with `resource not found` The connection name in code does not match the connection name in the Scalekit dashboard, or the connected account is not active. **Solution:** Open **AgentKit** > **Connections** in the dashboard. Verify the connection name matches exactly (case-sensitive). Then check that the connected account for your identifier shows **ACTIVE** status. Traces appear but tool spans are missing The tools were not bound to the LLM via `.bind_tools()`, so the model is generating text instead of structured tool calls. **Solution:** Ensure you call `llm = ChatOpenAI(...).bind_tools(tools)` and that the `tools` list is not empty. Print `len(tools)` after `get_tools()` to confirm tools loaded. ## Production notes [Section titled “Production notes”](#production-notes) **Token refresh is automatic.** Scalekit stores OAuth tokens per user per connector and refreshes them before expiry. Your agent code never handles refresh tokens directly. **Add multiple connectors.** Pass additional connection names to `get_tools()` to load tools from Gmail, Slack, GitHub, and others in a single call. LangSmith traces all of them identically. **Trace metadata.** Use LangSmith’s `@traceable` decorator or `with_config({"tags": [...]})` to add custom tags, metadata, or run names to your traces for filtering. **Cost tracking.** LangSmith captures token counts per LLM call. Combined with tool call traces, you get full-cost visibility per agent run. ## Next steps [Section titled “Next steps”](#next-steps) * [Configure more AgentKit connectors](/agentkit/connectors/) — add Slack, GitHub, Salesforce, or any of the 614 connectors alongside Gmail. * [Virtual MCP servers](/agentkit/mcp/overview/) — serve AgentKit tools over MCP for use with any MCP-compatible client. * [LangSmith evaluation](https://docs.smith.langchain.com/evaluation) — score agent responses and tool usage across test datasets. * [LangSmith trace filtering](https://docs.smith.langchain.com/how_to_guides/tracing/filter_traces_in_application) — filter traces by metadata, tags, latency, or error status. ## Related resources [Section titled “Related resources”](#related-resources) | Topic | Link | | ------------------------- | --------------------------------------------------------------------------------- | | AgentKit overview | [Overview](/agentkit/overview/) | | LangChain framework guide | [LangChain](/agentkit/examples/langchain/) | | Connections | [Configure a connection](/agentkit/connections/) | | Connected accounts | [Manage connected accounts](/agentkit/connected-accounts/) | | Sample repository | [agent-auth-examples](https://github.com/scalekit-developers/agent-auth-examples) | | LangSmith docs | [docs.smith.langchain.com](https://docs.smith.langchain.com) | --- # DOCUMENT BOUNDARY --- # Triage a Gmail inbox with AgentKit and the LiteLLM gateway > Node.js inbox triage agent: classify Gmail threads, route to GitHub repos, draft issues and replies via LiteLLM, and approve before any side effects. Build an automated inbox triage agent that reads your Gmail, classifies each thread, routes it to the right GitHub repository, and notifies Slack — then waits for your approval before creating issues or sending replies. This Node.js sample uses **Scalekit AgentKit** for OAuth tool execution (Gmail, GitHub, Slack) and a **LiteLLM gateway** for model-per-stage routing. The only LiteLLM-specific config is `LITELLM_BASE_URL` and a virtual API key from the dashboard. The sample repository is **[litellm-agentkit-inbox-triage](https://github.com/scalekit-developers/litellm-agentkit-inbox-triage)** on GitHub. ## What you are building [Section titled “What you are building”](#what-you-are-building) * **Gmail ingestion** — Poll for new threads using AgentKit-executed Gmail tools. A SQLite cursor prevents duplicate processing. * **Model-per-stage routing** — Each stage (`classify`, `research`, `tiebreak`, `draft`) calls the LiteLLM gateway with a different model name. Stage-to-model assignments live in `routing.yaml` at the repo root. * **Deterministic GitHub routing** — Keyword rules in `routing.yaml` pick a target repository; an optional LLM tie-breaker resolves ties. * **Research loop** — A small tool-calling loop searches related GitHub issues through AgentKit. * **Slack notification** — Posts a summary with a link to the pending decision. * **Human approval** — A localhost dashboard lists proposals. **Approve** creates the GitHub issue, sends the Gmail reply, and updates Slack. **Reject** discards without side effects. ## Automated triage pipeline [Section titled “Automated triage pipeline”](#automated-triage-pipeline) New Gmail threads flow through AgentKit into a multi-stage LiteLLM pipeline, then land in SQLite as pending proposals. ![Inbox triage pipeline: AgentKit reads Gmail, LiteLLM classifies each email, and routing rules send it to research or other steps](/d2/docs/cookbooks/litellm-agentkit-inbox-triage-0.svg) ## Human approval loop [Section titled “Human approval loop”](#human-approval-loop) Proposals wait in SQLite until you review them from the dashboard. ![Human approval loop: a proposal is stored, Slack is notified, and approving creates the issue while rejecting has no side effects](/d2/docs/cookbooks/litellm-agentkit-inbox-triage-1.svg) ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Scalekit account at [app.scalekit.com](https://app.scalekit.com). * Ability to create **AgentKit connections** for **Gmail**, **GitHub**, and **Slack**. Connection **names** must match what you put in `.env` (see [Configure a connection](/agentkit/connections/)). * A **virtual LiteLLM API key** from your LiteLLM dashboard. A small spend cap of roughly two US dollars covers a handful of test threads. * **Node.js 24 or newer** and **npm**. * An **interactive terminal** — the sample prints authorization links and waits for Enter after each connector. This recipe does not cover headless CI. 1. ## Clone the sample [Section titled “Clone the sample”](#clone-the-sample) ```bash git clone https://github.com/scalekit-developers/litellm-agentkit-inbox-triage.git cd litellm-agentkit-inbox-triage ``` 2. ## Configure AgentKit connections [Section titled “Configure AgentKit connections”](#configure-agentkit-connections) 1. Open [app.scalekit.com](https://app.scalekit.com) → **AgentKit** → **Connections** → **Create Connection** for **Gmail**, **GitHub**, and **Slack**. 2. Copy each **Connection name** exactly as shown in the dashboard into `GMAIL_CONNECTION_NAME`, `GITHUB_CONNECTION_NAME`, and `SLACK_CONNECTION_NAME` in your `.env` file. 3. For **GitHub**, confirm the connection includes the **`repo`** OAuth scope (needed to create issues and search across repositories). Check **AgentKit → Connections → GitHub → Scopes** in the dashboard. See [Configure scopes](/agentkit/connections/#configure-scopes) and the [GitHub connector](/agentkit/connectors/github/). 4. For **Gmail** and **Slack**, follow the dashboard wizard. If your workspace restricts OAuth apps, see the connector docs: [Gmail](/agentkit/connectors/gmail/), [Slack](/agentkit/connectors/slack/). Dashboard only loads after all three connectors are active The sample calls `setupConnectors` **before** it binds the Express dashboard. You will **not** reach `http://localhost:3000` until Gmail, GitHub, and Slack each show **connector active** in the logs. 3. ## Create a LiteLLM virtual key and verify the gateway [Section titled “Create a LiteLLM virtual key and verify the gateway”](#create-a-litellm-virtual-key-and-verify-the-gateway) Open your LiteLLM dashboard (the LiteLLM proxy’s admin UI) and create a **virtual API key** (optionally set a small budget cap for evaluation). Verify the gateway responds before continuing (load your `.env` first with `set -a && source .env && set +a`): ```bash curl -H "Authorization: Bearer $LITELLM_API_KEY" \ "$LITELLM_BASE_URL/v1/models" ``` Align `routing.yaml` → `models:` with the model IDs returned by that endpoint. 4. ## Configure and run the sample [Section titled “Configure and run the sample”](#configure-and-run-the-sample) Set these variables in `.env` before running: | Variable | Where to find it | | -------------------------- | ---------------------------------------------------- | | `SCALEKIT_ENVIRONMENT_URL` | Dashboard → **Settings** → Environment URL | | `SCALEKIT_CLIENT_ID` | Dashboard → **API Credentials** | | `SCALEKIT_CLIENT_SECRET` | Dashboard → **API Credentials** | | `GMAIL_CONNECTION_NAME` | Dashboard → **AgentKit → Connections** (exact label) | | `GITHUB_CONNECTION_NAME` | Same | | `SLACK_CONNECTION_NAME` | Same | | `LITELLM_BASE_URL` | Your LiteLLM gateway’s base URL | | `LITELLM_API_KEY` | LiteLLM dashboard → virtual key value | ```bash cp .env.example .env # Fill in the variables above npm install npm run dev ``` Complete each printed **authorization URL** in the browser, then press **Enter** in the terminal after each connector. When you see **All connectors active** and **dashboard listening on `http://localhost:3000`**, send a test email to the connected Gmail account. Within roughly one poll interval (default **5 seconds**), a proposal appears in the dashboard. 5. ## Approve or reject [Section titled “Approve or reject”](#approve-or-reject) Open **`http://localhost:3000`**. Review the classification, routed repository, related issues, and drafts. **Approve** runs GitHub issue creation, sends the Gmail reply, and updates Slack. **Reject** leaves external systems unchanged. 6. ## Extend the sample [Section titled “Extend the sample”](#extend-the-sample) To add routing targets or swap models per stage, edit `routing.yaml` — each entry maps keyword rules to a GitHub repository and assigns a model name to each pipeline stage. To add connectors, follow the [AgentKit connections guide](/agentkit/connections/) and add the new connection name to `.env`. ## Related resources [Section titled “Related resources”](#related-resources) | Topic | Link | | ----------------------------- | --------------------------------------------------------------- | | AgentKit overview | [Overview](/agentkit/overview/) | | Connections | [Configure a connection](/agentkit/connections/) | | Authorization links | [Authorize a user](/agentkit/tools/authorize/) | | Connected accounts | [Manage connected accounts](/agentkit/connected-accounts/) | | LiteLLM virtual keys | [Virtual keys](https://docs.litellm.ai/docs/proxy/virtual_keys) | | LiteLLM model routing | [Router](https://docs.litellm.ai/docs/routing) | | LiteLLM OpenAI-compatible API | [Proxy usage](https://docs.litellm.ai/docs/proxy/user_keys) | ## Common scenarios [Section titled “Common scenarios”](#common-scenarios) Why am I seeing random tool failures or `connection not found` errors? The `*_CONNECTION_NAME` variables in `.env` must match the connection labels exactly as shown in the dashboard — including capitalization and spacing. **Solution:** Open **AgentKit → Connections** in the dashboard, copy each connection name exactly, and paste it into `GMAIL_CONNECTION_NAME`, `GITHUB_CONNECTION_NAME`, and `SLACK_CONNECTION_NAME`. Why is GitHub returning a `403` or permission error? The GitHub AgentKit connection is missing the `repo` OAuth scope, which is required to create issues and search across repositories. **Solution:** In the dashboard, go to **AgentKit → Connections → GitHub → Scopes** and confirm `repo` is included. Re-authorize the connection if you need to add it. Why am I seeing `unknown model` errors from LiteLLM? A model name in `routing.yaml` is not available on your LiteLLM gateway instance. **Solution:** Run the following to list available models, then update `routing.yaml` → `models:` to match: ```bash curl -H "Authorization: Bearer $LITELLM_API_KEY" \ "$LITELLM_BASE_URL/v1/models" ``` Why isn’t the dashboard loading at localhost:3000? The sample binds the dashboard only after all three connectors finish authorization. If any connector step was skipped or the terminal is still waiting for Enter, the dashboard won’t start. **Solution:** Check the terminal output — the sample prints an authorization URL for each connector and waits for you to press Enter after completing it in the browser. For deeper debugging patterns, see [Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/). --- # DOCUMENT BOUNDARY --- # Build a LiveKit voice agent with Scalekit AgentKit tools > Give a LiveKit voice agent secure access to Google Calendar and the AgentKit connector catalog — no token ever reaches the browser or the LLM. A voice agent that answers “what’s on my calendar?” needs a Google OAuth token scoped to that specific user. [LiveKit](https://livekit.io) handles the realtime voice pipeline — speech-to-text, the LLM turn, text-to-speech, turn detection — but it has no built-in concept of per-user third-party credentials. Wire that up yourself and you’re building a token vault, a refresh cycle, and a way to keep both out of reach of the browser and the LLM, before you’ve written a single line of agent logic. Scalekit AgentKit removes that layer. It stores one OAuth session per connector per user and exposes a single `executeTool` call that runs any of 30,000+ connector tools on that user’s behalf. This cookbook connects a LiveKit Node.js agent to AgentKit directly — no MCP server, no separate auth service — and shows the one non-obvious part: getting the *identity* of the person talking to the agent from the browser to the worker process without ever exposing a credential. ## What you are building [Section titled “What you are building”](#what-you-are-building) * **A Next.js route** that dispatches a LiveKit agent into a room and mints a browser access token, carrying a Scalekit connection identifier through LiveKit’s dispatch `metadata` — the only channel between the two. * **A standalone LiveKit agent worker** (a separate Node process, not a Next.js route) that reads that identifier from `ctx.job.metadata` and calls `scalekit.actions.executeTool()` directly when the LLM decides to check the calendar. * **A pattern that generalizes**: swap `googlecalendar_list_events` for any AgentKit tool name and the identity-passing mechanism stays identical. The complete, working source (including the Next.js UI, both API routes, and the agent) is in [livekit-scalekit-voice-agent](https://github.com/scalekit-developers/livekit-scalekit-voice-agent). ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A [LiveKit Cloud](https://cloud.livekit.io) project — copy `LIVEKIT_URL`, `LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET` from **Settings → Keys**. No `lk` CLI login is required; the app and the agent both read these three values from the environment. * A Scalekit account with **AgentKit** enabled and a `googlecalendar` connection that shows as **Active** for the identifier you’ll test with. See [Configure a connection](/agentkit/connections/). * **Node.js 20.11+** — the agent worker uses `import.meta.filename`, which older Node versions don’t have. 1. ## Install dependencies [Section titled “Install dependencies”](#install-dependencies) This is a two-process app: a Next.js frontend/API layer, and a standalone agent worker that runs as a separate `tsx` process. Terminal ```bash npm install @scalekit-sdk/node livekit-server-sdk livekit-client @livekit/components-react @livekit/agents zod next react react-dom npm install -D tsx typescript ``` `@livekit/agents` is the Node Agents SDK — it ships the `voice.AgentSession` pipeline, the `defineAgent`/`cli.runApp` worker entrypoint, and the `llm.tool()` helper for function tools. `@scalekit-sdk/node` is the same Scalekit client either side of this app uses to call AgentKit. 2. ## Set environment variables [Section titled “Set environment variables”](#set-environment-variables) Create `.env.local` at the project root: .env.local ```bash # Scalekit — Settings → API Credentials in your Scalekit dashboard SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com SCALEKIT_CLIENT_ID=skc_your_client_id SCALEKIT_CLIENT_SECRET=your_client_secret # Development only: the identifier getSignedInUser() returns until you connect your auth TEST_IDENTIFIER=user@example.com # LiveKit Cloud — Settings → Keys LIVEKIT_URL=wss://your-project.livekit.cloud LIVEKIT_API_KEY=your_api_key LIVEKIT_API_SECRET=your_api_secret ``` `TEST_IDENTIFIER` stands in for whatever your app uses to identify a signed-in user. It’s only used in development. In production the identifier comes from the authenticated session — see [Production notes](#production-notes). 3. ## Carry the identifier through LiveKit dispatch metadata [Section titled “Carry the identifier through LiveKit dispatch metadata”](#carry-the-identifier-through-livekit-dispatch-metadata) The Next.js route that starts a call is the only place that decides which Scalekit identity the agent runs as. It takes the identifier from the signed-in user on the server, never from the request body, and sends it — not a token — inside LiveKit’s own `metadata` field on the agent dispatch. First, a stand-in for your app’s session lookup. Replace its body with however your app reads the signed-in user from a request: lib/auth.ts ```typescript export async function getSignedInUser(_req: Request): Promise<{ identifier: string } | null> { // Replace this with your app's session lookup, and return null when no one is signed in. if (process.env.NODE_ENV !== 'production' && process.env.TEST_IDENTIFIER) { return { identifier: process.env.TEST_IDENTIFIER }; } throw new Error('getSignedInUser is not connected to your auth yet'); } ``` Then the route: app/api/livekit/start/route.ts ```typescript 1 import { randomUUID } from 'node:crypto'; 2 import { NextResponse } from 'next/server'; 3 import { AccessToken, AgentDispatchClient } from 'livekit-server-sdk'; 4 import { getSignedInUser } from '@/lib/auth'; 5 6 const AGENT_NAME = 'scalekit-voice-agent'; 7 8 export async function POST(req: Request) { 9 const user = await getSignedInUser(req); 10 if (!user) { 11 return NextResponse.json({ error: 'Not signed in' }, { status: 401 }); 12 } 13 14 const livekitUrl = process.env.LIVEKIT_URL!; 15 const apiKey = process.env.LIVEKIT_API_KEY!; 16 const apiSecret = process.env.LIVEKIT_API_SECRET!; 17 18 const identifier = user.identifier; 19 const roomName = `voice-${randomUUID()}`; 20 const metadata = JSON.stringify({ scalekitConnectionId: identifier }); 21 22 const dispatchClient = new AgentDispatchClient(livekitUrl, apiKey, apiSecret); 23 await dispatchClient.createDispatch(roomName, AGENT_NAME, { metadata }); 24 25 const at = new AccessToken(apiKey, apiSecret, { identity: `user-${randomUUID()}` }); 26 at.addGrant({ roomJoin: true, room: roomName }); 27 const token = await at.toJwt(); 28 29 return NextResponse.json({ roomName, token, url: livekitUrl }); 30 } ``` `createDispatch(roomName, agentName, { metadata })` tells LiveKit’s cloud infrastructure to route this room to a worker registered under `agentName` — you’ll register the same string in the next step. `metadata` is a plain JSON string; LiveKit stores it and hands it to the worker’s job context untouched. The browser only ever receives `token` (a room-join JWT) and `roomName` — never a Scalekit credential. This is the entire security boundary The agent runs tools as whatever identifier this route puts in the dispatch metadata, so the identifier must come from the signed-in user on the server. Never read it from the request body, a query string or anything else the client can set: if you do, any caller can run tools on another user’s connected accounts. Only your server can create a dispatch, because it needs the LiveKit API secret, so the worker can trust the metadata it receives. 4. ## Read the identifier and call the tool directly [Section titled “Read the identifier and call the tool directly”](#read-the-identifier-and-call-the-tool-directly) The agent worker is a separate file, run as its own process — not a Next.js route. It parses the same metadata shape the dispatch call sent, then wires a Scalekit-backed function tool: agent/src/agent.ts ```typescript 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 import { cli, defineAgent, llm, ServerOptions, voice, type JobContext } from '@livekit/agents'; 3 import { z } from 'zod'; 4 5 const scalekit = new ScalekitClient( 6 process.env.SCALEKIT_ENVIRONMENT_URL!, 7 process.env.SCALEKIT_CLIENT_ID!, 8 process.env.SCALEKIT_CLIENT_SECRET!, 9 ); 10 11 const entry = async (ctx: JobContext): Promise => { 12 await ctx.connect(); 13 14 const metadata = JSON.parse(ctx.job.metadata || '{}') as { scalekitConnectionId?: string }; 15 const identifier = metadata.scalekitConnectionId; 16 if (!identifier) { 17 // Only the start route sets this. Don't fall back to a default user. 18 throw new Error('Dispatch metadata has no scalekitConnectionId'); 19 } 20 console.log(`[scalekit-voice-agent] job=${ctx.job.id} room=${ctx.room.name} identifier=${identifier}`); 21 22 const googlecalendar_list_events = llm.tool({ 23 description: "List events from the user's Google Calendar. Use this when the user asks about their schedule.", 24 parameters: z.object({ 25 calendar_id: z.string().optional().describe("Defaults to 'primary'."), 26 }), 27 execute: async ({ calendar_id }) => { 28 const result = await scalekit.actions.executeTool({ 29 connector: 'googlecalendar', 30 identifier, 31 toolName: 'googlecalendar_list_events', 32 toolInput: { calendar_id: calendar_id ?? 'primary' }, 33 }); 34 return result.data ?? result; 35 }, 36 }); 37 38 const agent = new voice.Agent({ 39 instructions: "You are a helpful voice assistant. Check the user's calendar when asked.", 40 tools: { googlecalendar_list_events }, 41 }); 42 43 const session = new voice.AgentSession({ 44 llm: 'openai/gpt-4o-mini', 45 stt: 'assemblyai/universal-streaming', 46 tts: 'cartesia/sonic-2', 47 }); 48 49 await session.start({ agent, room: ctx.room }); 50 await session.generateReply({ instructions: 'Greet the user and offer to help with their calendar.' }); 51 }; 52 53 export default defineAgent({ entry }); 54 55 cli.runApp(new ServerOptions({ agent: import.meta.filename, agentName: 'scalekit-voice-agent' })); ``` Three things worth naming explicitly: * **`llm.tool()`’s object key is the tool’s name** — there’s no `name` field inside the call. The LLM sees the tool as `googlecalendar_list_events` because that’s the key in the `tools: { googlecalendar_list_events }` map. * **`voice.Agent` is a plain constructor** — `new voice.Agent({ instructions, tools })`. There’s no static factory method. * **The three model strings are [LiveKit Inference](https://docs.livekit.io/agents/) identifiers**, not your own API keys. They route STT/LLM/TTS through LiveKit Cloud’s gateway, billed to your LiveKit project — no separate OpenAI, AssemblyAI, or Cartesia account needed to get this running. `agentName: 'scalekit-voice-agent'` must match the string `AgentDispatchClient.createDispatch()` used in step 3 exactly. That’s the only thing connecting the two processes — get it wrong and the dispatch succeeds, a room gets created, and the agent simply never joins it. 5. ## Run both processes [Section titled “Run both processes”](#run-both-processes) Terminal 1 — Next.js app ```bash npm run dev ``` Terminal 2 — agent worker ```bash npx tsx watch --env-file=.env.local agent/src/agent.ts dev ``` `--env-file` matters here: Next.js loads `.env.local` automatically, but a plain `tsx` process doesn’t unless you tell it to. ## Testing [Section titled “Testing”](#testing) Confirm the identity actually reaches the agent before wiring up a browser. Hit the dispatch route directly. In development, `getSignedInUser()` returns `TEST_IDENTIFIER`: Terminal ```bash curl -X POST http://localhost:3000/api/livekit/start ``` The agent worker’s terminal should log that identifier within a couple of seconds: Terminal 2 output ```text [scalekit-voice-agent] job=AJ_giyT9fjSxPg8 room=voice-2eb7cc3d-... identifier=user@example.com ``` If that line doesn’t appear, the agent never received the dispatch — see [Common mistakes](#common-mistakes) below before checking anything else. Once it does, open your frontend, click **Start**, and ask “what’s on my calendar today?” — the agent should call `executeTool`, get back real calendar data, and speak a summary. ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) Agent never joins the room `agentName` in `ServerOptions` doesn’t match the second argument to `createDispatch()`. LiveKit’s dispatch matches by exact string — there’s no error, no timeout message, just a room with no agent in it. **Solution:** Compare the two strings directly. In the code above, both are `'scalekit-voice-agent'`. A trailing space or a casing difference is enough to break this silently. `TypeError: … is not a function` around `voice.Agent` Older examples (including early drafts of this pattern) show `voice.Agent.create({...})`. That method doesn’t exist in `@livekit/agents` — `voice.Agent` is a plain constructor: `new voice.Agent({ instructions, tools })`. **Solution:** Use `new voice.Agent(...)`, not a static factory. `dev:agent` can’t find `SCALEKIT_ENVIRONMENT_URL` or `LIVEKIT_URL` The agent worker is a standalone Node process. Unlike Next.js, it does not load `.env.local` on its own. **Solution:** Run it with `tsx watch --env-file=.env.local agent/src/agent.ts dev`, not a bare `tsx agent/src/agent.ts`. Agent speaks a generic error instead of calendar data The identifier reaching the agent doesn’t have an **Active** `googlecalendar` connection in Scalekit — most often because the identifier `getSignedInUser()` returned (`TEST_IDENTIFIER` in development) doesn’t match the identifier you authorized in the dashboard. **Solution:** Check **AgentKit → Connections** in the Scalekit dashboard, or call `scalekit.tools.listScopedTools(identifier, { filter: { connectionNames: ['googlecalendar'] } })` from a debug route to confirm which tools that exact identifier can see. ## Production notes [Section titled “Production notes”](#production-notes) **No token ever reaches the browser or the LLM.** The Next.js route returns only `roomName`, `url` and a room-join JWT scoped to one room — never a Scalekit credential or the identifier. The agent worker is the only process that ever constructs a `ScalekitClient`, and `executeTool()`’s result — plain calendar data — is the only thing that reaches the LLM. **Tools run through `executeTool()`.** The LiveKit Node Agents SDK (`@livekit/agents` 1.4.11) has no MCP toolset, so the agent calls `executeTool()` directly, as shown above. **Connect `getSignedInUser()` to your auth before you deploy.** `TEST_IDENTIFIER` is a development convenience, and the stub throws in production until you replace it. Read the identifier from the signed-in user’s session on the server that handles the dispatch request, never from a field the client sends. **Add more tools by changing one string.** The `connector`/`toolName` pair in `executeTool()` is the only connector-specific part of this code. Swap `googlecalendar_list_events` for any of the 614 [AgentKit connectors](/agentkit/connectors/) and the identity-passing mechanism is unchanged. ## Next steps [Section titled “Next steps”](#next-steps) * [Configure more connectors](/agentkit/connectors/) — extend beyond Google Calendar to Gmail, Slack, or GitHub. * [Connected accounts](/agentkit/connected-accounts/) — check connection status and revoke access programmatically. * [AgentKit quickstart](/agentkit/quickstart/) — connect your first user in under five minutes. * [LiveKit Agents docs](https://docs.livekit.io/agents/) — turn detection, interruption handling, and telephony/SIP. ## Related resources [Section titled “Related resources”](#related-resources) | Topic | Link | | ------------------- | --------------------------------------------------------------------------------------------------- | | AgentKit overview | [Overview](/agentkit/overview/) | | All connectors | [Connectors](/agentkit/connectors/) | | Connected accounts | [Manage connected accounts](/agentkit/connected-accounts/) | | Sample repository | [livekit-scalekit-voice-agent](https://github.com/scalekit-developers/livekit-scalekit-voice-agent) | | LiveKit Agents docs | [docs.livekit.io/agents](https://docs.livekit.io/agents/) | --- # DOCUMENT BOUNDARY --- # M2M JWT verification with JWKS and OAuth scopes > How JSON Web Key Sets work with Scalekit, how to use the /keys endpoint to verify machine-to-machine tokens, and how OAuth scopes map to JWT claims for authorization. When you add OAuth 2.0 client credentials for your APIs, callers receive **JWT access tokens**. Before you trust any claim, you must **verify the signature** using Scalekit’s public keys (**JWKS**). After verification, you **authorize** the request—often by checking **OAuth scopes** carried in the token. This cookbook explains how JWKS and scopes fit together for Scalekit M2M flows: where keys live, how verification works at a high level, how scopes are defined and stored, and how to enforce them in your service. ## Why JWKS and scopes belong in one place [Section titled “Why JWKS and scopes belong in one place”](#why-jwks-and-scopes-belong-in-one-place) * **JWKS answers “is this token real?”** — You use the key identified by `kid` in the JWT header to validate the signature (typically **RS256**). * **Scopes answer “what may this client do?”** — After the token is valid, you inspect the `scopes` claim (and your routing rules) to allow or deny the operation. Skipping either step breaks your security model: verified-but-overpowered clients, or unverified tokens. ## JWKS and Scalekit keys [Section titled “JWKS and Scalekit keys”](#jwks-and-scalekit-keys) A **JSON Web Key Set (JWKS)** is JSON that lists one or more **JWKs**—public key material identified by a `kid` (key ID). Scalekit puts the matching `kid` in the JWT header so your validator can pick the right key without baking certificates into your app. Each environment publishes signing keys at: ```http GET https:///keys ``` Use the same base URL as `/oauth/token` (for example `https://your-app.scalekit.dev`). Example response shape: Example JWKS document ```json { "keys": [ { "use": "sig", "kty": "RSA", "kid": "snk_58327480989122566", "alg": "RS256", "n": "…", "e": "AQAB" } ] } ``` For access tokens, use the key where `use` is `sig` and `alg` is `RS256`. ## Verify an access token [Section titled “Verify an access token”](#verify-an-access-token) At implementation time, your API typically: 1. **Extracts** the bearer token from `Authorization: Bearer `. 2. **Decodes** the JWT header (base64url, first segment) and reads `kid` and `alg`. Do not trust the payload until the signature checks out. 3. **Resolves the signing key** — fetch `https:///keys`, or use a JWKS client (for example `jwks-rsa` in Node.js) with **caching** and refresh when you see an unknown `kid`. 4. **Verifies** the signature with your JWT library (RS256 for Scalekit access tokens). 5. **Validates claims** such as `exp`, `iss` (your environment URL), and `aud` if your API relies on audience restrictions. 6. **Authorizes** the operation using application claims—especially **`scopes`** (covered in the next section). Prefer the Scalekit SDK when possible SDKs can validate access tokens against JWKS and optionally enforce scopes. See the [M2M API authentication quickstart](/authenticate/m2m/api-auth-quickstart/) and [Authenticate customer apps](/guides/m2m/api-auth-m2m-clients/). Use generic JWT + JWKS libraries when you need custom middleware or an unsupported runtime. ### Operational practices [Section titled “Operational practices”](#operational-practices) * **Cache JWKS** responses; refetch when verification fails with an unknown `kid` (key rotation). * **Fail closed** on bad signature, wrong issuer, or expired token (`401`; use `403` when the token is valid but not allowed). * **Never** skip signature verification based on the payload alone. ## OAuth scopes for machine clients [Section titled “OAuth scopes for machine clients”](#oauth-scopes-for-machine-clients) **Scopes** are permission names you attach to an OAuth client. In M2M flows they describe *what* a client may do—separate from *who* it is (`client_id` / `sub`). ### Why scopes matter [Section titled “Why scopes matter”](#why-scopes-matter) Without scopes, any valid client could hit any endpoint. Scopes let you apply **least privilege**, **document** what each integration is for, and **enforce** rules in your API by reading the `scopes` array on the JWT. ### How scopes work in Scalekit M2M [Section titled “How scopes work in Scalekit M2M”](#how-scopes-work-in-scalekit-m2m) 1. When you **register an API client** for an organization, you pass a `scopes` array (REST or SDKs). 2. Scalekit stores those scopes and includes them on issued access tokens. 3. Your API **verifies the JWT** (steps above), then checks that `scopes` includes what the route requires. Use a consistent naming pattern such as `resource:action` (for example `deployments:read`, `deployments:write`). ### Register scopes on the client [Section titled “Register scopes on the client”](#register-scopes-on-the-client) Scopes are set at **client creation** (and when you update the client via the API). Example: scopes on create client (illustrative) ```json "scopes": [ "deploy:applications", "read:deployments" ] ``` The same values appear on the client record and in issued tokens. Token response vs JWT payload The `/oauth/token` response may include a space-separated `scope` string for OAuth compatibility. For authorization logic, rely on the JWT payload’s **`scopes` array**. See the [quickstart](/authenticate/m2m/api-auth-quickstart/) for a decoded example. ### Validate scopes on your API [Section titled “Validate scopes on your API”](#validate-scopes-on-your-api) After the token is verified: * **Read `scopes`** from the payload, for example: scopes in JWT payload (example) ```json "scopes": [ "deploy:applications", "read:deployments" ] ``` * **Compare** what the token grants to what the route allows (for example require `deploy:applications` on `POST /deploy`); return `403` if a required scope is missing. * **Use SDK helpers** where they fit your stack to combine signature and scope checks (see the [quickstart](/authenticate/m2m/api-auth-quickstart/)). ## Related [Section titled “Related”](#related) * [Add OAuth 2.0 to your APIs](/authenticate/m2m/api-auth-quickstart/) — client registration, tokens, examples * [API keys](/authenticate/m2m/api-keys/) — long-lived keys; patterns may differ from OAuth client credentials * [Authenticate customer apps](/guides/m2m/api-auth-m2m-clients/) — customer-facing API auth and JWKS examples --- # DOCUMENT BOUNDARY --- # Build a Mastra agent with Scalekit AgentKit tools > Give a Mastra agent access to Gmail and the Scalekit AgentKit connector catalog — zero manual OAuth handling. A [Mastra](https://mastra.ai) agent that reads emails needs a Gmail OAuth token. An agent that also posts to Slack needs a second token. Each tool means another OAuth flow, another token store, another refresh cycle. Before you write any agent logic, you are already maintaining parallel credential pipelines. Scalekit AgentKit eliminates that overhead. It stores one OAuth session per connector per user, handles token refresh automatically, and gives your agent a single API surface for 614 connectors. This recipe shows how to discover AgentKit tools at runtime, wrap them as native Mastra tools, and run them through a Mastra agent — all in TypeScript, with no Python backend. ## What you are building [Section titled “What you are building”](#what-you-are-building) * **A Mastra agent** that fetches Gmail messages through Scalekit AgentKit. * **Dynamic tool discovery** — the agent discovers available tools at runtime from Scalekit, instead of hardcoding tool definitions. * **Authorization link** — if the user has not connected their Gmail account, the agent generates an authorization URL. * **A pattern you can extend** to any of Scalekit’s 614 [connectors](/agentkit/connectors/) by changing a single string. The complete source is available in the [mastra-agentkit-example](https://github.com/scalekit-developers/mastra-agentkit-example) repository. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * A Scalekit account at [app.scalekit.com](https://app.scalekit.com) with API credentials (**Settings → API Credentials**). * A **Gmail** connection configured under **AgentKit → Connections**. See [Configure a connection](/agentkit/connections/). * An OpenAI API key. * **Node.js 18+** and **pnpm** (or npm). 1. ## Install dependencies [Section titled “Install dependencies”](#install-dependencies) Terminal ```bash pnpm add @mastra/core @scalekit-sdk/node @ai-sdk/openai zod dotenv pnpm add -D tsx typescript @types/node ``` `@mastra/core` provides the `Agent` and `createTool` primitives. `@scalekit-sdk/node` handles authentication, tool discovery, and tool execution against the Scalekit API. `@ai-sdk/openai` connects the agent to GPT-4o. 2. ## Set environment variables [Section titled “Set environment variables”](#set-environment-variables) Create a `.env` file at the project root: .env ```bash # Scalekit — from app.scalekit.com → Settings → API Credentials # Threat: leaked credentials grant full API access to your Scalekit environment. # Never commit this file to version control; add .env to .gitignore. SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev SCALEKIT_CLIENT_ID=skc_your_client_id SCALEKIT_CLIENT_SECRET=your_client_secret # OpenAI # Threat: exposed key allows unauthorized model usage billed to your account. OPENAI_API_KEY=sk-your-openai-key # User and connection — replace with values from your application USER_IDENTIFIER=user_123 CONNECTION_NAME=gmail ``` | Variable | Purpose | | -------------------------- | --------------------------------------------------------- | | `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL (starts with `https://`) | | `SCALEKIT_CLIENT_ID` | Client ID from API Credentials (starts with `skc_`) | | `SCALEKIT_CLIENT_SECRET` | Client secret from API Credentials | | `OPENAI_API_KEY` | OpenAI API key for GPT-4o | | `USER_IDENTIFIER` | A unique identifier for the end user in your application | | `CONNECTION_NAME` | The connection name configured in your Scalekit dashboard | 3. ## Initialize Scalekit and ensure the user is connected [Section titled “Initialize Scalekit and ensure the user is connected”](#initialize-scalekit-and-ensure-the-user-is-connected) Create `src/index.ts`. Start by initializing the Scalekit client and checking whether the user has an active Gmail connection: src/index.ts ```typescript 1 import { Agent } from '@mastra/core/agent'; 2 import { createTool } from '@mastra/core/tools'; 3 import { openai } from '@ai-sdk/openai'; 4 import { ScalekitClient } from '@scalekit-sdk/node'; 5 import { z } from 'zod'; 6 import 'dotenv/config'; 7 8 const IDENTIFIER = process.env.USER_IDENTIFIER || 'user_123'; 9 const CONNECTION = process.env.CONNECTION_NAME || 'gmail'; 10 11 const scalekit = new ScalekitClient( 12 process.env.SCALEKIT_ENVIRONMENT_URL!, 13 process.env.SCALEKIT_CLIENT_ID!, 14 process.env.SCALEKIT_CLIENT_SECRET!, 15 ); 16 17 const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ 18 connectionName: CONNECTION, 19 identifier: IDENTIFIER, 20 }); 21 22 if (connectedAccount?.status?.toString() !== '1') { 23 const { link } = await scalekit.actions.getAuthorizationLink({ 24 connectionName: CONNECTION, 25 identifier: IDENTIFIER, 26 }); 27 console.log(`\n[${CONNECTION}] Authorization required.`); 28 console.log(`Open this link:\n\n ${link}\n`); 29 console.log('Press Enter once you have completed the OAuth flow...'); 30 await new Promise((resolve) => { 31 process.stdin.resume(); 32 process.stdin.once('data', () => { process.stdin.pause(); resolve(); }); 33 }); 34 } ``` `getOrCreateConnectedAccount` returns an existing session if one exists or creates a pending one. If the account is not active (status `1`), `getAuthorizationLink` returns a URL you open in a browser. Scalekit handles the full OAuth exchange — your application never sees the provider’s client secret. Production flow In a web application, you would redirect the user to the authorization link in the browser and handle the callback. The CLI approach here is for development and testing. 4. ## Discover tools from Scalekit [Section titled “Discover tools from Scalekit”](#discover-tools-from-scalekit) Once the user is connected, list the tools available for their account: src/index.ts (continued) ```typescript const toolsResponse = await scalekit.tools.listTools({ filter: { connector: CONNECTION, identifier: IDENTIFIER }, pageSize: 50, }); const scalekitTools = toolsResponse.tools; console.log( `Discovered ${scalekitTools.length} tools: ` + scalekitTools.map((t) => (t.definition as any)?.name).join(', ') ); ``` `listTools` returns tool definitions that include a `name`, `description`, and `input_schema` (a JSON Schema object). The `filter` parameter scopes results to the connector and user — the agent only sees tools the user has authorized. 5. ## Convert Scalekit tools to Mastra tools [Section titled “Convert Scalekit tools to Mastra tools”](#convert-scalekit-tools-to-mastra-tools) Mastra agents accept tools created with `createTool`. Each Scalekit tool needs to be wrapped: src/index.ts (continued) ```typescript 1 const mastraTools: Record> = {}; 2 3 for (const tool of scalekitTools) { 4 const def = tool.definition as Record | undefined; 5 if (!def?.name) continue; 6 7 const toolName: string = def.name; 8 const description: string = def.description || toolName; 9 10 // Use a permissive Zod schema — Scalekit validates inputs server-side. 11 const inputSchema = z.object({}).passthrough(); 12 13 mastraTools[toolName] = createTool({ 14 id: toolName, 15 description, 16 inputSchema, 17 execute: async ({ context }) => { 18 const result = await scalekit.tools.executeTool({ 19 toolName, 20 connector: CONNECTION, 21 identifier: IDENTIFIER, 22 params: context as Record, 23 }); 24 return result; 25 }, 26 }); 27 } ``` The `inputSchema` uses `z.object({}).passthrough()` — a permissive schema that lets the LLM pass any parameters through. Scalekit validates inputs server-side, so client-side validation is optional. If you want stricter types, convert the JSON Schema from `def.input_schema` into a typed Zod schema. The `execute` function calls `scalekit.tools.executeTool()`, which sends the tool call to Scalekit. Scalekit injects the user’s OAuth token, calls the third-party API, and returns the structured response. 6. ## Build and run the agent [Section titled “Build and run the agent”](#build-and-run-the-agent) Create the Mastra agent with the discovered tools and run it: src/index.ts (continued) ```typescript const agent = new Agent({ name: 'gmail-assistant', instructions: 'You are a helpful Gmail assistant. Use the available tools to fulfill requests. ' + 'Always confirm what you did after completing an action.', model: openai('gpt-4o'), tools: mastraTools, }); const prompt = process.argv[2] || 'Fetch my last 5 unread emails and summarize them.'; console.log(`\nPrompt: ${prompt}\n`); const result = await agent.generate(prompt); console.log(result.text); ``` Set `"type": "module"` and add a start script in `package.json`. The code uses top-level `await`, which needs ES modules: package.json (type and scripts) ```json { "type": "module", "scripts": { "start": "tsx src/index.ts" } } ``` 7. ## Run and verify [Section titled “Run and verify”](#run-and-verify) Terminal ```bash pnpm start ``` On the first run, if the user hasn’t authorized Gmail, you see the authorization flow: Terminal ```text [gmail] Authorization required. Open this link: https://auth.scalekit.dev/connect/... Press Enter once you have completed the OAuth flow... ``` After authorization (or on subsequent runs), the agent runs: Terminal ```text Connected account for user_123 is active. Discovered 8 tools: gmail_fetch_mails, gmail_send_message, gmail_list_threads, ... Created 8 Mastra tools. Prompt: Fetch my last 5 unread emails and summarize them. Here are your 5 most recent unread emails: 1. "Q1 roadmap feedback needed" — Sarah Chen (1h ago) Requesting feedback on the product roadmap by Friday. 2. "Deploy failed: production" — GitHub Actions (2h ago) CI pipeline failed on the main branch, test suite timeout. 3. "New PR review requested" — Lin Feng (3h ago) Review requested on PR #412: refactor auth middleware. ... ``` You can also pass a custom prompt: Terminal ```bash pnpm start "Search for emails from GitHub and list the subjects" ``` ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) Connected account stays in PENDING_AUTH You did not complete the OAuth flow in the browser. AgentKit waits for you to authorize through the URL returned by `getAuthorizationLink`. **Solution:** Open the printed URL in a browser, complete the Google OAuth consent, and return to the terminal. The connected account status updates to `ACTIVE` after a successful callback. Tool list is empty The connection name in code does not match the connection name in the Scalekit dashboard, or the connected account is not active. **Solution:** Open **AgentKit → Connections** in the dashboard. Verify the connection name matches exactly (case-sensitive). Then check that the connected account for your identifier shows **ACTIVE** status. `executeTool` fails with identifier error The `identifier` you passed to `executeTool` does not match the identifier you used when creating the connected account. **Solution:** Use the same `identifier` value throughout — `getOrCreateConnectedAccount`, `listTools`, and `executeTool` must all receive the same string. Agent generates text instead of calling tools The model did not receive tool definitions with enough detail to trigger a tool call. This happens when every tool has an empty description or when the `inputSchema` is missing entirely. **Solution:** Verify that `scalekitTools` is not empty after discovery. Print `Object.keys(mastraTools)` to confirm tools were created. If tools exist but the model still does not call them, check that the tool descriptions are informative — the LLM uses descriptions to decide when a tool is relevant. ## Production notes [Section titled “Production notes”](#production-notes) **Token refresh is automatic.** Scalekit stores OAuth tokens per user per connector and refreshes them before expiry. Your agent code never handles refresh tokens directly. **Scope tools per user.** The `identifier` parameter in `listTools` and `executeTool` ensures each user only accesses their own connected accounts. Never share an identifier across users. **Add more connectors.** Change `CONNECTION_NAME` to `slack`, `notion`, `googlecalendar`, or any of the 614 [supported connectors](/agentkit/connectors/). The code is identical — only the connection name changes. **Error handling in production.** Wrap `executeTool` calls in try/catch to handle network errors and expired connections gracefully. Return a clear message to the user when a tool call fails instead of letting the agent retry silently. **MCP alternative.** If you prefer Mastra’s built-in MCP client over manual tool wrapping, see the [Mastra MCP example](/agentkit/examples/mastra/). That approach requires a per-user MCP URL generated from the Python SDK. ## Next steps [Section titled “Next steps”](#next-steps) * [Configure more connectors](/agentkit/connectors/) — add Slack, GitHub, Salesforce, and others alongside Gmail. * [Mastra MCP integration](/agentkit/examples/mastra/) — use Mastra’s native MCP client with a Scalekit MCP URL. * [AgentKit quickstart](/agentkit/quickstart/) — connect your first user in under five minutes. * [Connected accounts](/agentkit/connected-accounts/) — manage user connections, check status, and revoke access programmatically. ## Related resources [Section titled “Related resources”](#related-resources) | Topic | Link | | ------------------ | ----------------------------------------------------------------------------------------- | | AgentKit overview | [Overview](/agentkit/overview/) | | All connectors | [Connectors](/agentkit/connectors/) | | Connected accounts | [Manage connected accounts](/agentkit/connected-accounts/) | | Mastra MCP example | [Mastra](/agentkit/examples/mastra/) | | Sample repository | [mastra-agentkit-example](https://github.com/scalekit-developers/mastra-agentkit-example) | | Mastra docs | [mastra.ai/docs](https://mastra.ai/docs) | --- # DOCUMENT BOUNDARY --- # Migrate from Auth0 to Scalekit > Move users, organizations, and enterprise SSO off Auth0 to Scalekit Full Stack Auth with a safe, incremental cutover. Migrating a B2B app off Auth0 is risky because three things move at once: user records, the organization or tenant structure, and enterprise SSO connections. Do it in one big switch and you risk locking customers out. This recipe moves each piece to [Scalekit Full Stack Auth](/authenticate/fsa/quickstart/) in a safe, reversible order, then cuts traffic over behind a feature flag. The approach avoids re-hashing passwords. Instead of copying credentials, you point your app at Scalekit’s hosted login and let users re-authenticate through SSO, social login, or passwordless on their next visit. This is the recommended path for B2B products, where most enterprise users already sign in through an identity provider rather than a password. Prefer a guided cutover? The Scalekit Solutions team has run dozens of migrations. For password-hash migration or a staged cut-over plan, [contact us](/support/contact-us) and we’ll design the rollout with you. ## What you build [Section titled “What you build”](#what-you-build) * A field mapping from Auth0 users, organizations, and connections to Scalekit * A one-time import script that recreates organizations and users with `external_id` back-references * Enterprise SSO connections rebuilt in Scalekit for each customer that used them * An incremental cutover behind a feature flag, with a rollback path ## Who needs this [Section titled “Who needs this”](#who-needs-this) This recipe is for you if: * You authenticate a B2B or multi-tenant app on Auth0 today, using Auth0 Organizations or per-tenant connections. * You want Scalekit to own hosted login, sessions, enterprise SSO, and SCIM going forward. * You can run a short backfill script and toggle a feature flag in your app. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * An Auth0 tenant with a Machine-to-Machine application authorized for the Auth0 Management API. * A Scalekit account with API credentials from the dashboard. See [Set up Scalekit](/authenticate/fsa/quickstart/). * The Scalekit SDK installed in your backend. ## How Auth0 concepts map to Scalekit [Section titled “How Auth0 concepts map to Scalekit”](#how-auth0-concepts-map-to-scalekit) Start from the data model. Every later step follows this mapping. | Auth0 concept | Scalekit concept | Notes | | ------------------------------------ | -------------------- | -------------------------------------------------------------------------------------- | | Organization | Organization | Store the Auth0 `org_id` as the Scalekit `external_id`. | | User | User + membership | Users belong to an organization through a membership that carries roles. | | `user_id` (for example `auth0\|abc`) | User `external_id` | Preserves lookups between systems during cutover. | | Enterprise connection (SAML, OIDC) | SSO connection | Recreated per organization in Scalekit; secrets are not exportable from Auth0. | | Roles and permissions | Roles | Recreate roles in Scalekit, then attach them to memberships on import. | | Database (password) users | Hosted login re-auth | Users re-verify through SSO, social, or passwordless. See the following password note. | ## Migrate the data [Section titled “Migrate the data”](#migrate-the-data) 1. ## Export your Auth0 data [Section titled “Export your Auth0 data”](#export-your-auth0-data) Pull three datasets from Auth0 using the [Management API](https://auth0.com/docs/api/management/v2) or the [User Import / Export extension](https://auth0.com/docs/customize/extensions/user-import-export-extension). Create a [bulk user export job](https://auth0.com/docs/manage-users/user-migration/bulk-user-exports) to get every user as newline-delimited JSON: Export Auth0 users ```bash # Security: pass the Management API token from an environment variable, never inline it. curl "https://YOUR_AUTH0_DOMAIN/api/v2/jobs/users-exports" \ --request POST \ --header "Authorization: Bearer $AUTH0_MGMT_TOKEN" \ --header 'Content-Type: application/json' \ --data '{ "format": "json", "fields": [ { "name": "user_id" }, { "name": "email" }, { "name": "email_verified" }, { "name": "given_name" }, { "name": "family_name" } ] }' ``` Poll `GET /api/v2/jobs/{job_id}` until the job reports `completed`, then download the file it returns. Then export [organizations](https://auth0.com/docs/manage-users/organizations) and their members: Export Auth0 organizations and members ```bash curl "https://YOUR_AUTH0_DOMAIN/api/v2/organizations" \ --header "Authorization: Bearer $AUTH0_MGMT_TOKEN" # For each organization id returned above: curl "https://YOUR_AUTH0_DOMAIN/api/v2/organizations/{org_id}/members" \ --header "Authorization: Bearer $AUTH0_MGMT_TOKEN" ``` Finally, list the enterprise connections you rebuild in Scalekit. Record the identity provider, metadata URL, and which organizations use each one. Connection secrets stay in the identity provider and are not exportable. List Auth0 enterprise connections ```bash curl "https://YOUR_AUTH0_DOMAIN/api/v2/connections?strategy=samlp" \ --header "Authorization: Bearer $AUTH0_MGMT_TOKEN" ``` 2. ## Install the Scalekit SDK [Section titled “Install the Scalekit SDK”](#install-the-scalekit-sdk) Add the SDK to the backend that runs your import script. * Node.js ```bash npm install @scalekit-sdk/node ``` * Python ```sh pip install scalekit-sdk-python ``` * Go ```sh go get -u github.com/scalekit-inc/scalekit-sdk-go ``` * Java ```groovy /* Gradle users - add the following to your dependencies in build file */ implementation "com.scalekit:scalekit-sdk-java:2.1.3" ``` ```xml com.scalekit scalekit-sdk-java 2.1.3 ``` 3. ## Import organizations first [Section titled “Import organizations first”](#import-organizations-first) Create each Auth0 organization in Scalekit and set `external_id` to the Auth0 `org_id`. This back-reference lets you attach users to the right organization and reconcile records during cutover. * Node.js import-organizations.js ```javascript // organizations: rows read from your Auth0 organizations export for (const org of organizations) { const result = await scalekit.organization.createOrganization(org.display_name, { externalId: org.id, // Auth0 org_id, preserved for lookups metadata: { source: 'auth0' }, }); console.log(`Created organization: ${result.id}`); } ``` * Python import_organizations.py ```python from scalekit.v1.organizations.organizations_pb2 import CreateOrganization # organizations: rows read from your Auth0 organizations export for org in organizations: result = scalekit_client.organization.create_organization( CreateOrganization( display_name=org["display_name"], external_id=org["id"], # Auth0 org_id, preserved for lookups metadata={"source": "auth0"}, ) ) print(f"Created organization: {result.id}") ``` * Go import_organizations.go ```go // organizations: rows read from your Auth0 organizations export for _, org := range organizations { result, err := scalekitClient.Organization.CreateOrganization( ctx, org.DisplayName, scalekit.CreateOrganizationOptions{ ExternalID: org.ID, // Auth0 org_id, preserved for lookups Metadata: map[string]interface{}{"source": "auth0"}, }, ) if err != nil { log.Fatal(err) } fmt.Printf("Created organization: %s\n", result.ID) } ``` * Java ImportOrganizations.java ```java // organizations: rows read from your Auth0 organizations export for (Map org : organizations) { CreateOrganization createOrganization = CreateOrganization.newBuilder() .setDisplayName((String) org.get("display_name")) .setExternalId((String) org.get("id")) // Auth0 org_id, preserved for lookups .putMetadata("source", "auth0") .build(); Organization result = scalekitClient.organizations().create(createOrganization); System.out.println("Created organization: " + result.getId()); } ``` 4. ## Import users into their organizations [Section titled “Import users into their organizations”](#import-users-into-their-organizations) Create each user inside the organization it belongs to, and set the user `external_id` to the Auth0 `user_id`. Attach roles through the membership so access control works on the first login. Set `sendInvitationEmail` to `false` to skip invite emails during a bulk backfill. Scalekit marks the membership `active` and treats the email as verified. * Node.js import-users.js ```javascript const { user } = await scalekit.user.createUserAndMembership(organizationId, { email: row.email, externalId: row.user_id, // Auth0 user_id, e.g. "auth0|abc123" sendInvitationEmail: false, userProfile: { firstName: row.given_name, lastName: row.family_name, }, metadata: { roles: row.roles?.join(',') ?? '' }, }); console.log(`Created user: ${user.id}`); ``` * Python import_users.py ```python 1 from scalekit.v1.users.users_pb2 import CreateUser 2 from scalekit.v1.commons.commons_pb2 import UserProfile 3 4 user_msg = CreateUser( 5 email=row["email"], 6 external_id=row["user_id"], # Auth0 user_id, e.g. "auth0|abc123" 7 user_profile=UserProfile( 8 first_name=row["given_name"], 9 last_name=row["family_name"], 10 ), 11 ) 12 13 create_resp, _ = scalekit_client.user.create_user_and_membership( 14 organization_id, user_msg 15 ) 16 print(f"Created user: {create_resp.user.id}") ``` * Go import_users.go ```go newUser := &usersv1.CreateUser{ Email: row.Email, ExternalId: row.UserID, // Auth0 user_id, e.g. "auth0|abc123" UserProfile: &usersv1.CreateUserProfile{ FirstName: row.GivenName, LastName: row.FamilyName, }, } cuResp, err := scalekitClient.User().CreateUserAndMembership(ctx, organizationID, newUser, false) if err != nil { log.Fatal(err) } fmt.Printf("Created user: %s\n", cuResp.User.Id) ``` * Java ImportUsers.java ```java CreateUser createUser = CreateUser.newBuilder() .setEmail(row.email) .setExternalId(row.userId) // Auth0 user_id, e.g. "auth0|abc123" .setUserProfile( CreateUserProfile.newBuilder() .setFirstName(row.givenName) .setLastName(row.familyName) .build()) .build(); CreateUserAndMembershipResponse cuResp = scalekitClient.users() .createUserAndMembership(organizationId, createUser); System.out.println("Created user: " + cuResp.getUser().getId()); ``` Batch the import and run requests in parallel for speed, but respect rate limits. Roles referenced on the membership must exist first. Create them under **User Management > Roles** or with the SDK. See [Create roles and permissions](/authenticate/authz/create-roles-permissions/). 5. ## Rebuild enterprise SSO connections [Section titled “Rebuild enterprise SSO connections”](#rebuild-enterprise-sso-connections) For every customer that signed in through an Auth0 enterprise connection, recreate the connection in Scalekit against the same identity provider. You configure this per organization, so each customer keeps its own SAML or OIDC setup. Follow [Add modular SSO](/authenticate/sso/add-modular-sso/) for each organization. Reuse the identity provider metadata you recorded during export, then re-run the identity provider’s setup to issue fresh SAML or OIDC credentials to Scalekit. Connection secrets from Auth0 cannot be reused. 6. ## Point your app at Scalekit hosted login [Section titled “Point your app at Scalekit hosted login”](#point-your-app-at-scalekit-hosted-login) Replace the Auth0 login redirect and session validation with Scalekit. * Register your callback and post-logout URLs under **Settings > Redirects**. See the [redirect URI guide](/guides/dashboard/redirects/). * Swap Auth0 SDK session middleware for the Scalekit SDK, or validate access tokens against Scalekit’s JWKS endpoint. * Read authorization from the `roles` claim that Scalekit issues, in place of Auth0 roles or scopes. 7. ## Cut over incrementally and verify [Section titled “Cut over incrementally and verify”](#cut-over-incrementally-and-verify) Roll out behind a feature flag so you can reverse the switch without a redeploy. 1. Route 5 to 10 percent of traffic to Scalekit login and confirm those users authenticate, receive sessions, and see the right roles. 2. Watch authentication success rates and error logs. Verify SSO connections resolve for enterprise organizations. 3. Increase the percentage in stages until all traffic uses Scalekit. 4. Keep the Auth0 tenant read-only until you’re confident, so rollback stays available. ## Handle password-based users [Section titled “Handle password-based users”](#handle-password-based-users) Auth0 database users authenticate with a password hash that stays inside Auth0 and can’t be exported. Two paths keep those users signed in: * **Re-authentication (recommended).** On first visit after cutover, users sign in through SSO, social login, or [passwordless](/authenticate/auth-methods/passwordless/). No password moves, and the `external_id` mapping links them back to their imported record. * **Password-hash migration.** If you must carry password hashes over, the Scalekit Solutions team handles this directly. [Contact us](/support/contact-us) before you start the import. ## Common mistakes [Section titled “Common mistakes”](#common-mistakes) Users import but can’t sign in after cutover Confirm your callback URL is registered under **Settings > Redirects**, and that the email on the Auth0 record matches the Scalekit record exactly. Mismatched or missing `external_id` values also break reconciliation during a staged rollout. Roles don’t apply on first login Roles referenced on a membership must exist in Scalekit before import. Create them first, then re-run the affected user rows. Read access control from the `roles` claim, not from Auth0 scopes. Enterprise SSO fails for one customer Each organization needs its own SSO connection. Verify the connection is enabled for that organization and that the identity provider metadata matches what you recorded during export. Test with identity-provider-initiated login. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) * [Migrate to Full Stack Auth](/fsa/guides/migration-guide/): the vendor-neutral migration reference this recipe builds on. * [Add modular SSO](/authenticate/sso/add-modular-sso/): rebuild each enterprise connection in Scalekit. * [Create roles and permissions](/authenticate/authz/create-roles-permissions/): set up the roles your imported memberships reference. STYLE-CHECK: PASSED --- # DOCUMENT BOUNDARY --- # Build a multi-user GitHub PR summarizer agent > Build a GitHub PR summarizer that binds each connected GitHub account to a secure browser session instead of trusting a client-supplied user ID. This recipe builds a GitHub PR summarizer with a browser UI and a secure connected-account flow. Each user connects GitHub once, then the app reuses that connected token for later PR summary requests in the same browser session. The important security rule is straightforward: **never accept a user ID from the browser and use it as the Scalekit connected-account identifier**. Instead, mint an opaque identifier on the server, store it in your own session store, and complete the flow with [user verification for connected accounts](/agentkit/user-verification/). The finished app does four things: * lists the most-discussed open pull requests in a repository * fetches each PR’s diff and comment thread through Scalekit’s GitHub connector * asks an LLM to summarize the PRs in plain language * binds every GitHub connection to a secure browser session instead of a client-supplied identifier The complete source is available in the [render-ai-agent-deploykit](https://github.com/scalekit-developers/render-ai-agent-deploykit) repository. You can also [watch the video walkthrough](https://youtu.be/w3atzSkKE1w) to see the full setup and demo end-to-end. Why this cookbook stays TypeScript-only This sample uses Render’s Node SDK and ships as a TypeScript project, so the cookbook mirrors the repo and stays TypeScript-only. For multi-language examples of the verification flow itself, see [user verification for connected accounts](/agentkit/user-verification/). ## What you are building [Section titled “What you are building”](#what-you-are-building) The app runs as a Node web service on Render. It serves an HTML page with a **Connect GitHub** button and a form for `owner` and `repo`. Under the hood, the flow looks like this: ```text 1 Browser (original tab) Browser (new tab) 2 │ │ 3 ▼ GET / │ 4 Express server sets signed session cookie │ 5 │ │ 6 ▼ POST /api/auth │ 7 Scalekit returns GitHub auth link │ 8 │ │ 9 │ opens auth link ─────────────────► ▼ 10 │ GitHub OAuth consent 11 │ │ 12 │ polls GET /api/auth/status ▼ 13 │ ◄─── Scalekit API: ACTIVE ──► Scalekit verifies account 14 │ 15 ▼ page auto-reloads 16 │ 17 ▼ POST /api/summarize { repository } 18 Scalekit runs GitHub requests with the connected user's token ``` The OAuth flow opens in a **new tab** so the app page stays intact. The original tab polls the Scalekit API until the connected account becomes `ACTIVE`, then auto-reloads to show the connected state. ## 1. Set up the GitHub connector [Section titled “1. Set up the GitHub connector”](#1-set-up-the-github-connector) Create the connector once per Scalekit environment. 1. Go to [app.scalekit.com](https://app.scalekit.com) → **AgentKit** > **Connections** > **Create Connection** 2. Find **GitHub** and click **Create** 3. Follow the setup — Scalekit creates and manages the GitHub OAuth app for you 4. Note the **connection name** assigned (e.g. `github-qkHFhMip`) — you’ll set this as `GITHUB_CONNECTION_NAME` in your environment Connection names are unique per environment Scalekit generates a unique GitHub connection name for each environment. Do not copy one from a tutorial or another project. Always use the exact value from your own Scalekit Dashboard. ## 2. Configure user verification (required) [Section titled “2. Configure user verification (required)”](#2-configure-user-verification-required) Scalekit’s user verification setting controls what happens after a user completes GitHub OAuth. Set it in **AgentKit > Settings > User Verification** in the [Scalekit dashboard](https://app.scalekit.com). | Mode | When to use | What happens after OAuth | | ------------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **None** (the default for new environments) | Trying the app | The connected account goes `ACTIVE` as soon as OAuth completes, with no identity check. | | **Scalekit users only** | Development and testing | Scalekit verifies the user internally. The connected account goes `ACTIVE` automatically. The app detects this by polling the Scalekit API. | | **Custom user verifier** | Production | Scalekit redirects the browser to your app’s `/user/verify` callback. The server calls `verifyConnectedAccountUser` to activate the account. The app also polls the Scalekit API as a fallback. | The app works in every mode without code changes. Switch to **Custom user verifier** before you deploy for real users. Stuck on waiting for authorization? If you complete GitHub OAuth but the app never shows “GitHub connected,” check this setting first. With **Custom user verifier**, the account stays pending until your `/user/verify` route runs. The app builds that URL from the request’s origin, so open the app on its deployed URL, not a local one. For the full verification model, see [user verification for connected accounts](/agentkit/user-verification/). ## 3. Create the project [Section titled “3. Create the project”](#3-create-the-project) Terminal ```bash mkdir render-pr-summarizer && cd render-pr-summarizer npm init -y npm install @renderinc/sdk@0 @scalekit-sdk/node openai dotenv express npm install -D typescript tsx @types/node @types/express ``` package.json ```json { "type": "module", "scripts": { "dev": "tsx src/main.ts", "build": "tsc", "start": "node dist/main.js" } } ``` tsconfig.json ```json { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "rootDir": "src", "outDir": "dist", "strict": true, "skipLibCheck": true }, "include": ["src"] } ``` ## 4. Configure environment variables [Section titled “4. Configure environment variables”](#4-configure-environment-variables) Terminal ```bash cp .env.example .env ``` .env ```bash 1 PORT=3000 2 SESSION_SECRET=replace-with-openssl-rand-hex-32 3 4 OPENAI_API_KEY=your-api-key 5 OPENAI_MODEL=gpt-4.1-mini 6 # Leave OPENAI_BASE_URL empty for OpenAI direct. 7 # Set it to a proxy URL for LiteLLM, Azure OpenAI, Ollama, etc. 8 # OPENAI_BASE_URL=https://your-litellm-proxy.example.com 9 10 SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com 11 SCALEKIT_CLIENT_ID=your-scalekit-client-id 12 SCALEKIT_CLIENT_SECRET=your-scalekit-client-secret 13 GITHUB_CONNECTION_NAME=your-github-connection-name 14 15 # Optional — the app auto-detects its public URL from proxy headers. 16 # Only set this if you need to pin the callback origin explicitly. 17 # PUBLIC_BASE_URL=http://localhost:3000 ``` Generate `SESSION_SECRET` with: Terminal ```bash openssl rand -hex 32 ``` Any OpenAI-compatible API works The sample uses the `openai` npm package with a configurable `baseURL`. Set `OPENAI_BASE_URL` to route calls through LiteLLM, Azure OpenAI, Ollama, or any other OpenAI-compatible endpoint. The API key must match the endpoint it is sent to. PUBLIC_BASE_URL is optional The app infers its public URL from Render’s `x-forwarded-proto` and `host` proxy headers automatically. You only need to set `PUBLIC_BASE_URL` if you are behind a custom domain or an unusual reverse proxy. On first deploy to Render, you can leave it unset — the app works without it. ## 5. Add Scalekit auth helpers [Section titled “5. Add Scalekit auth helpers”](#5-add-scalekit-auth-helpers) The helper layer creates connected accounts, generates auth links, verifies the callback, and routes GitHub API calls through Scalekit’s connector. src/scalekit.ts ```typescript 1 import "dotenv/config"; 2 import { ConnectorStatus, ScalekitClient } from "@scalekit-sdk/node"; 3 import type { JsonObject } from "@bufbuild/protobuf"; 4 5 let _scalekit: ScalekitClient | null = null; 6 7 function getScalekit(): ScalekitClient { 8 if (_scalekit) return _scalekit; 9 if (!process.env.SCALEKIT_ENVIRONMENT_URL || !process.env.SCALEKIT_CLIENT_ID || !process.env.SCALEKIT_CLIENT_SECRET) { 10 throw new Error("Missing SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, or SCALEKIT_CLIENT_SECRET"); 11 } 12 _scalekit = new ScalekitClient( 13 process.env.SCALEKIT_ENVIRONMENT_URL, 14 process.env.SCALEKIT_CLIENT_ID, 15 process.env.SCALEKIT_CLIENT_SECRET, 16 ); 17 return _scalekit; 18 } 19 20 export const scalekit = new Proxy({} as ScalekitClient, { 21 get(_target, prop) { 22 return (getScalekit() as unknown as Record)[prop]; 23 }, 24 }); 25 26 const githubConnectionName = process.env.GITHUB_CONNECTION_NAME; 27 if (!githubConnectionName) { 28 throw new Error( 29 "GITHUB_CONNECTION_NAME is required. Copy the connection name from Scalekit Dashboard > AgentKit > Connections.", 30 ); 31 } 32 // Typed as string, so the functions below can use it under strict mode 33 const GITHUB_CONNECTION_NAME: string = githubConnectionName; 34 35 export async function getGitHubAuthLink( 36 identifier: string, 37 opts: { state: string; userVerifyUrl: string }, 38 ): Promise { 39 await scalekit.actions.getOrCreateConnectedAccount({ 40 connectionName: GITHUB_CONNECTION_NAME, 41 identifier, 42 }); 43 44 const res = await scalekit.actions.getAuthorizationLink({ 45 connectionName: GITHUB_CONNECTION_NAME, 46 identifier, 47 state: opts.state, 48 userVerifyUrl: opts.userVerifyUrl, 49 }); 50 51 if (!res.link) { 52 throw new Error( 53 `Scalekit did not return a GitHub authorization link for '${GITHUB_CONNECTION_NAME}' and identifier '${identifier}'`, 54 ); 55 } 56 57 return res.link; 58 } 59 60 export async function verifyUser(params: { 61 authRequestId: string; 62 identifier: string; 63 }): Promise { 64 await scalekit.actions.verifyConnectedAccountUser({ 65 authRequestId: params.authRequestId, 66 identifier: params.identifier, 67 }); 68 } 69 70 /** 71 * Check the connected account status via Scalekit API. 72 * Returns true when the account is active (OAuth complete and verified). 73 */ 74 export async function isAccountActive(identifier: string): Promise { 75 try { 76 const res = await scalekit.actions.getConnectedAccount({ 77 connectionName: GITHUB_CONNECTION_NAME, 78 identifier, 79 }); 80 return res.connectedAccount?.status === ConnectorStatus.ACTIVE; 81 } catch { 82 return false; 83 } 84 } 85 86 export async function githubTool( 87 identifier: string, 88 toolName: string, 89 toolInput: Record, 90 ): Promise { 91 const res = await scalekit.actions.executeTool({ 92 toolName, 93 toolInput, 94 connector: GITHUB_CONNECTION_NAME, 95 identifier, 96 }); 97 98 return res.data ?? {}; 99 } 100 101 export async function githubRequest( 102 identifier: string, 103 path: string, 104 options: { 105 method?: string; 106 headers?: Record; 107 queryParams?: Record; 108 } = {}, 109 ) { 110 const res = await scalekit.actions.request({ 111 connectionName: GITHUB_CONNECTION_NAME, 112 identifier, 113 path, 114 method: options.method ?? "GET", 115 headers: options.headers, 116 queryParams: options.queryParams, 117 }); 118 119 return res.data; 120 } ``` Use the exact connector name The `connector` value in `executeTool` must be the full connection name from your own Scalekit environment, not the generic provider string `"github"`. ## 6. Bind the browser session to an opaque identifier [Section titled “6. Bind the browser session to an opaque identifier”](#6-bind-the-browser-session-to-an-opaque-identifier) The session layer is the security boundary for the whole app. Create `src/session.ts` and store three things: * a signed session cookie sent to the browser * an opaque `usr_...` identifier stored on the server * a one-time `state` value stored on the server while OAuth is in flight src/session.ts ```typescript 1 import { createHmac, randomBytes, timingSafeEqual } from "node:crypto"; 2 import type { Request, Response } from "express"; 3 4 const COOKIE_NAME = "sid"; 5 const STATE_TTL_MS = 10 * 60 * 1000; 6 7 interface SessionEntry { 8 identifier: string; 9 pendingState?: string; 10 pendingStateExpiresAt?: number; 11 connectedAt?: number; 12 } 13 14 const store = new Map(); 15 16 function getSecret(): string { 17 const secret = process.env.SESSION_SECRET; 18 if (!secret) { 19 throw new Error("SESSION_SECRET is required"); 20 } 21 return secret; 22 } 23 24 function sign(sessionId: string): string { 25 const mac = createHmac("sha256", getSecret()).update(sessionId).digest("base64url"); 26 return `${sessionId}.${mac}`; 27 } 28 29 function unsign(signed: string): string | null { 30 const dot = signed.lastIndexOf("."); 31 if (dot < 0) return null; 32 33 const sessionId = signed.slice(0, dot); 34 const mac = signed.slice(dot + 1); 35 const expected = createHmac("sha256", getSecret()).update(sessionId).digest("base64url"); 36 37 const expectedBuf = Buffer.from(expected); 38 const macBuf = Buffer.from(mac); 39 if (expectedBuf.length !== macBuf.length) return null; 40 41 return timingSafeEqual(expectedBuf, macBuf) ? sessionId : null; 42 } 43 44 export function requireSession(req: Request, res: Response) { 45 const cookies = Object.fromEntries( 46 (req.headers.cookie ?? "") 47 .split(";") 48 .flatMap((pair) => { 49 const eq = pair.indexOf("="); 50 if (eq < 0) return []; 51 try { 52 return [[pair.slice(0, eq).trim(), decodeURIComponent(pair.slice(eq + 1).trim())]]; 53 } catch { 54 return []; 55 } 56 }), 57 ); 58 59 const raw = cookies[COOKIE_NAME]; 60 let sessionId = raw ? unsign(raw) : null; 61 let entry = sessionId ? store.get(sessionId) ?? null : null; 62 63 if (!sessionId || !entry) { 64 sessionId = randomBytes(32).toString("base64url"); 65 entry = { identifier: "" }; 66 store.set(sessionId, entry); 67 } 68 69 // The cookie only carries a random opaque session id. HMAC signing is enough 70 // to detect tampering because the sensitive identifier stays server-side. 71 const protoHeader = req.get("x-forwarded-proto"); 72 const requestIsSecure = req.secure || protoHeader?.split(",")[0]?.trim() === "https"; 73 const secure = 74 process.env.NODE_ENV === "production" || 75 process.env.PUBLIC_BASE_URL?.startsWith("https://") === true || 76 requestIsSecure; 77 const parts = [ 78 `${COOKIE_NAME}=${sign(sessionId)}`, 79 "HttpOnly", 80 "SameSite=Lax", 81 "Path=/", 82 `Max-Age=${7 * 24 * 60 * 60}`, 83 ]; 84 if (secure) parts.push("Secure"); 85 res.setHeader("Set-Cookie", parts.join("; ")); 86 87 return { entry }; 88 } 89 90 export function mintIdentifier(entry: SessionEntry): string { 91 if (!entry.identifier) { 92 entry.identifier = `usr_${randomBytes(16).toString("hex")}`; 93 } 94 return entry.identifier; 95 } 96 97 export function setPendingState(entry: SessionEntry, state: string): void { 98 entry.pendingState = state; 99 entry.pendingStateExpiresAt = Date.now() + STATE_TTL_MS; 100 } 101 102 export function consumePendingState(entry: SessionEntry, incoming: string): boolean { 103 const stored = entry.pendingState; 104 const expiresAt = entry.pendingStateExpiresAt; 105 entry.pendingState = undefined; 106 entry.pendingStateExpiresAt = undefined; 107 108 if (!stored || !expiresAt || Date.now() > expiresAt) return false; 109 110 const storedBuf = Buffer.from(stored); 111 const incomingBuf = Buffer.from(incoming); 112 if (storedBuf.length !== incomingBuf.length) return false; 113 114 return timingSafeEqual(storedBuf, incomingBuf); 115 } 116 117 export function markConnected(entry: SessionEntry): void { 118 entry.connectedAt = Date.now(); 119 } 120 121 export function isConnected(entry: SessionEntry): boolean { 122 return entry.connectedAt !== undefined; 123 } ``` Never trust query params for identity Read the identifier from your own session store, not from the URL and not from the request body. The callback query string only proves that Scalekit completed an OAuth flow. Your server must decide which local user session owns that new connection. ## 7. Add the tasks [Section titled “7. Add the tasks”](#7-add-the-tasks) The task layer now accepts a server-side `identifier`, not a browser-supplied `userId`. src/tasks.ts ```typescript 1 import { task } from "@renderinc/sdk/workflows"; 2 import OpenAI from "openai"; 3 import { githubRequest, githubTool, getGitHubAuthLink } from "./scalekit.js"; 4 5 export interface PRSummaryInput { 6 identifier: string; 7 owner: string; 8 repo: string; 9 } 10 11 const fetchOpenPRs = task( 12 { name: "fetchOpenPRs", retry: { maxRetries: 3, waitDurationMs: 1000 } }, 13 async function fetchOpenPRs(identifier: string, owner: string, repo: string) { 14 const raw = await githubTool(identifier, "github_pull_requests_list", { 15 owner, 16 repo, 17 state: "open", 18 }); 19 20 const r = raw as Record; 21 const list = Array.isArray(raw) 22 ? raw 23 : Array.isArray(r.array) ? r.array 24 : Array.isArray(r.pull_requests) ? r.pull_requests 25 : Array.isArray(r.data) ? r.data 26 : null; 27 28 if (!list) { 29 throw new Error(`Unexpected response shape: ${JSON.stringify(raw).slice(0, 200)}`); 30 } 31 32 type PRItem = { number: number; title: string; comments: number; review_comments: number }; 33 return (list as PRItem[]) 34 .sort((a, b) => (b.comments + b.review_comments) - (a.comments + a.review_comments)) 35 .slice(0, 5); 36 }, 37 ); 38 39 const fetchPRDetails = task( 40 { name: "fetchPRDetails", retry: { maxRetries: 3, waitDurationMs: 1000 } }, 41 async function fetchPRDetails(identifier: string, owner: string, repo: string, prNumber: number) { 42 const [diffRaw, commentsRaw] = await Promise.all([ 43 githubRequest(identifier, `/repos/${owner}/${repo}/pulls/${prNumber}`, { 44 headers: { Accept: "application/vnd.github.diff" }, 45 }), 46 githubRequest(identifier, `/repos/${owner}/${repo}/issues/${prNumber}/comments`), 47 ]); 48 49 const diff = typeof diffRaw === "string" ? diffRaw.slice(0, 3000) : ""; 50 const comments = Array.isArray(commentsRaw) ? commentsRaw : []; 51 52 return { diff, comments }; 53 }, 54 ); 55 56 export const setupGitHubAuthTask = task( 57 { name: "setupGitHubAuth" }, 58 async function setupGitHubAuth(params: { 59 identifier: string; 60 state: string; 61 userVerifyUrl: string; 62 }) { 63 const link = await getGitHubAuthLink(params.identifier, { 64 state: params.state, 65 userVerifyUrl: params.userVerifyUrl, 66 }); 67 68 return { authLink: link }; 69 }, 70 ); 71 72 // ---- LLM summary ---- 73 74 function createOpenAIClient(): OpenAI { 75 const apiKey = process.env.OPENAI_API_KEY; 76 if (!apiKey) throw new Error("OPENAI_API_KEY not set"); 77 return new OpenAI({ 78 apiKey, 79 ...(process.env.OPENAI_BASE_URL && { baseURL: process.env.OPENAI_BASE_URL }), 80 }); 81 } 82 83 const generateSummary = task( 84 { name: "generateSummary", retry: { maxRetries: 3, waitDurationMs: 2000 } }, 85 async function generateSummary( 86 prs: { number: number; title: string; diff: string; comments: { body?: string }[] }[], 87 owner: string, 88 repo: string, 89 ): Promise { 90 if (prs.length === 0) return "No open pull requests found in this repository."; 91 92 const client = createOpenAIClient(); 93 const prBlocks = prs 94 .map((pr) => { 95 const bodies = pr.comments.slice(0, 5).map((c) => `> ${(c.body ?? "").slice(0, 300)}`).join("\n"); 96 return `PR #${pr.number} — ${pr.title}\n${bodies || "No comments."}\nDiff:\n${pr.diff || "(not available)"}`; 97 }) 98 .join("\n\n---\n\n"); 99 100 const response = await client.chat.completions.create({ 101 model: process.env.OPENAI_MODEL ?? "gpt-4.1-mini", 102 messages: [ 103 { 104 role: "system", 105 content: 106 "Summarize each PR in one paragraph (3-4 sentences) for a team lead. " + 107 "Cover what it does, how much discussion happened, and whether it looks close to merging.", 108 }, 109 { role: "user", content: `Repository: ${owner}/${repo}\n\n${prBlocks}` }, 110 ], 111 }); 112 113 return response.choices[0].message.content ?? "(no summary generated)"; 114 }, 115 ); 116 117 // ---- Root task ---- 118 119 export const summarizePRsTask = task( 120 { name: "summarizePRs", timeoutSeconds: 120 }, 121 async function summarizePRs(input: PRSummaryInput) { 122 const { identifier, owner, repo } = input; 123 const topPRs = await fetchOpenPRs(identifier, owner, repo); 124 125 if (topPRs.length === 0) { 126 return { repository: `${owner}/${repo}`, prsAnalyzed: [] as string[], summary: "No open pull requests found." }; 127 } 128 129 const details = await Promise.all( 130 topPRs.map((pr) => fetchPRDetails(identifier, owner, repo, pr.number)), 131 ); 132 133 const prsForSummary = topPRs.map((pr, i) => ({ 134 number: pr.number, 135 title: pr.title, 136 diff: details[i].diff, 137 comments: details[i].comments as { body?: string }[], 138 })); 139 140 const summary = await generateSummary(prsForSummary, owner, repo); 141 142 return { 143 repository: `${owner}/${repo}`, 144 prsAnalyzed: topPRs.map((p) => `#${p.number}: ${p.title}`), 145 summary, 146 }; 147 }, 148 ); ``` ## 8. Wire the HTTP server [Section titled “8. Wire the HTTP server”](#8-wire-the-http-server) The HTTP server owns the secure flow. It issues the session cookie, starts the GitHub auth flow, validates the callback, and blocks summary requests until the session is connected. src/server.ts ```typescript 1 import crypto from "node:crypto"; 2 import express from "express"; 3 import { setupGitHubAuthTask, summarizePRsTask } from "./tasks.js"; 4 import { isAccountActive, verifyUser } from "./scalekit.js"; 5 import { 6 consumePendingState, 7 isConnected, 8 markConnected, 9 mintIdentifier, 10 requireSession, 11 setPendingState, 12 } from "./session.js"; 13 import { renderHomePage, renderAuthCompletePage } from "./views.js"; 14 import type { Request } from "express"; 15 16 function getConfiguredPublicBaseUrl(): string | null { 17 const value = process.env.PUBLIC_BASE_URL; 18 return value ? value.replace(/\/$/, "") : null; 19 } 20 21 function getRequestOrigin(req: Request): string { 22 const configured = getConfiguredPublicBaseUrl(); 23 if (configured) return configured; 24 25 const protoHeader = req.get("x-forwarded-proto"); 26 const proto = protoHeader?.split(",")[0]?.trim() || req.protocol || "http"; 27 const host = req.get("x-forwarded-host") || req.get("host"); 28 if (!host) { 29 throw new Error("Could not determine the public origin for this request"); 30 } 31 return `${proto}://${host}`; 32 } 33 34 export function startServer(): void { 35 const app = express(); 36 app.set("trust proxy", true); 37 app.use(express.json()); 38 39 app.get("/", (req, res) => { 40 const { entry } = requireSession(req, res); 41 res.type("html").send(renderHomePage({ connected: isConnected(entry) })); 42 }); 43 44 // Polled by the original tab while the OAuth tab is open. 45 // Checks the in-memory session first, then queries the Scalekit API 46 // to detect when the connected account becomes ACTIVE. 47 app.get("/api/auth/status", async (req, res) => { 48 const { entry } = requireSession(req, res); 49 if (isConnected(entry)) { 50 res.json({ connected: true }); 51 return; 52 } 53 if (entry.identifier && await isAccountActive(entry.identifier)) { 54 markConnected(entry); 55 res.json({ connected: true }); 56 return; 57 } 58 res.json({ connected: false }); 59 }); 60 61 app.post("/api/auth", async (req, res) => { 62 const { entry } = requireSession(req, res); 63 const identifier = mintIdentifier(entry); 64 65 const state = crypto.randomUUID(); 66 setPendingState(entry, state); 67 68 const result = await setupGitHubAuthTask({ 69 identifier, 70 state, 71 userVerifyUrl: `${getRequestOrigin(req)}/user/verify`, 72 }); 73 74 res.json({ authLink: result.authLink }); 75 }); 76 77 // Callback for custom user verification mode. When Scalekit is 78 // configured in "Scalekit users only" mode, this route may not fire — 79 // the /api/auth/status polling handles that case via the Scalekit API. 80 app.get("/user/verify", async (req, res) => { 81 const { auth_request_id, state } = req.query as Record; 82 if (!auth_request_id || !state) { 83 res.status(400).send("Missing auth_request_id or state"); 84 return; 85 } 86 87 const { entry } = requireSession(req, res); 88 if (!entry.identifier) { 89 res.status(400).send("No pending authorization for this session"); 90 return; 91 } 92 93 if (!consumePendingState(entry, state)) { 94 res.status(400).send("Invalid or expired state"); 95 return; 96 } 97 98 await verifyUser({ 99 authRequestId: auth_request_id, 100 identifier: entry.identifier, 101 }); 102 103 markConnected(entry); 104 // This handler runs in the OAuth tab. Render a minimal page 105 // telling the user to close it — the original tab is polling 106 // /api/auth/status and will auto-reload. 107 res.type("html").send(renderAuthCompletePage()); 108 }); 109 110 app.post("/api/summarize", async (req, res) => { 111 const { entry } = requireSession(req, res); 112 if (!isConnected(entry)) { 113 res.status(401).json({ error: "Connect your GitHub account first" }); 114 return; 115 } 116 117 // The UI sends { repository: "https://github.com/owner/repo" } or "owner/repo". 118 // Parse the string into separate owner and repo values. 119 const { repository } = req.body as { repository?: string }; 120 if (!repository) { 121 res.status(400).json({ error: "Provide a GitHub repository URL or owner/repo name." }); 122 return; 123 } 124 125 let owner: string | undefined; 126 let repo: string | undefined; 127 try { 128 const url = new URL(repository); 129 const segments = url.pathname.split("/").filter(Boolean); 130 owner = segments[0]; 131 repo = segments[1]?.replace(/\.git$/, ""); 132 } catch { 133 const parts = repository.split("/"); 134 owner = parts[0]; 135 repo = parts[1]?.replace(/\.git$/, ""); 136 } 137 138 if (!owner || !repo) { 139 res.status(400).json({ error: "Provide a GitHub repository URL or owner/repo name." }); 140 return; 141 } 142 143 const result = await summarizePRsTask({ identifier: entry.identifier, owner, repo }); 144 res.json(result); 145 }); 146 147 // Render sets PORT; locally the app listens on 3000 148 const port = Number(process.env.PORT ?? 3000); 149 app.listen(port, () => { 150 console.log(`Listening on http://localhost:${port}`); 151 }); 152 } ``` Then add the entry point that `npm run dev` and `npm start` run: src/main.ts ```typescript import { startServer } from "./server.js"; startServer(); ``` ## 9. Render the browser UI [Section titled “9. Render the browser UI”](#9-render-the-browser-ui) The UI only asks for a repository. It does not ask for a user identifier. After a successful connection, the page auto-reloads and shows a connected banner. The key change from a naive implementation: `connectGitHub()` opens the auth link in a **new tab** instead of navigating the current page. This keeps the app intact even if the OAuth redirect chain doesn’t return cleanly. The original tab polls `/api/auth/status` and auto-reloads when the Scalekit API reports the account as `ACTIVE`. src/views.ts ```typescript 1 export function renderAuthCompletePage(): string { 2 return ` 3 4 5
6

✓ GitHub connected

7

You can close this tab and return to the app. The original page will update automatically.

8
9 10 `; 11 } 12 13 export function renderHomePage({ connected }: { connected: boolean }): string { 14 const connectedBanner = connected 15 ? `
✓ GitHub connected
` 16 : `
Connect GitHub before summarizing pull requests.
`; 17 const authButtonLabel = connected ? "Reconnect GitHub" : "Connect GitHub"; 18 19 return ` 20 21 22 ${connectedBanner} 23 24
25 26 27

28
      
88
    
89
  `;
90
}
```

## 10. Run locally

[Section titled “10. Run locally”](#10-run-locally)

1. Copy `.env.example` to `.env` and fill in your values.
2. Run `npm install`.
3. Run `npm run dev`.
4. Open `http://localhost:3000`.
5. Click **Connect GitHub**. A new tab opens for the GitHub OAuth flow.
6. Complete the OAuth consent in the new tab.
7. The new tab shows “GitHub connected — you can close this tab” (in custom verification mode) or a Scalekit success page (in Scalekit-users-only mode).
8. The original tab auto-detects the connection and reloads, showing a **GitHub connected** banner.
9. Enter a repository URL or `owner/repo`, then generate a summary.

Public repositories work with any connected GitHub account. Private repositories only work if the connected account has access.

## 11. Deploy to Render

[Section titled “11. Deploy to Render”](#11-deploy-to-render)

Render deploys the app as a web service from `render.yaml`.

Set these environment variables in Render:

| Variable                   | Required | Notes                                                                 |
| -------------------------- | -------- | --------------------------------------------------------------------- |
| `SCALEKIT_ENVIRONMENT_URL` | Yes      | From Scalekit dashboard → Developers → API Credentials                |
| `SCALEKIT_CLIENT_ID`       | Yes      | Same location                                                         |
| `SCALEKIT_CLIENT_SECRET`   | Yes      | Same location                                                         |
| `GITHUB_CONNECTION_NAME`   | Yes      | From AgentKit → Connectors                                            |
| `OPENAI_API_KEY`           | Yes      | OpenAI key or proxy token                                             |
| `OPENAI_BASE_URL`          | No       | Leave empty for OpenAI direct. Set for LiteLLM/Azure/Ollama.          |
| `OPENAI_MODEL`             | No       | Default: `gpt-4.1-mini`                                               |
| `SESSION_SECRET`           | Auto     | `render.yaml` auto-generates this                                     |
| `PUBLIC_BASE_URL`          | No       | Auto-detected from proxy headers. Only needed behind a custom domain. |

After deploying, configure user verification in the Scalekit dashboard ([step 2](#2-configure-user-verification-required)). The app will not complete the GitHub connection flow without this.

## Production notes

[Section titled “Production notes”](#production-notes)

* **User verification mode**: Switch to **Custom user verifier** in the Scalekit dashboard before going to production. This ensures your backend confirms which session owns each new connection.
* **Shared session store**: The sample stores session data in memory. Use Redis or a database-backed shared store in production.
* **Short-lived OAuth state**: The sample expires the pending `state` after 10 minutes and consumes it after a single callback.
* **Session-bound identifier**: The browser never chooses the identifier that Scalekit uses to look up the connected account.
* **Connector-backed GitHub requests**: The sample routes both PR listing and PR detail fetches through Scalekit so the connected user’s token is used consistently.

## Next steps

[Section titled “Next steps”](#next-steps)

* Read [user verification for connected accounts](/agentkit/user-verification/) for the full verification model and additional examples.
* Read [authorize a user](/agentkit/tools/authorize/) for the status-polling pattern used to detect when a connected account becomes `ACTIVE`.
* Open the [render-ai-agent-deploykit](https://github.com/scalekit-developers/render-ai-agent-deploykit) repository to compare the full implementation against the snippets in this cookbook.

---
# DOCUMENT BOUNDARY
---

# Build an agent that books meetings and drafts emails

> Connect a Python agent to Google Calendar and Gmail via Scalekit to find free slots, book meetings, and draft follow-up emails.

Scheduling a meeting sounds simple: find a free slot, create an event, send a confirmation. But in an agent, each of those steps crosses a tool boundary — and each tool requires its own OAuth token. Without a managed auth layer, you end up writing token-fetching, refresh logic, and error handling three times over before you write a single line of scheduling logic. This cookbook solves that by using Scalekit to own the OAuth lifecycle for each connector, so your agent can focus on the workflow itself.

This is a Python recipe for agents that call two or more external APIs on behalf of a user. The code on this page is the complete script: save the steps below in one file, `meeting_scheduler_agent.py`.

**The core problems this solves:**

* **One token per connector** — Google Calendar and Gmail use separate OAuth scopes and separate access tokens. Your agent must manage both independently.
* **First-run authorization is blocking** — If the user has not yet authorized a connector, your agent cannot proceed until they complete the browser OAuth flow.
* **Token expiry is silent** — A token that worked yesterday fails today, and the failure looks identical to a permissions error.
* **Chaining tool outputs is fragile** — The event link from the Calendar API needs to appear in the Gmail draft. If the Calendar call fails mid-workflow, the draft gets a broken link or never gets created.

Scalekit exposes a `connected_accounts` abstraction that maps a user ID to an authorized OAuth session per connector. When your agent calls `get_or_create_connected_account`, Scalekit returns the user’s account for that connector. If it isn’t `ACTIVE` yet, `get_authorization_link` gives you a URL for the user to authorize. From then on, Scalekit stores the tokens and refreshes them automatically.

Your agent never handles a token. It calls `actions.execute_tool` with a tool name, such as `googlecalendar_query_freebusy`, and Scalekit makes the Google API call with the right user’s credentials. The authorization step is one function for every connector, and each API call is one `execute_tool` call.

1. **Set up the environment**

   Create a `.env` file at the project root with your Scalekit credentials:

   ```bash
   SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com
   SCALEKIT_CLIENT_ID=your-client-id
   SCALEKIT_CLIENT_SECRET=your-client-secret
   ```

   Install dependencies:

   ```bash
   pip install scalekit-sdk-python python-dotenv
   ```

   In the Scalekit Dashboard, create two connections for your environment:

   * `googlecalendar` — Google Calendar OAuth connection
   * `gmail` — Gmail OAuth connection

   The script references these names literally. The names must match exactly.

2. **Initialize the Scalekit client**

   meeting_scheduler_agent.py

   ```python
   1
   import os
   2
   from datetime import datetime, timezone, timedelta
   3


   4
   from dotenv import load_dotenv
   5
   from scalekit import ScalekitClient
   6


   7
   load_dotenv()
   8


   9
   # Never hard-code credentials — they would be exposed in source control
   10
   # and CI logs. Pull them from environment variables instead.
   11
   scalekit_client = ScalekitClient(
   12
       env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
   13
       client_id=os.getenv("SCALEKIT_CLIENT_ID"),
   14
       client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
   15
   )
   16


   17
   actions = scalekit_client.actions
   18


   19
   # Replace with a real user identifier from your application's session
   20
   USER_ID = "user_123"
   21
   ATTENDEE_EMAIL = "attendee@example.com"
   22
   MEETING_TITLE = "Quick Sync"
   23
   DURATION_MINUTES = 60
   24
   SEARCH_DAYS = 3
   25
   WORK_START_HOUR = 9   # UTC
   26
   WORK_END_HOUR = 17    # UTC
   ```

   `scalekit_client.actions` is the entry point for all connected-account operations. Initialize it once and pass `actions` to the functions below.

3. **Authorize each connector**

   The `authorize` function makes sure the user has an active connected account for a connector. On first run, it prints an authorization link and waits for the user to finish the browser OAuth flow:

   ```python
   1
   def authorize(connector: str) -> None:
   2
       """Ensure the user has an ACTIVE connected account for this connector."""
   3
       response = actions.get_or_create_connected_account(
   4
           connection_name=connector,
   5
           identifier=USER_ID,
   6
       )
   7


   8
       if response.connected_account.status != "ACTIVE":
   9
           link_response = actions.get_authorization_link(
   10
               connection_name=connector,
   11
               identifier=USER_ID,
   12
           )
   13
           print(f"\nOpen this link to authorize {connector}:\n{link_response.link}\n")
   14
           input("Press Enter after completing authorization in your browser…")
   15


   16
           response = actions.get_connected_account(
   17
               connection_name=connector,
   18
               identifier=USER_ID,
   19
           )
   20
           if response.connected_account.status != "ACTIVE":
   21
               raise RuntimeError(f"{connector} is still not authorized")
   ```

   Call it once per connector before any tool calls:

   ```python
   authorize("googlecalendar")
   authorize("gmail")
   ```

   After the first successful authorization, the account is already `ACTIVE` on later runs and the `if` block is skipped. Scalekit refreshes expired tokens automatically.

4. **Query calendar availability**

   Call `googlecalendar_query_freebusy` to get the user’s busy intervals:

   ```python
   1
   def get_busy_slots() -> list[dict]:
   2
       """Fetch busy intervals for the user's primary calendar."""
   3
       now = datetime.now(timezone.utc)
   4
       window_end = now + timedelta(days=SEARCH_DAYS)
   5


   6
       result = actions.execute_tool(
   7
           tool_name="googlecalendar_query_freebusy",
   8
           connection_name="googlecalendar",
   9
           identifier=USER_ID,
   10
           tool_input={
   11
               "calendar_ids": ["primary"],
   12
               "time_min": now.isoformat(),
   13
               "time_max": window_end.isoformat(),
   14
           },
   15
       )
   16
       return result.data["calendars"]["primary"]["busy"]
   ```

   The tool returns Google’s free/busy response as `result.data`. If the call fails, `execute_tool` raises an exception with the error from Google, so the caller never gets a silently wrong result. The `busy` list contains `{"start": "...", "end": "..."}` dicts with ISO 8601 timestamps.

5. **Find the first open slot**

   Walk forward in one-hour increments from now and return the first candidate that falls within working hours and does not overlap a busy interval:

   ```python
   1
   def parse_time(value: str) -> datetime:
   2
       # Google returns UTC times with a trailing Z, which fromisoformat
   3
       # accepts only from Python 3.11
   4
       return datetime.fromisoformat(value.replace("Z", "+00:00"))
   5


   6


   7
   def find_free_slot(busy_slots: list[dict]) -> tuple[datetime, datetime] | None:
   8
       """Return the first open one-hour slot during working hours in UTC.
   9


   10
       Returns None if no slot is available in the search window.
   11
       """
   12
       now = datetime.now(timezone.utc)
   13
       # Round up to the next whole hour so the candidate is always in the future
   14
       candidate = now.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)
   15
       window_end = now + timedelta(days=SEARCH_DAYS)
   16


   17
       while candidate < window_end:
   18
           slot_end = candidate + timedelta(minutes=DURATION_MINUTES)
   19


   20
           if WORK_START_HOUR <= candidate.hour < WORK_END_HOUR:
   21
               overlap = any(
   22
                   candidate < parse_time(b["end"]) and slot_end > parse_time(b["start"])
   23
                   for b in busy_slots
   24
               )
   25
               if not overlap:
   26
                   return candidate, slot_end
   27


   28
           candidate += timedelta(hours=1)
   29


   30
       return None
   ```

   This is a useful first-draft strategy: simple, readable, easy to debug. Its limits are real (one-hour granularity, UTC-only, primary calendar only) and addressed in [Production notes](#production-notes) below.

6. **Create the calendar event**

   Call `googlecalendar_create_event` and return the event’s link, which you’ll include in the email draft:

   ```python
   1
   def create_event(start: datetime) -> str:
   2
       """Create a calendar event and return its link."""
   3
       result = actions.execute_tool(
   4
           tool_name="googlecalendar_create_event",
   5
           connection_name="googlecalendar",
   6
           identifier=USER_ID,
   7
           tool_input={
   8
               "summary": MEETING_TITLE,
   9
               "description": "Scheduled by agent",
   10
               "start_datetime": start.isoformat(),
   11
               "event_duration_minutes": DURATION_MINUTES,
   12
               "timezone": "UTC",
   13
               "attendees_emails": [ATTENDEE_EMAIL],
   14
           },
   15
       )
   16
       return result.data["event"]["htmlLink"]
   ```

   The tool returns the created event under `event`, and its `htmlLink` is the calendar event URL. Google also sends an invitation email to each attendee automatically when the event is created; the draft you create in the next step is a separate follow-up, not the invitation itself.

7. **Draft the confirmation email**

   Call `gmail_create_draft` with the recipient, subject and body. The tool builds the email message for you:

   ```python
   1
   def create_draft(event_link: str, start: datetime) -> None:
   2
       """Create a Gmail draft with the meeting details."""
   3
       body = (
   4
           f"Hi,\n\n"
   5
           f"I've scheduled '{MEETING_TITLE}' for "
   6
           f"{start.strftime('%A, %B %d at %H:%M UTC')} ({DURATION_MINUTES} min).\n\n"
   7
           f"Calendar link: {event_link}\n\n"
   8
           f"Looking forward to it!"
   9
       )
   10


   11
       actions.execute_tool(
   12
           tool_name="gmail_create_draft",
   13
           connection_name="gmail",
   14
           identifier=USER_ID,
   15
           tool_input={
   16
               "to": ATTENDEE_EMAIL,
   17
               "subject": f"Invitation: {MEETING_TITLE}",
   18
               "body": body,
   19
           },
   20
       )
   21
       print("Draft created in Gmail.")
   ```

   The script creates a draft, not a sent message. The user reviews it before sending. This is the right default for an agent — it takes the action but keeps a human in the loop for outbound communication.

8. **Wire it together**

   ```python
   1
   def main() -> None:
   2
       print("Authorizing Google Calendar…")
   3
       authorize("googlecalendar")
   4


   5
       print("Authorizing Gmail…")
   6
       authorize("gmail")
   7


   8
       print("Checking calendar availability…")
   9
       busy_slots = get_busy_slots()
   10


   11
       slot = find_free_slot(busy_slots)
   12
       if not slot:
   13
           print(f"No free slot found in the next {SEARCH_DAYS} days.")
   14
           return
   15


   16
       start, end = slot
   17
       print(f"Found slot: {start.strftime('%A %B %d, %H:%M')} UTC")
   18


   19
       print("Creating calendar event…")
   20
       event_link = create_event(start)
   21
       print(f"Event created: {event_link}")
   22


   23
       print("Creating Gmail draft…")
   24
       create_draft(event_link, start)
   25


   26


   27
   if __name__ == "__main__":
   28
       main()
   ```

## Testing

[Section titled “Testing”](#testing)

Run the agent from the command line:

```bash
python meeting_scheduler_agent.py
```

On first run, you should see two authorization prompts in sequence:

```plaintext
1
Authorizing Google Calendar…
2


3
Open this link to authorize googlecalendar:
4
https://accounts.google.com/o/oauth2/auth?...
5


6
Press Enter after completing authorization in your browser…
7


8
Authorizing Gmail…
9


10
Open this link to authorize gmail:
11
https://accounts.google.com/o/oauth2/auth?...
12


13
Press Enter after completing authorization in your browser…
14


15
Checking calendar availability…
16
Found slot: Wednesday March 11, 10:00 UTC
17
Creating calendar event…
18
Event created: https://calendar.google.com/calendar/event?eid=...
19
Creating Gmail draft…
20
Draft created in Gmail.
```

On subsequent runs, the authorization prompts are skipped and the agent goes straight to availability checking.

Verify the results:

1. Open Google Calendar — you should see the event on the chosen date
2. Open Gmail — you should see a draft in the Drafts folder with the event link

## Common mistakes

[Section titled “Common mistakes”](#common-mistakes)

* **Connection name mismatch** — If you name the Scalekit connection `google-calendar` instead of `googlecalendar`, `get_or_create_connected_account` returns an error. The name in the Dashboard must match the `connection_name` in the script exactly.

* **Missing OAuth scopes** — If a tool call fails with `403 Forbidden`, the connection is missing a required scope. Calendar needs `https://www.googleapis.com/auth/calendar` and Gmail needs `https://www.googleapis.com/auth/gmail.compose`. Add them to the connection in the Scalekit Dashboard, and to your OAuth app if you use your own credentials, then authorize again.

* **UTC times without timezone info** — Passing a naive `datetime` (without `timezone.utc`) to `isoformat()` produces a string without a UTC offset, and Google Calendar rejects it. Always construct datetimes with `timezone.utc`.

* **`USER_ID` not matching your session** — The script uses a hardcoded `"user_123"`. In production, replace this with the actual user ID from your application’s session. A mismatch means the tool calls act on the wrong user’s accounts.

## Production notes

[Section titled “Production notes”](#production-notes)

**Timezone handling** — The working-hours check (`WORK_START_HOUR`, `WORK_END_HOUR`) is UTC-only. In production, convert the user’s local timezone and the attendee’s timezone before searching. The `zoneinfo` module (Python 3.9+) handles this without third-party dependencies.

**Slot granularity** — The one-hour increment misses 30- and 15-minute openings. For real scheduling, use the busy intervals directly to calculate the gaps between events, then filter by minimum duration.

**Multiple calendars** — The free/busy query checks only `primary`. Users who manage work and personal calendars separately will show false availability. Add their other calendar IDs to `calendar_ids`; `googlecalendar_list_calendars` returns them.

**Draft vs send** — Creating a draft is safer for a first deployment. When you’re confident in the agent’s output quality, switch from `gmail_create_draft` to `gmail_send_message` to make the agent fully autonomous. Add a confirmation step before making this change.

**Error recovery** — If `create_event` succeeds but `create_draft` fails, you have an orphaned event with no follow-up email. In production, wrap the two calls in a compensation pattern: keep the event’s `id` from `result.data["event"]` and delete it with `googlecalendar_delete_event` if the draft creation fails.

**Rate limits** — Google Calendar and Gmail both have per-user quotas. If your agent runs frequently for the same user, add exponential backoff around the `execute_tool` calls.

## Next steps

[Section titled “Next steps”](#next-steps)

* **Add user input** — Replace the hardcoded `ATTENDEE_EMAIL`, `MEETING_TITLE`, and `DURATION_MINUTES` with parameters parsed from natural language using an LLM tool call.
* **Build the JavaScript equivalent** — The Node.js SDK has the same calls: `getOrCreateConnectedAccount`, `getAuthorizationLink` and `executeTool`.
* **Handle re-authorization** — If a user revokes access, the account’s status is no longer `ACTIVE` and tool calls fail. Catch that, send the user a new authorization link, and retry instead of crashing.
* **Explore other connectors** — The same `authorize()` pattern works for any Scalekit-supported connector: Slack, Notion, Jira. Swap the connection name and call that connector’s tools. Each connector page lists its tools.
* **Review the Scalekit AgentKit quickstart** — For a broader overview of the connected-accounts model, see the [AgentKit quickstart](/agentkit/quickstart).

---
# DOCUMENT BOUNDARY
---

# Enforce seat limits with SCIM provisioning

> Block over-quota user creation and alert admins when SCIM pushes users beyond your plan seat limit.

SCIM (System for Cross-domain Identity Management) provisioning runs unsupervised. When a customer’s HR system pushes user #51 to a 50-seat plan, your application will create that user unless you explicitly block it. Scalekit delivers the provisioning events; your application decides whether to act on them.

This cookbook shows the two-event pattern that keeps your seat count accurate and tells admins when they need to upgrade their plan.

Full Stack Auth handles this automatically

This pattern applies to **Modular SCIM** customers who manage their own user database. If you use Scalekit Full Stack Auth, seat enforcement is built in — you don’t need this cookbook.

## SCIM does not enforce seat limits — your app must

[Section titled “SCIM does not enforce seat limits — your app must”](#scim-does-not-enforce-seat-limits--your-app-must)

Scalekit translates IdP-specific provisioning protocols into a consistent set of webhook events. It does not know your billing model, your seat limits, or which organizations have room for more users. That logic lives in your application.

When a user is added in the IdP, Scalekit fires `organization.directory.user_created`. When a user is removed or deactivated, Scalekit fires `organization.directory.user_deleted`. Your webhook handler is the gate between those events and your user table.

## Two webhook events carry the full user lifecycle

[Section titled “Two webhook events carry the full user lifecycle”](#two-webhook-events-carry-the-full-user-lifecycle)

Both events include the `organization_id`, which lets you look up the seat limit for that specific customer.

| Event                                 | When it fires                     | What to do                                            |
| ------------------------------------- | --------------------------------- | ----------------------------------------------------- |
| `organization.directory.user_created` | IdP adds or activates a user      | Check count — create user or block and notify         |
| `organization.directory.user_deleted` | IdP removes or deactivates a user | Decrement count — clear any blocked-provisioning flag |

## Track a user count per organization in your database

[Section titled “Track a user count per organization in your database”](#track-a-user-count-per-organization-in-your-database)

Add a table that stores the provisioned user count and seat limit for each organization. The examples below use plain SQL — translate to your ORM if preferred.

db/schema.sql

```sql
CREATE TABLE org_seat_usage (
  org_id       TEXT PRIMARY KEY,
  seat_limit   INTEGER NOT NULL,
  used_seats   INTEGER NOT NULL DEFAULT 0
);
```

Seed this table when you onboard a new customer. Update `seat_limit` whenever the customer upgrades or downgrades their plan.

## Block creation when the count reaches the limit

[Section titled “Block creation when the count reaches the limit”](#block-creation-when-the-count-reaches-the-limit)

The `user_created` handler increments the seat counter and creates the user only when there is room. Always return `200` to Scalekit — returning an error code causes Scalekit to retry delivery, which does not help when the block is intentional.

Verify webhook signatures before processing

Always verify that events come from Scalekit before acting on them. An unverified endpoint that mutates your database can be triggered by forged requests. See the [SCIM provisioning quickstart](/directory/scim/quickstart/) for how to verify signatures using the Scalekit SDK.

Keep the lock inside the transaction

The `SELECT ... FOR UPDATE` must run inside the same explicit transaction as the `INSERT` and `UPDATE`. In autocommit mode, a `FOR UPDATE` outside a transaction is released immediately after the select — it provides no protection against concurrent writes.

* Node.js

  webhook-handler.ts

  ```ts
  1
  import express from 'express'
  2


  3
  const app = express()
  4
  app.use(express.json())
  5


  6
  app.post('/webhooks/scalekit', async (req, res) => {
  7
    const event = req.body
  8


  9
    if (event.type === 'organization.directory.user_created') {
  10
      const orgId = event.organization_id
  11
      const directoryUser = event.data
  12
      let seatLimitReached = false
  13


  14
      // Run the check and insert in a single transaction.
  15
      // FOR UPDATE inside the transaction holds the lock until commit.
  16
      await db.transaction(async (tx) => {
  17
        const usage = await tx.queryOne(
  18
          'SELECT seat_limit, used_seats FROM org_seat_usage WHERE org_id = $1 FOR UPDATE',
  19
          [orgId]
  20
        )
  21


  22
        if (!usage || usage.used_seats >= usage.seat_limit) {
  23
          seatLimitReached = true
  24
          return
  25
        }
  26


  27
        await tx.query(
  28
          'INSERT INTO users (id, org_id, email, name) VALUES ($1, $2, $3, $4)',
  29
          [directoryUser.id, orgId, directoryUser.email, directoryUser.name]
  30
        )
  31
        await tx.query(
  32
          'UPDATE org_seat_usage SET used_seats = used_seats + 1 WHERE org_id = $1',
  33
          [orgId]
  34
        )
  35
      })
  36


  37
      if (seatLimitReached) {
  38
        // Seat limit reached — skip user creation and alert the admin.
  39
        await notifyAdminSeatLimitReached(orgId)
  40
      }
  41
    }
  42


  43
    // Return 200 so Scalekit does not retry this event.
  44
    res.sendStatus(200)
  45
  })
  ```

* Python

  webhook_handler.py

  ```python
  1
  from flask import Flask, request
  2


  3
  app = Flask(__name__)
  4


  5
  @app.route('/webhooks/scalekit', methods=['POST'])
  6
  def handle_webhook():
  7
      event = request.get_json()
  8


  9
      if event.get('type') == 'organization.directory.user_created':
  10
          org_id = event['organization_id']
  11
          directory_user = event['data']
  12
          seat_limit_reached = False
  13


  14
          # Run the check and insert in a single transaction.
  15
          # FOR UPDATE inside the transaction holds the lock until commit.
  16
          with db.transaction() as tx:
  17
              usage = tx.query_one(
  18
                  'SELECT seat_limit, used_seats FROM org_seat_usage '
  19
                  'WHERE org_id = %s FOR UPDATE',
  20
                  (org_id,)
  21
              )
  22


  23
              if not usage or usage['used_seats'] >= usage['seat_limit']:
  24
                  seat_limit_reached = True
  25
              else:
  26
                  tx.execute(
  27
                      'INSERT INTO users (id, org_id, email, name) VALUES (%s, %s, %s, %s)',
  28
                      (directory_user['id'], org_id,
  29
                       directory_user['email'], directory_user['name'])
  30
                  )
  31
                  tx.execute(
  32
                      'UPDATE org_seat_usage SET used_seats = used_seats + 1 '
  33
                      'WHERE org_id = %s',
  34
                      (org_id,)
  35
                  )
  36


  37
          if seat_limit_reached:
  38
              # Seat limit reached — skip user creation and alert the admin.
  39
              notify_admin_seat_limit_reached(org_id)
  40


  41
      # Return 200 so Scalekit does not retry this event.
  42
      return '', 200
  ```

* Go

  webhook_handler.go

  ```go
  1
  package main
  2


  3
  import (
  4
    "encoding/json"
  5
    "net/http"
  6
  )
  7


  8
  func webhookHandler(w http.ResponseWriter, r *http.Request) {
  9
    var event map[string]interface{}
  10
    if err := json.NewDecoder(r.Body).Decode(&event); err != nil {
  11
      http.Error(w, "bad request", http.StatusBadRequest)
  12
      return
  13
    }
  14


  15
    if event["type"] == "organization.directory.user_created" {
  16
      orgID := event["organization_id"].(string)
  17
      data := event["data"].(map[string]interface{})
  18
      seatLimitReached := false
  19


  20
      // Run the check and insert in a single transaction.
  21
      // FOR UPDATE inside the transaction holds the lock until commit.
  22
      tx, _ := db.Begin()
  23
      var seatLimit, usedSeats int
  24
      err := tx.QueryRow(
  25
        "SELECT seat_limit, used_seats FROM org_seat_usage WHERE org_id = $1 FOR UPDATE",
  26
        orgID,
  27
      ).Scan(&seatLimit, &usedSeats)
  28


  29
      if err != nil || usedSeats >= seatLimit {
  30
        seatLimitReached = true
  31
        tx.Rollback()
  32
      } else {
  33
        tx.Exec(
  34
          "INSERT INTO users (id, org_id, email, name) VALUES ($1, $2, $3, $4)",
  35
          data["id"], orgID, data["email"], data["name"],
  36
        )
  37
        tx.Exec(
  38
          "UPDATE org_seat_usage SET used_seats = used_seats + 1 WHERE org_id = $1",
  39
          orgID,
  40
        )
  41
        tx.Commit()
  42
      }
  43


  44
      if seatLimitReached {
  45
        // Seat limit reached — skip user creation and alert the admin.
  46
        notifyAdminSeatLimitReached(orgID)
  47
      }
  48
    }
  49


  50
    // Return 200 so Scalekit does not retry this event.
  51
    w.WriteHeader(http.StatusOK)
  52
  }
  ```

* Java

  WebhookController.java

  ```java
  1
  import org.springframework.web.bind.annotation.*;
  2
  import java.util.Map;
  3
  import java.util.concurrent.atomic.AtomicBoolean;
  4


  5
  @RestController
  6
  public class WebhookController {
  7


  8
    @PostMapping("/webhooks/scalekit")
  9
    public ResponseEntity handleWebhook(@RequestBody Map event) {
  10
      if ("organization.directory.user_created".equals(event.get("type"))) {
  11
        String orgId = (String) event.get("organization_id");
  12
        Map directoryUser = (Map) event.get("data");
  13
        AtomicBoolean seatLimitReached = new AtomicBoolean(false);
  14


  15
        // Run the check and insert in a single transaction.
  16
        // FOR UPDATE inside the transaction holds the lock until commit.
  17
        transactionTemplate.execute(status -> {
  18
          OrgSeatUsage usage = db.queryForObject(
  19
            "SELECT seat_limit, used_seats FROM org_seat_usage WHERE org_id = ? FOR UPDATE",
  20
            OrgSeatUsage.class, orgId
  21
          );
  22


  23
          if (usage == null || usage.getUsedSeats() >= usage.getSeatLimit()) {
  24
            seatLimitReached.set(true);
  25
            return null;
  26
          }
  27


  28
          db.update(
  29
            "INSERT INTO users (id, org_id, email, name) VALUES (?, ?, ?, ?)",
  30
            directoryUser.get("id"), orgId,
  31
            directoryUser.get("email"), directoryUser.get("name")
  32
          );
  33
          db.update(
  34
            "UPDATE org_seat_usage SET used_seats = used_seats + 1 WHERE org_id = ?",
  35
            orgId
  36
          );
  37
          return null;
  38
        });
  39


  40
        if (seatLimitReached.get()) {
  41
          // Seat limit reached — skip user creation and alert the admin.
  42
          notifyAdminSeatLimitReached(orgId);
  43
        }
  44
      }
  45


  46
      // Return 200 so Scalekit does not retry this event.
  47
      return ResponseEntity.ok().build();
  48
    }
  49
  }
  ```

## Decrement the count when a user is removed

[Section titled “Decrement the count when a user is removed”](#decrement-the-count-when-a-user-is-removed)

The `user_deleted` handler decreases the seat counter and clears any pending seat-limit notification. This lets the next `user_created` event succeed without manual intervention from your team.

Webhook events are delivered at least once

Scalekit may deliver the same `user_deleted` event more than once. The `GREATEST(used_seats - 1, 0)` guard prevents the counter from going below zero, but it does not prevent double-decrements on duplicate events. For high-reliability systems, track processed event IDs using `event.id` from the webhook payload and skip events you have already handled.

* Node.js

  webhook-handler.ts

  ```ts
  1
  if (event.type === 'organization.directory.user_deleted') {
  2
    const orgId = event.organization_id
  3
    const directoryUser = event.data
  4


  5
    await db.transaction(async (tx) => {
  6
      // Remove the user and decrement the counter atomically.
  7
      await tx.query('DELETE FROM users WHERE id = $1', [directoryUser.id])
  8
      await tx.query(
  9
        'UPDATE org_seat_usage SET used_seats = GREATEST(used_seats - 1, 0) WHERE org_id = $1',
  10
        [orgId]
  11
      )
  12
      // Clear any pending seat-limit notification so the next user can be provisioned.
  13
      await tx.query(
  14
        "DELETE FROM notifications WHERE org_id = $1 AND type = 'seat_limit_reached'",
  15
        [orgId]
  16
      )
  17
    })
  18
  }
  ```

* Python

  webhook_handler.py

  ```python
  1
  if event.get('type') == 'organization.directory.user_deleted':
  2
      org_id = event['organization_id']
  3
      directory_user = event['data']
  4


  5
      with db.transaction() as tx:
  6
          # Remove the user and decrement the counter atomically.
  7
          tx.execute('DELETE FROM users WHERE id = %s', (directory_user['id'],))
  8
          tx.execute(
  9
              'UPDATE org_seat_usage SET used_seats = GREATEST(used_seats - 1, 0) '
  10
              'WHERE org_id = %s',
  11
              (org_id,)
  12
          )
  13
          # Clear any pending seat-limit notification so the next user can be provisioned.
  14
          tx.execute(
  15
              "DELETE FROM notifications WHERE org_id = %s AND type = 'seat_limit_reached'",
  16
              (org_id,)
  17
          )
  ```

* Go

  webhook_handler.go

  ```go
  1
  if event["type"] == "organization.directory.user_deleted" {
  2
    orgID := event["organization_id"].(string)
  3
    data := event["data"].(map[string]interface{})
  4


  5
    tx, _ := db.Begin()
  6
    // Remove the user and decrement the counter atomically.
  7
    tx.Exec("DELETE FROM users WHERE id = $1", data["id"])
  8
    tx.Exec(
  9
      "UPDATE org_seat_usage SET used_seats = GREATEST(used_seats - 1, 0) WHERE org_id = $1",
  10
      orgID,
  11
    )
  12
    // Clear any pending seat-limit notification so the next user can be provisioned.
  13
    tx.Exec(
  14
      "DELETE FROM notifications WHERE org_id = $1 AND type = 'seat_limit_reached'",
  15
      orgID,
  16
    )
  17
    tx.Commit()
  18
  }
  ```

* Java

  WebhookController.java

  ```java
  1
  if ("organization.directory.user_deleted".equals(event.get("type"))) {
  2
    String orgId = (String) event.get("organization_id");
  3
    Map directoryUser = (Map) event.get("data");
  4


  5
    transactionTemplate.execute(status -> {
  6
      // Remove the user and decrement the counter atomically.
  7
      db.update("DELETE FROM users WHERE id = ?", directoryUser.get("id"));
  8
      db.update(
  9
        "UPDATE org_seat_usage SET used_seats = GREATEST(used_seats - 1, 0) WHERE org_id = ?",
  10
        orgId
  11
      );
  12
      // Clear any pending seat-limit notification so the next user can be provisioned.
  13
      db.update(
  14
        "DELETE FROM notifications WHERE org_id = ? AND type = 'seat_limit_reached'",
  15
        orgId
  16
      );
  17
      return null;
  18
    });
  19
  }
  ```

## Notify admins without spamming them

[Section titled “Notify admins without spamming them”](#notify-admins-without-spamming-them)

A new `user_created` event fires for every blocked user. Without deduplication, your admin will receive one email per rejected provisioning attempt. Use an idempotent insert to fire the notification only once per organization until the condition is resolved.

db/schema.sql

```sql
CREATE TABLE notifications (
  id         SERIAL PRIMARY KEY,
  org_id     TEXT NOT NULL,
  type       TEXT NOT NULL,
  resolved   BOOLEAN NOT NULL DEFAULT FALSE,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  UNIQUE (org_id, type, resolved)
);
```

The `UNIQUE (org_id, type, resolved)` constraint blocks duplicate active notifications. Insert with `ON CONFLICT DO NOTHING` to skip the insert when a notification already exists:

* Node.js

  notify.ts

  ```ts
  async function notifyAdminSeatLimitReached(orgId: string) {
    // Insert only if no unresolved notification exists for this org.
    const result = await db.query(
      `INSERT INTO notifications (org_id, type, resolved)
       VALUES ($1, 'seat_limit_reached', FALSE)
       ON CONFLICT (org_id, type, resolved) DO NOTHING`,
      [orgId]
    )


    // rowCount is 0 when the conflict was skipped — admin already notified.
    if (result.rowCount === 0) return


    // Send the alert once: email, Slack, in-app — your choice.
    await sendAdminAlert(orgId, 'Seat limit reached — users are not being provisioned.')
  }
  ```

* Python

  notify.py

  ```python
  def notify_admin_seat_limit_reached(org_id: str) -> None:
      # Insert only if no unresolved notification exists for this org.
      result = db.execute(
          """INSERT INTO notifications (org_id, type, resolved)
             VALUES (%s, 'seat_limit_reached', FALSE)
             ON CONFLICT (org_id, type, resolved) DO NOTHING""",
          (org_id,)
      )


      # rowcount is 0 when the conflict was skipped — admin already notified.
      if result.rowcount == 0:
          return


      # Send the alert once: email, Slack, in-app — your choice.
      send_admin_alert(org_id, 'Seat limit reached — users are not being provisioned.')
  ```

* Go

  notify.go

  ```go
  1
  func notifyAdminSeatLimitReached(orgID string) {
  2
    // Insert only if no unresolved notification exists for this org.
  3
    result, _ := db.Exec(
  4
      `INSERT INTO notifications (org_id, type, resolved)
  5
       VALUES ($1, 'seat_limit_reached', FALSE)
  6
       ON CONFLICT (org_id, type, resolved) DO NOTHING`,
  7
      orgID,
  8
    )
  9


  10
    // RowsAffected is 0 when the conflict was skipped — admin already notified.
  11
    rows, _ := result.RowsAffected()
  12
    if rows == 0 {
  13
      return
  14
    }
  15


  16
    // Send the alert once: email, Slack, in-app — your choice.
  17
    sendAdminAlert(orgID, "Seat limit reached — users are not being provisioned.")
  18
  }
  ```

* Java

  NotificationService.java

  ```java
  public void notifyAdminSeatLimitReached(String orgId) {
    // Insert only if no unresolved notification exists for this org.
    int rows = db.update(
      "INSERT INTO notifications (org_id, type, resolved) " +
      "VALUES (?, 'seat_limit_reached', FALSE) " +
      "ON CONFLICT (org_id, type, resolved) DO NOTHING",
      orgId
    );


    // rows is 0 when the conflict was skipped — admin already notified.
    if (rows == 0) return;


    // Send the alert once: email, Slack, in-app — your choice.
    sendAdminAlert(orgId, "Seat limit reached — users are not being provisioned.");
  }
  ```

When a user is removed and the count drops below the limit, the `user_deleted` handler deletes the notification row. The next blocked `user_created` event will insert a fresh notification and trigger a new alert.

***

**Related guides**

* [SCIM provisioning quickstart](/directory/scim/quickstart/) — set up webhooks and the Directory API, including signature verification
* [Directory webhook events reference](/directory/reference/directory-events/) — full event payload schemas

---
# DOCUMENT BOUNDARY
---

# Search Scalekit docs with ref.tools

> Configure ref.tools MCP to search Scalekit documentation directly from Cursor, Claude Code, or Windsurf without leaving your IDE.

Every time you need to look up a Scalekit API, scope name, or configuration option, you break your flow: open a new tab, search the docs, copy the answer, switch back. With ref.tools configured as an MCP server, your AI coding assistant can search Scalekit documentation inline and return accurate, up-to-date answers without you leaving the editor. Setup takes about two minutes.

## The problem

[Section titled “The problem”](#the-problem)

AI coding assistants are good at generating code, but they have two failure modes when it comes to third-party docs:

* **Hallucination** — The model invents an API that doesn’t exist or gets parameter names wrong because its training data is incomplete
* **Stale knowledge** — Even accurate training data goes out of date as SDKs and APIs evolve

Both problems get worse when you’re working with a narrowly scoped platform like Scalekit. The model may have seen very little training data about it, and what it did see may be outdated.

The standard workaround is to paste docs into the chat manually — which means constant context-switching between your editor and a browser. ref.tools solves both problems by connecting your AI assistant directly to live Scalekit documentation through an MCP tool call.

## Who needs this

[Section titled “Who needs this”](#who-needs-this)

This cookbook is for you if:

* ✅ You use Cursor, Claude Code, Windsurf, or another MCP-compatible AI assistant
* ✅ You’re building with Scalekit (auth, SSO, MCP servers, M2M, SCIM)
* ✅ You want accurate, up-to-date answers without context-switching to a browser

You **don’t** need this if:

* ❌ You prefer pasting docs into your chat manually
* ❌ Your AI assistant doesn’t support MCP

## The solution

[Section titled “The solution”](#the-solution)

[ref.tools](https://ref.tools) is a documentation search platform that indexes third-party docs — including Scalekit — and exposes them as an MCP tool called `ref_search_documentation`. Once you add the ref.tools MCP server to your AI assistant, you can prompt it to search Scalekit docs and it will call the tool and return current results directly in chat.

The server supports two transports:

* **Streamable HTTP** (recommended) — Direct HTTP connection using your API key; lower latency, no local process required
* **stdio** (legacy) — Runs a local `npx` process; works with any MCP client that supports stdio

## Set up ref.tools

[Section titled “Set up ref.tools”](#set-up-reftools)

1. ### Get your API key

   [Section titled “Get your API key”](#get-your-api-key)

   1. Go to [ref.tools](https://ref.tools) and sign in
   2. Search for **Scalekit** to confirm the documentation source is indexed
   3. Open the **Quick Install** panel for Scalekit — your API key is pre-filled in the install commands
   4. Copy your API key; you’ll use it in the next step

2. ### Add the MCP server to your AI assistant

   [Section titled “Add the MCP server to your AI assistant”](#add-the-mcp-server-to-your-ai-assistant)

   Pick your tool and apply the matching configuration.

   #### Claude Code

   [Section titled “Claude Code”](#claude-code)

   Run this command in your terminal to add the MCP server globally across all projects:

   ```bash
   claude mcp add --transport http ref-context https://api.ref.tools/mcp \
     --header "x-ref-api-key: YOUR_API_KEY"
   ```

   To scope it to a single project instead, add `--scope project` to the command.

   #### Cursor

   [Section titled “Cursor”](#cursor)

   Add the following to `.cursor/mcp.json` in your project root (or via **Settings → MCP**):

   .cursor/mcp.json

   ```json
   {
     "ref-context": {
       "type": "http",
       "url": "https://api.ref.tools/mcp?apiKey=YOUR_API_KEY"
     }
   }
   ```

   #### Windsurf

   [Section titled “Windsurf”](#windsurf)

   Add the following to `~/.codeium/windsurf/mcp_config.json`:

   \~/.codeium/windsurf/mcp_config.json

   ```json
   {
     "ref-context": {
       "serverUrl": "https://api.ref.tools/mcp?apiKey=YOUR_API_KEY"
     }
   }
   ```

   #### Other (stdio)

   [Section titled “Other (stdio)”](#other-stdio)

   For any MCP client that supports stdio, add to your MCP config:

   mcp.json

   ```json
   {
     "ref-context": {
       "command": "npx",
       "args": ["ref-tools-mcp@latest"],
       "env": {
         "REF_API_KEY": "YOUR_API_KEY"
       }
     }
   }
   ```

   This requires Node.js installed locally. The `npx` command fetches and runs the server on first use.

3. ### Verify it’s working

   [Section titled “Verify it’s working”](#verify-its-working)

   1. Restart your AI assistant (or use its MCP reload command if available)

   2. Open a new chat and send this prompt:

      ```plaintext
      Use ref to look up how to add OAuth 2.1 authorization to an MCP server with Scalekit
      ```

   3. Your assistant should call the `ref_search_documentation` tool and return results from `docs.scalekit.com`

   If the tool doesn’t appear, check that you restarted the assistant after saving the config, and that the API key is correct.

Keep your API key private

Never commit your ref.tools API key to source control. For project-level configs checked into git, pass the key through an environment variable and reference it as `$REF_API_KEY` in your config, or add the config file to `.gitignore`.

## Example searches to try

[Section titled “Example searches to try”](#example-searches-to-try)

Once ref.tools is connected, use phrases like “use ref to…” or “look up in ref…” to trigger the tool explicitly:

* `Use ref to find the Scalekit MCP auth quickstart`
* `Look up how to configure SSO with Scalekit`
* `Use ref to find Scalekit M2M token documentation`
* `Search Scalekit docs for SCIM provisioning setup`
* `Use ref to look up Scalekit SDK environment variables`

You can also just ask naturally — most assistants will call the tool automatically when the question is about Scalekit.

## Common mistakes

[Section titled “Common mistakes”](#common-mistakes)

API key committed to git

* **Symptom**: Your key appears in git history or a public repository
* **Cause**: Config file with the key inline was committed
* **Fix**: Use an environment variable (`$REF_API_KEY`) and add the config file to `.gitignore` if it contains real credentials

Wrong transport for your client

* **Symptom**: MCP server fails to connect or appears as disconnected
* **Cause**: Some clients only support stdio; others support both HTTP and stdio
* **Fix**: Check your client’s MCP documentation. Cursor and Claude Code support streamable HTTP. Older or less common clients may require stdio.

Server name not matching what the client expects

* **Symptom**: Tool calls fail with “unknown tool” or the server doesn’t appear in the tool list
* **Cause**: The config key (e.g., `ref-context`) doesn’t match what you reference in prompts, or the client uses a different config field name
* **Fix**: Confirm the key in your config file matches the server name shown in your client’s MCP settings panel

Tool not appearing after config change

* **Symptom**: You updated the config but the `ref_search_documentation` tool isn’t available
* **Cause**: The MCP connection wasn’t refreshed
* **Fix**: Fully restart your AI assistant, or use its MCP reload command (Claude Code: `claude mcp list` to verify; Cursor: reload the window)

## Next steps

[Section titled “Next steps”](#next-steps)

For further setup, authentication options, and available documentation sources, see the links below.

* [Add OAuth 2.1 authorization to MCP servers](/authenticate/mcp/quickstart) — the most common thing developers look up using ref
* [ref.tools](https://ref.tools) — browse all available documentation sources you can add alongside Scalekit
* [M2M authentication overview](/guides/m2m/overview) — machine-to-machine auth patterns frequently searched via ref

---
# DOCUMENT BOUNDARY
---

# Sync B2B billing with Scalekit and Chargebee

> Map Scalekit organizations to Chargebee customers, run hosted checkout, and keep subscription state in sync via webhooks.

Multi-tenant B2B SaaS apps authenticate users through Scalekit organizations, but bill through Chargebee subscriptions. Those two systems do not share a database. Without an explicit mapping, you end up with duplicate Chargebee customers, subscriptions that never activate after checkout, or feature gates that read stale plan data.

This cookbook wires Scalekit organizations and sessions to Chargebee using **org-mode billing**: the organization ID from the access token (`oid`) becomes the billing `referenceId`, Scalekit webhooks provision Chargebee customers, and Chargebee webhooks keep your local subscription table current. You own the routes, schema, and authorization that connect the two systems.

Working example repo

Patterns below are implemented end-to-end in [saas-auth-chargebee-example](https://github.com/scalekit-developers/saas-auth-chargebee-example) (Next.js, Scalekit, Chargebee Node SDK, Drizzle + SQLite). Clone it to follow along locally, including the in-app journey at `/guide`.

## What you get

[Section titled “What you get”](#what-you-get)

* Chargebee customers created when Scalekit fires `organization.created` (not on every user signup)
* Local org ↔ Chargebee customer mapping keyed by the Scalekit organization ID
* Hosted checkout and customer portal via Chargebee hosted pages
* Local subscription cache driven by Chargebee webhooks, plus optional eager sync on checkout redirect
* Session-scoped authorization so billing APIs only act on the caller’s org (`referenceId === oid`)
* Lifecycle hooks for product logic (after customer create, subscription complete/cancel, authorize deny)

## Who needs this

[Section titled “Who needs this”](#who-needs-this)

This cookbook is for you if:

* ✅ You authenticate with Scalekit and use **organizations** (`oid` in access tokens)
* ✅ You bill **per organization**, not per individual user
* ✅ You use Chargebee hosted checkout or the customer portal
* ✅ You maintain a local subscription cache to gate features in your app

You **don’t** need this if:

* ❌ You bill per user, not per organization
* ❌ Scalekit manages your entire product catalog and entitlements (no separate billing system)

## How the integration fits together

[Section titled “How the integration fits together”](#how-the-integration-fits-together)

Treat the Scalekit **organization ID** as the single billing reference for the tenant. Scalekit authenticates the user and org, your app owns the mapping and local subscription cache, and Chargebee owns catalog, checkout, and billing state.

![Architecture: User logs in via Scalekit, app provisions Chargebee customers and hosted checkout, Chargebee returns checkout success and subscription webhooks, app stores org mapping and subscription cache in local DB](/.netlify/images?url=_astro%2Farchitecture.Cp7V-ALL.png\&w=1568\&h=740\&dpl=6ae471fc143c9a4c27550463)

The integration has four seams:

1. **Provision on org create** — Scalekit `organization.created` webhook → create a Chargebee customer and store the mapping locally.
2. **Authorize every billing call** — session `oid` must match the billing `referenceId` before any Chargebee API call.
3. **Future subscription before checkout** — create a local row with `status: future`, stamp `pendingSubscriptionId` on Chargebee customer metadata, then redirect to hosted checkout.
4. **Reconcile from Chargebee** — subscription webhooks (and an eager sync on checkout redirect) update the local row to `active`, `in_trial`, or cancelled.

Screenshots below are from the [reference demo](https://github.com/scalekit-developers/saas-auth-chargebee-example) so you can match each milestone to the product UI.

## Before you start

[Section titled “Before you start”](#before-you-start)

| Prerequisite                                               | Where to get it                                                |
| ---------------------------------------------------------- | -------------------------------------------------------------- |
| Scalekit environment with organizations                    | [Scalekit dashboard](https://app.scalekit.com/)                |
| OAuth client (`skc_...`) + redirect URI                    | **API Keys** in the dashboard                                  |
| Chargebee sandbox site (Product Catalog 2.0)               | Chargebee test site                                            |
| Plan **item price** ID (for example `growth-plan-monthly`) | Chargebee **Product Catalog** — reference prices by ID in code |
| Test payment gateway (`gw_...`)                            | Chargebee **Payment Gateways**                                 |
| Public tunnel for webhooks                                 | ngrok, LocalTunnel, or similar                                 |

## Step 1: Install packages

[Section titled “Step 1: Install packages”](#step-1-install-packages)

Install the Scalekit and Chargebee SDKs on the **server** (API routes, webhook handlers). Use whichever package manager you use in your app (`npm`, `pnpm`, or `yarn`); the example below matches the reference app:

Terminal

```bash
npm install @scalekit-sdk/node chargebee
```

Snippets in this cookbook are **Node.js / Next.js App Router**, aligned with the reference app. Scalekit client concepts (token validation, webhook verification, `oid`) apply across SDKs; adapt routes and session storage if you run another stack. Chargebee’s primary SDK surface used here is the Node package.

Use your ORM of choice for the local billing tables (the reference app uses Drizzle + SQLite). Keep Chargebee secret API keys server-side only; publishable keys for Chargebee.js may use `NEXT_PUBLIC_*` if you embed payment components.

## Step 2: Configure environment variables

[Section titled “Step 2: Configure environment variables”](#step-2-configure-environment-variables)

Define these in your server environment (for example `.env` locally and your host’s secrets store in production).

| Variable                                                    | Purpose                                                            |
| ----------------------------------------------------------- | ------------------------------------------------------------------ |
| `SCALEKIT_ENVIRONMENT_URL`                                  | Scalekit environment URL                                           |
| `SCALEKIT_CLIENT_ID` / `SCALEKIT_CLIENT_SECRET`             | OAuth client                                                       |
| `SCALEKIT_REDIRECT_URI`                                     | OAuth callback (for example `http://localhost:3000/auth/callback`) |
| `SCALEKIT_WEBHOOK_SECRET`                                   | Verify Scalekit webhook signatures                                 |
| `CHARGEBEE_SITE`                                            | Chargebee site subdomain                                           |
| `CHARGEBEE_API_KEY`                                         | Full-access API key for your Chargebee site                        |
| `CHARGEBEE_PLAN_ITEM_PRICE_ID`                              | Default plan item price ID from Product Catalog 2.0                |
| `CHARGEBEE_GATEWAY_ACCOUNT_ID`                              | Optional gateway pin for hosted checkout (`gw_...`)                |
| `CHARGEBEE_WEBHOOK_USERNAME` / `CHARGEBEE_WEBHOOK_PASSWORD` | Basic Auth for Chargebee webhooks (recommended in production)      |
| `NEXT_PUBLIC_APP_URL`                                       | App base URL for hosted-page redirects                             |

.env.example

```bash
SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev
SCALEKIT_CLIENT_ID=skc_...
SCALEKIT_CLIENT_SECRET=
SCALEKIT_REDIRECT_URI=http://localhost:3000/auth/callback
SCALEKIT_WEBHOOK_SECRET=
CHARGEBEE_SITE=your-site-test
CHARGEBEE_API_KEY=
CHARGEBEE_PLAN_ITEM_PRICE_ID=growth-plan-monthly
CHARGEBEE_GATEWAY_ACCOUNT_ID=gw_your_test_gateway_id
CHARGEBEE_WEBHOOK_USERNAME=
CHARGEBEE_WEBHOOK_PASSWORD=
NEXT_PUBLIC_APP_URL=http://localhost:3000
```

## Step 3: Add local schema

[Section titled “Step 3: Add local schema”](#step-3-add-local-schema)

Add tables for organizations, subscriptions, and optional line items. The organization row holds the Chargebee customer ID; subscriptions are keyed by `reference_id` (the Scalekit org ID from `oid`). Default new subscriptions to `future` so checkout can reconcile before Chargebee assigns a subscription ID.

db/schema.sql

```sql
1
CREATE TABLE organization (
2
  id TEXT PRIMARY KEY,
3
  display_name TEXT,
4
  chargebee_customer_id TEXT UNIQUE,
5
  updated_at INTEGER
6
);
7


8
CREATE TABLE subscription (
9
  id TEXT PRIMARY KEY,
10
  reference_id TEXT NOT NULL,
11
  chargebee_customer_id TEXT,
12
  chargebee_subscription_id TEXT UNIQUE,
13
  status TEXT NOT NULL DEFAULT 'future',
14
  period_start INTEGER,
15
  period_end INTEGER,
16
  trial_start INTEGER,
17
  trial_end INTEGER,
18
  canceled_at INTEGER,
19
  seats INTEGER,
20
  metadata TEXT
21
);
22


23
CREATE TABLE subscription_item (
24
  id TEXT PRIMARY KEY,
25
  subscription_id TEXT NOT NULL REFERENCES subscription(id) ON DELETE CASCADE,
26
  item_price_id TEXT NOT NULL,
27
  item_type TEXT NOT NULL,
28
  quantity INTEGER NOT NULL,
29
  unit_price INTEGER,
30
  amount INTEGER
31
);
```

Translate to your ORM. Index `reference_id` — webhook handlers and list endpoints query by org ID on every request.

**After this step:** migrations applied; empty tables ready for provisioning and checkout.

## Step 4: Initialize clients

[Section titled “Step 4: Initialize clients”](#step-4-initialize-clients)

Create lazy singletons from environment variables. Never hardcode API keys.

lib/scalekit.ts

```ts
1
import { ScalekitClient } from '@scalekit-sdk/node';
2


3
let scalekitClient: ScalekitClient | null = null;
4


5
export function getScalekitClient(): ScalekitClient {
6
  if (!scalekitClient) {
7
    const envUrl = process.env.SCALEKIT_ENVIRONMENT_URL;
8
    const clientId = process.env.SCALEKIT_CLIENT_ID;
9
    const clientSecret = process.env.SCALEKIT_CLIENT_SECRET;
10
    if (!envUrl || !clientId || !clientSecret) {
11
      throw new Error(
12
        'Set SCALEKIT_ENVIRONMENT_URL, SCALEKIT_CLIENT_ID, and SCALEKIT_CLIENT_SECRET.'
13
      );
14
    }
15
    scalekitClient = new ScalekitClient(envUrl, clientId, clientSecret);
16
  }
17
  return scalekitClient;
18
}
```

lib/chargebee.ts

```ts
import Chargebee from 'chargebee';


let chargebeeClient: Chargebee | null = null;


export function getChargebeeClient(): Chargebee {
  if (!chargebeeClient) {
    const site = process.env.CHARGEBEE_SITE;
    const apiKey = process.env.CHARGEBEE_API_KEY;
    if (!site || !apiKey) {
      throw new Error('Set CHARGEBEE_SITE and CHARGEBEE_API_KEY.');
    }
    chargebeeClient = new Chargebee({ site, apiKey });
  }
  return chargebeeClient;
}
```

## Step 5: Provision Chargebee customers from Scalekit webhooks

[Section titled “Step 5: Provision Chargebee customers from Scalekit webhooks”](#step-5-provision-chargebee-customers-from-scalekit-webhooks)

Register a Scalekit webhook for `organization.created`, `organization.updated`, and `organization.deleted`. Point it at your public URL (use a tunnel in local dev):

```text
https://your-domain.com/api/webhooks/scalekit
```

Verify the signature on the **raw request body** before parsing JSON.

Verify webhook signatures

Never parse the body before verification. Re-serialized JSON breaks signature checks. Read `req.text()` (or the raw buffer), verify, then `JSON.parse`.

api/webhooks/scalekit/route.ts

```ts
1
import { NextRequest, NextResponse } from 'next/server';
2
import { getScalekitClient } from '@/lib/scalekit';
3
import { createOrgCustomer } from '@/lib/billing/create-org-customer';
4
import { cleanupOrganizationBilling } from '@/lib/billing/cleanup-org';
5
import { upsertOrganization } from '@/lib/db/organizations';
6


7
export async function POST(req: NextRequest) {
8
  const rawBody = await req.text();
9
  const secret = process.env.SCALEKIT_WEBHOOK_SECRET;
10
  if (!secret) {
11
    return NextResponse.json({ error: 'Webhook secret not configured' }, { status: 500 });
12
  }
13


14
  const headers: Record = {};
15
  req.headers.forEach((value, key) => {
16
    headers[key.toLowerCase()] = value;
17
  });
18


19
  const client = getScalekitClient();
20
  const isValid = client.verifyWebhookPayload(secret, headers, rawBody);
21
  if (!isValid) {
22
    return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
23
  }
24


25
  const event = JSON.parse(rawBody);
26
  const organizationId = event.organization_id ?? event.data?.id;
27


28
  if (event.type === 'organization.created' && organizationId) {
29
    await createOrgCustomer({
30
      organizationId,
31
      displayName: event.data?.display_name ?? null,
32
    });
33
  } else if (event.type === 'organization.updated' && organizationId) {
34
    await upsertOrganization({
35
      id: organizationId,
36
      displayName: event.data?.display_name ?? null,
37
    });
38
  } else if (event.type === 'organization.deleted' && organizationId) {
39
    await cleanupOrganizationBilling(organizationId);
40
  }
41


42
  return NextResponse.json({ received: true });
43
}
```

`createOrgCustomer` upserts the local organization row, creates a Chargebee customer if one does not exist, and stores `organizationId` in Chargebee `meta_data`. Make it idempotent: check the local mapping before calling `customer.create`, and handle races if checkout runs before the webhook finishes.

lib/billing/create-org-customer.ts

```ts
const { customer } = await chargebee.customer.create({
  company: displayName ?? undefined,
  email: email ?? undefined,
  preferred_currency_code: 'USD',
  meta_data: {
    organizationId,
    customerType: 'organization',
  },
});


await setChargebeeCustomerId(organizationId, customer.id);
```

Return `2xx` after accepting the event. Scalekit retries on non-2xx responses. The reference app enqueues work with `setImmediate` so the HTTP response is fast; either pattern works if handlers are idempotent.

**After this step:** create an organization in Scalekit → local `organization` row and a Chargebee customer with matching `organizationId` metadata appear.

In the reference app dashboard, steps 1–3 show **Done** and the org is linked to a Chargebee customer before you open billing:

![Dashboard after sign-in: organization linked to Chargebee customer, subscribe step current](/.netlify/images?url=_astro%2F02-dashboard-org-linked.5twfG6-G.png\&w=3024\&h=1780\&dpl=6ae471fc143c9a4c27550463)

## Step 6: Read the organization ID from the session

[Section titled “Step 6: Read the organization ID from the session”](#step-6-read-the-organization-id-from-the-session)

Billing routes need org context from the access token. Validate the token on every request and require the `oid` claim. Do not call `/userinfo` for billing context.

lib/auth/require-session.ts

```ts
1
import { decodeJwt } from 'jose';
2


3
const isValid = await scalekit.validateAccessToken(accessToken);
4
if (!isValid) {
5
  throw new SessionError(401, 'Invalid or expired token');
6
}
7


8
// Safe after validateAccessToken: signature and standard claims already checked.
9
const claims = decodeJwt(accessToken);
10


11
const organizationId = claims.oid as string | undefined;
12
if (!organizationId) {
13
  throw new SessionError(403, 'Organization context required for billing');
14
}
15


16
return {
17
  userId: claims.sub as string,
18
  email: claims.email as string,
19
  organizationId,
20
};
```

## Step 7: Authorize billing references

[Section titled “Step 7: Authorize billing references”](#step-7-authorize-billing-references)

Before any Chargebee API call, confirm the caller’s session org matches the billing reference. Extend with a product hook to deny delinquent orgs without changing Chargebee configuration.

lib/auth/authorize-reference.ts

```ts
1
export type AuthorizeReferenceAction =
2
  | 'create'
3
  | 'update'
4
  | 'cancel'
5
  | 'portal'
6
  | 'list';
7


8
export async function authorizeReference({
9
  userId,
10
  organizationId,
11
  referenceId,
12
  action,
13
}: {
14
  userId: string;
15
  organizationId: string;
16
  referenceId: string;
17
  action: AuthorizeReferenceAction;
18
}): Promise {
19
  if (referenceId !== organizationId) {
20
    return false;
21
  }
22
  // Optional: return false from onAuthorizeReference to deny specific orgs.
23
  return onAuthorizeReference({ userId, organizationId, referenceId, action }) !== false;
24
}
```

**After this step:** a request with `referenceId` that does not match session `oid` returns `403` before Chargebee is called.

## Step 8: Start hosted checkout with a future subscription

[Section titled “Step 8: Start hosted checkout with a future subscription”](#step-8-start-hosted-checkout-with-a-future-subscription)

When an org admin clicks **Subscribe**, create a local `future` row first, stamp pending IDs on the Chargebee customer, then call `hostedPage.checkoutNewForItems`. Use **item price IDs** from your Chargebee product catalog.

api/subscription/create/route.ts

```ts
1
const ctx = await requireSession();
2
const referenceId = body.referenceId ?? ctx.organizationId;
3


4
if (
5
  !(await authorizeReference({
6
    userId: ctx.userId,
7
    organizationId: ctx.organizationId,
8
    referenceId,
9
    action: 'create',
10
  }))
11
) {
12
  return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
13
}
14


15
const active = await findActiveByReferenceId(referenceId);
16
if (active) {
17
  return NextResponse.json(
18
    { error: 'An active subscription already exists for this organization' },
19
    { status: 400 }
20
  );
21
}
22


23
const customerId = await getOrCreateCustomerId({
24
  organizationId: referenceId,
25
  email: ctx.email,
26
});
27


28
const localSub = await createFutureSubscription({
29
  referenceId,
30
  chargebeeCustomerId: customerId,
31
});
32


33
await chargebee.customer.update(customerId, {
34
  meta_data: {
35
    pendingSubscriptionId: localSub.id,
36
    pendingReferenceId: referenceId,
37
    organizationId: referenceId,
38
    userId: ctx.userId,
39
  },
40
});
41


42
const result = await chargebee.hostedPage.checkoutNewForItems({
43
  subscription_items: [{ item_price_id: planItemPriceId, quantity: seats }],
44
  customer: { id: customerId },
45
  redirect_url: successRedirect, // includes local subscriptionId for eager sync
46
  cancel_url: absoluteUrl(cancelUrl),
47
  // Optional: pin gateway when Smart Routing cannot auto-select
48
  // ...getHostedCheckoutCardOptions(),
49
});
50


51
return NextResponse.json({ mode: 'hosted', url: result.hosted_page.url });
```

The `future` row gives your app a stable ID to reconcile against before Chargebee assigns a subscription ID. Reject create when an active subscription already exists for the org.

**After this step:** `POST /api/subscription/create` returns `{ mode: 'hosted', url }`; completing checkout in the sandbox creates or updates the Chargebee subscription.

In the demo, the billing page lists plans scoped to the session org. **Subscribe** calls your create route, then redirects to Chargebee hosted pages:

![Billing page: Growth plan with Subscribe, integration journey on subscribe step](/.netlify/images?url=_astro%2F03-billing-choose-plan.B2QfAshm.png\&w=3024\&h=1780\&dpl=6ae471fc143c9a4c27550463)

![Chargebee hosted cart: Growth Plan monthly with 14-day free trial](/.netlify/images?url=_astro%2F04-chargebee-cart.7xKdgMWn.png\&w=3024\&h=1780\&dpl=6ae471fc143c9a4c27550463)

![Chargebee hosted checkout: account details, payment method, and order summary](/.netlify/images?url=_astro%2F05-chargebee-checkout.D0JjaVN1.png\&w=3024\&h=1780\&dpl=6ae471fc143c9a4c27550463)

## Step 9: Configure Chargebee webhooks

[Section titled “Step 9: Configure Chargebee webhooks”](#step-9-configure-chargebee-webhooks)

In the Chargebee dashboard, create a webhook endpoint that points to:

```text
https://your-domain.com/api/webhooks/chargebee
```

Protect the route with HTTP Basic Auth using `CHARGEBEE_WEBHOOK_USERNAME` and `CHARGEBEE_WEBHOOK_PASSWORD`. Enter the same credentials under Basic Authentication in the Chargebee webhook settings.

Subscribe at least to these events (names as in Chargebee / the Node SDK):

| Chargebee event                                   | Action                                              |
| ------------------------------------------------- | --------------------------------------------------- |
| `subscription_created`                            | Link `chargebee_subscription_id`, set status        |
| `subscription_activated` / `subscription_started` | Mark `active` or `in_trial`, run entitlements hooks |
| `subscription_changed` / `subscription_renewed`   | Update plan, seats, period dates                    |
| `subscription_cancelled`                          | Mark cancelled, revoke entitlements                 |
| `customer_deleted`                                | Clear local customer mapping                        |

Lookup order when matching a webhook to a local row:

1. `chargebee_subscription_id` on the local row
2. Subscription metadata (if you stamp IDs on the Chargebee subscription)
3. `meta_data.pendingSubscriptionId` on the Chargebee customer
4. `future` row by `reference_id`

Chargebee retries on failure

Return `500` when your handler fails so Chargebee retries. Return `200` only after the database write succeeds. Scalekit org webhooks can return `200` immediately and process async — failure modes differ by provider.

api/webhooks/chargebee/route.ts

```ts
1
import { NextRequest, NextResponse } from 'next/server';
2
import {
3
  WebhookAuthenticationError,
4
  basicAuthValidator,
5
} from 'chargebee';
6
import { processChargebeeWebhookEvent } from '@/lib/billing/chargebee-webhook-handler';
7


8
export async function POST(req: NextRequest) {
9
  const username = process.env.CHARGEBEE_WEBHOOK_USERNAME;
10
  const password = process.env.CHARGEBEE_WEBHOOK_PASSWORD;
11


12
  const headers: Record = {};
13
  req.headers.forEach((value, key) => {
14
    headers[key.toLowerCase()] = value;
15
  });
16


17
  if (username && password) {
18
    try {
19
      await basicAuthValidator(
20
        (user, pass) => user === username && pass === password
21
      )(headers);
22
    } catch (err) {
23
      if (err instanceof WebhookAuthenticationError) {
24
        return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
25
      }
26
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
27
    }
28
  }
29


30
  const event = await req.json();
31
  await processChargebeeWebhookEvent(event);
32
  return NextResponse.json({ received: true });
33
}
```

**After this step:** cancel or change a plan in Chargebee → local subscription status updates without a page refresh loop.

After a successful checkout (and webhook or eager sync), the billing UI shows the live plan status — for example **In Trial** — and the journey marks subscribe and webhook sync as **Done**:

![Billing after checkout: In Trial subscription and integration journey complete through webhook sync](/.netlify/images?url=_astro%2F06-billing-subscription-active.bChq_VBK.png\&w=3024\&h=1780\&dpl=6ae471fc143c9a4c27550463)

## Step 10: Eager-sync on checkout redirect

[Section titled “Step 10: Eager-sync on checkout redirect”](#step-10-eager-sync-on-checkout-redirect)

Hosted checkout redirects to your success URL before webhooks arrive. Add an eager sync so the billing page shows the subscription immediately. Webhooks remain the source of truth for ongoing changes.

api/subscription/success/route.ts

```ts
1
export async function GET(request: NextRequest) {
2
  const subscriptionId = request.nextUrl.searchParams.get('subscriptionId');
3
  const callbackURL =
4
    request.nextUrl.searchParams.get('callbackURL') ?? '/billing?success=1';
5


6
  if (subscriptionId) {
7
    const local = await findSubscriptionById(subscriptionId);
8
    if (local?.chargebeeSubscriptionId) {
9
      const result = await chargebee.subscription.retrieve(
10
        local.chargebeeSubscriptionId
11
      );
12
      await syncLocalFromChargebeeSubscription(local, result.subscription);
13
    } else if (local?.chargebeeCustomerId) {
14
      // Fallback: list recent subscriptions for the customer and sync the latest
15
      const result = await chargebee.subscription.subscriptionsForCustomer(
16
        local.chargebeeCustomerId,
17
        { limit: 10 }
18
      );
19
      // ...sync latest to local row
20
    }
21
  }
22


23
  return NextResponse.redirect(new URL(callbackURL, request.url));
24
}
```

Validate `callbackURL` against an allowlist of relative paths so redirects cannot leave your site.

## Define plans

[Section titled “Define plans”](#define-plans)

Customer provisioning works without a plan catalog in code. To sell plans, map Chargebee **item price IDs** to names, limits, and optional trials. Keep pricing ownership in Chargebee; store entitlement metadata in your app.

**Static configuration** (fine for a small catalog):

lib/billing/plans.ts

```ts
1
export type PlanConfig = {
2
  itemPriceId: string;
3
  name: string;
4
  limits: Record;
5
  freeTrial?: { days: number };
6
};
7


8
export const PLANS: PlanConfig[] = [
9
  {
10
    itemPriceId:
11
      process.env.CHARGEBEE_PLAN_ITEM_PRICE_ID ?? 'growth-plan-monthly',
12
    name: 'Growth',
13
    limits: { seats: 25 },
14
    freeTrial: { days: 14 },
15
  },
16
];
```

**Dynamic configuration (recommended for maintainability):** load plans from your database so marketing and price IDs stay out of source control. Map rows to the same `PlanConfig` shape and fail closed if the query errors.

## Common flows

[Section titled “Common flows”](#common-flows)

Expand a question when you need that path. Each answer assumes the numbered steps above are in place (schema, webhooks, authorize, hosted checkout).

How do I start hosted checkout for a plan?

Call your create route from the client with the Chargebee **item price ID** and redirect URLs. After authorize and a local `future` row, the server returns a hosted page URL.

app/billing/checkout (client)

```ts
const res = await fetch('/api/subscription/create', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    itemPriceId: 'growth-plan-monthly',
    successUrl: '/billing?success=1',
    cancelUrl: '/billing',
  }),
});
const data = await res.json();
if (data.url) {
  window.location.href = data.url;
}
```

How do I list active subscriptions for the current org?

Read from your **local** cache after `authorizeReference` — not from Chargebee on every request. Use that result for billing UI and feature gates.

api/subscription/list/route.ts

```ts
const subs = await findActiveByReferenceId(ctx.organizationId);
return NextResponse.json({
  subscriptions: subs.map((sub) => ({
    id: sub.id,
    status: sub.status,
    seats: sub.seats,
    trialEnd: sub.trialEnd,
    periodEnd: sub.periodEnd,
  })),
});
```

How do I change plans when a subscription already exists?

Use an update route that authorizes the reference, then opens Chargebee hosted update (or applies a subscription change API) for the existing `chargebee_subscription_id`.

Do not call create again while an active subscription exists. Return a clear error (for example `400` with “active subscription already exists”) so the UI can open the portal or update flow instead.

How do I send users to the Chargebee billing portal?

Create a portal session for the org’s Chargebee customer and redirect to `access_url` so they manage payment methods and invoices.

api/subscription/portal/route.ts

```ts
// After authorizeReference(..., action: 'portal')
const portalSession = await chargebee.portalSession.create({
  customer: { id: chargebeeCustomerId },
  redirect_url: absoluteUrl(returnUrl),
});
return NextResponse.json({
  url: portalSession.portal_session.access_url,
});
```

How do I gate a feature behind an active plan?

Check local subscription status (and plan line items if you store them). Treat `active` and `in_trial` as entitled unless your product rules differ.

lib/billing/require-active-plan.ts

```ts
async function requireActivePlan(
  organizationId: string,
  planItemPriceId: string
): Promise {
  const subs = await findActiveByReferenceId(organizationId);
  // Join subscription_item or stored plan metadata as your schema requires
  return subs.some(
    (sub) =>
      (sub.status === 'active' || sub.status === 'in_trial') &&
      /* plan matches planItemPriceId */
      true
  );
}
```

How do I restrict who can create or update org billing?

Org-mode billing already scopes by session `oid` via `authorizeReference`. Tighten further by combining that check with role checks (owner/admin) from the Scalekit token or your membership store before calling Chargebee.

## Customer customization (optional)

[Section titled “Customer customization (optional)”](#customer-customization-optional)

Use hooks so product logic stays out of webhook routes.

lib/subscription-hooks.ts

```ts
1
export async function onCustomerCreate(params: {
2
  organizationId: string;
3
  chargebeeCustomerId: string;
4
  displayName?: string | null;
5
}): Promise {
6
  // Analytics, CRM sync, internal tenant linking
7
}
8


9
export async function onSubscriptionComplete(ctx: {
10
  referenceId: string;
11
  subscriptionId: string;
12
  chargebeeSubscriptionId: string;
13
  status: string;
14
}): Promise {
15
  // Enable SSO, flip feature flags, send onboarding email
16
}
17


18
/** Return false to deny billing actions for a reference. */
19
export async function onAuthorizeReference(_params: {
20
  userId: string;
21
  organizationId: string;
22
  referenceId: string;
23
  action: 'create' | 'update' | 'cancel' | 'portal' | 'list';
24
}): Promise {
25
  return true;
26
}
```

## Database schema overview

[Section titled “Database schema overview”](#database-schema-overview)

| Model                   | Role                                                                                                       |
| ----------------------- | ---------------------------------------------------------------------------------------------------------- |
| **`organization`**      | Optional `chargebee_customer_id`; primary key is the Scalekit org ID                                       |
| **`subscription`**      | `reference_id` (org), Chargebee subscription/customer IDs, status, trial and period dates, seats, metadata |
| **`subscription_item`** | Line items (plans, addons, charges) with quantity and pricing details                                      |

**Source of truth:** Chargebee owns catalog, invoices, and payment state. Your database is a cache for authorization and UI. After schema changes, migrate your app database the same way you migrate other tables—there is no separate billing CLI.

## Testing

[Section titled “Testing”](#testing)

Run this validation after wiring both webhook endpoints through a tunnel:

1. **Create an organization** in Scalekit (or fire `organization.created` from the dashboard).
2. **Confirm provisioning** — local `organization` row exists; Chargebee shows a customer with matching `organizationId` metadata.
3. **Sign in** as a user in that org and open your billing page.
4. **Start checkout** — `POST /api/subscription/create` returns `{ mode: 'hosted', url }`. Complete payment with test card `4111 1111 1111 1111`.
5. **Confirm redirect** — browser lands on `/billing?success=1` and the subscription appears without a manual refresh.
6. **Replay a webhook** — send a test `subscription_activated` event from Chargebee and confirm the local row updates.

Check session org context

```bash
curl -s http://localhost:3000/api/session \
  -H "Cookie: scalekit_session=" | jq '.organizationId'
```

## Troubleshooting

[Section titled “Troubleshooting”](#troubleshooting)

| Symptom                                    | What to check                                                                                                                                    |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Orphan org / no Chargebee customer         | Scalekit webhook URL and events; `SCALEKIT_WEBHOOK_SECRET`; signature uses **raw** body; handler logs                                            |
| Webhooks ignored (Chargebee)               | URL path; Basic Auth credentials match env; selected events; tunnel health                                                                       |
| Checkout OK, UI still free tier            | Was a `future` row created? Is `pendingSubscriptionId` on the customer? Eager sync route? Chargebee webhooks delivering?                         |
| Status out of date                         | `chargebee_customer_id` / `chargebee_subscription_id` populated? Event types subscribed? Handler returns 500 on DB failure so Chargebee retries? |
| Billing API returns 403                    | `referenceId` must equal session `oid`; do not key customers by email alone                                                                      |
| `no_applicable_gateway` on hosted checkout | Add a test gateway; set `CHARGEBEE_GATEWAY_ACCOUNT_ID`; enable Smart Routing                                                                     |
| Checkout succeeds but no redirect          | `NEXT_PUBLIC_APP_URL` must be in Chargebee **Allowed redirect domains**; declined cards prevent redirect                                         |
| Duplicate Chargebee customers              | Race between org webhook and first checkout — make `createOrgCustomer` idempotent                                                                |

## Production notes

[Section titled “Production notes”](#production-notes)

* **Replace SQLite** with Postgres or your production database. Keep the `reference_id` index.
* **Rotate webhook secrets** independently for Scalekit and Chargebee. Store them in a secrets manager.
* **Make handlers idempotent** — Chargebee retries; upsert by `chargebee_subscription_id`.
* **Handle org deletion** — on `organization.deleted`, cancel active Chargebee subscriptions and delete local rows so billing does not continue.
* **Do not expose Chargebee secret API keys client-side** — only publishable keys belong in `NEXT_PUBLIC_*` variables.

## Helpful prompts

[Section titled “Helpful prompts”](#helpful-prompts)

Use these FAQ-style prompts with an AI coding agent (Cursor, Claude Code, Copilot CLI, Codex, and similar). First install the Scalekit authstack plugin for your agent, then paste a prompt from a section below (replace bracketed placeholders with your stack).

### 1. Set up your coding agent

[Section titled “1. Set up your coding agent”](#1-set-up-your-coding-agent)

Run the Scalekit CLI setup so your agent loads Scalekit auth patterns (reduces hallucinations on sessions, webhooks, and orgs):

Terminal

```bash
npx @scalekit-inc/cli setup
```

For repeated use:

Terminal

```bash
npm install -g @scalekit-inc/cli
scalekit setup
```

`setup` with no arguments launches an interactive wizard and detects installed tools. Target one agent explicitly if you prefer:

Terminal

```bash
npx @scalekit-inc/cli setup cursor
npx @scalekit-inc/cli setup claude
npx @scalekit-inc/cli setup codex
npx @scalekit-inc/cli setup copilot
```

See the [Scalekit CLI](/dev-kit/cli/) for flags and what gets installed. After setup, open your agent and use a prompt below.

### 2. Prompts (after setup)

[Section titled “2. Prompts (after setup)”](#2-prompts-after-setup)

How do I scaffold Scalekit webhooks and Chargebee customer provisioning?

```text
I'm integrating Chargebee with Scalekit organizations (org-mode billing). Generate:
1. Scalekit webhook route that verifies the raw body with verifyWebhookPayload
2. createOrgCustomer that upserts local org and creates a Chargebee customer with
   meta_data.organizationId and customerType organization
3. authorizeReference requiring referenceId === session oid
My framework is [Next.js App Router / Express / Hono]. Database is [Drizzle / Prisma / Kysely].
```

How do I scaffold subscription create with a future row and hosted checkout?

```text
Write POST /api/subscription/create that:
- requireSession with oid
- authorizeReference for action create
- rejects if an active subscription exists for the org
- creates a local future subscription row
- stamps pendingSubscriptionId on the Chargebee customer metadata
- calls hostedPage.checkoutNewForItems with item price IDs
Return { mode: "hosted", url }.
```

How do I test Scalekit and Chargebee webhooks locally?

```text
Walk me through testing Scalekit and Chargebee webhooks locally with ngrok.
App runs on port 3000. Endpoints: /api/webhooks/scalekit and /api/webhooks/chargebee.
Include ngrok command, dashboard URLs to register, SCALEKIT_WEBHOOK_SECRET,
and CHARGEBEE_WEBHOOK_USERNAME/PASSWORD Basic Auth.
```

How do I generate a pricing page that starts checkout?

```text
Generate a React pricing page that calls POST /api/subscription/create with
itemPriceId, successUrl /billing?success=1, cancelUrl /billing.
Show loading while redirecting. If the API says an active subscription exists,
call POST /api/subscription/portal instead.
```

How do I implement a feature gate for an active plan?

```text
Using a local subscriptions table keyed by Scalekit organization ID, write
requireActivePlan(organizationId, itemPriceId) that returns true only for
active or in_trial rows. Show usage in a Next.js API route.
```

Why are my webhooks not reaching the server?

```text
My Scalekit or Chargebee webhooks are not reaching the server. Auth base paths
are /api/webhooks/scalekit and /api/webhooks/chargebee. Host is [Vercel / Railway / VPS].
Walk through DNS, routing, middleware, signature/Basic Auth, and env configuration.
```

Why is subscription status not updating after checkout or cancel?

```text
Subscription status in my database does not update after checkout or cancel.
Setup: Scalekit organizations + Chargebee, local future row + pendingSubscriptionId.
List likely causes and how to verify chargebee_customer_id and
chargebee_subscription_id are populated.
```

## Resources

[Section titled “Resources”](#resources)

* [saas-auth-chargebee-example](https://github.com/scalekit-developers/saas-auth-chargebee-example) — runnable reference app
* [External IDs and metadata](/guides/external-ids-and-metadata/) — map Scalekit orgs to internal tenant IDs
* [Implement webhooks](https://docs.scalekit.com/authenticate/implement-workflows/implement-webhooks/#_top) — webhook reference

---
# DOCUMENT BOUNDARY
---

# Overview of modelling users and organizations

> Put together a data model for your app's users and organizations

Authenticated users now have access to your app.

Now is the time to consider how you’ll structure your data model for users and organizations. This foundational model will serve you well as you implement features such as workspaces, user invitations, role-based access control, and more. Ultimately, this enables your application to fully support B2B use cases.

Organizations and Users are the two first-class entities in Scalekit

* An **Organization** serves as a dedicated tenant within the application, representing a distinct entity like a company or project. A **User** is an individual account granted access to interact with the application. Typically belong to organization(s).

This is a simplified view of the relationship between these two entities

![Data model diagram: a B2B project containing two organizations, each with users showing id, name, email, and role attributes](/.netlify/images?url=_astro%2F1-k.Cosz1iTD.png\&w=2984\&h=3570\&dpl=6ae471fc143c9a4c27550463)

This model makes it easy to implement essential B2B capabilities in your application.

## Flexible user sign-in options for organizations

[Section titled “Flexible user sign-in options for organizations”](#flexible-user-sign-in-options-for-organizations)

Configure your application to support multiple authentication methods, allowing users to choose their preferred sign-in options.

Also, this is crucial for enabling organization administrators to set and enforce specific authentication policies for their users.

![Organization sign-in options: one Scalekit user can belong to several organizations, each with its own login methods such as SSO or magic link](/d2/docs/fsa/data-modelling-0.svg)

A primary use case is implementing enterprise Single Sign-On (SSO). This allows your customers to authenticate their users through their organization’s existing Identity Provider (IdP), such as Okta, Google, or Microsoft Entra ID where IdP verifies the user’s identity, granting them secure access to your application.

With Scalekit as your authentication platform, administrators can easily enforce authentication policies for their organization’s users. Scalekit handles this enforcement automatically, either applying organization-specific policies or defaulting to your application’s preferred authentication methods on the login page. Configuring these settings is straightforward—simply toggle the desired options in your Scalekit environment through the dashboard or API.

#### User records deduplication

[Section titled “User records deduplication”](#user-records-deduplication)

Regardless of which authentication methods your users choose, Scalekit automatically recognizes users with identical email addresses as the same individual. This eliminates the need for your application to manage multiple user records for the same person and ensures consistent identity recognition across different authentication flows.

* Two different Users cannot have the same email address within the same Scalekit environment.
* Scalekit automatically consolidates accounts. If a user logs in with an email and password and later uses Google OAuth with the same email, both authentication methods will be linked to the same User record.

## On how users join and leave organizations

[Section titled “On how users join and leave organizations”](#on-how-users-join-and-leave-organizations)

Control how users join and are provisioned into organizations. Scalekit provides a flexible user provisioning engine to manage the entire user lifecycle.

This includes:

* Sending and managing user invitations.
* Allowing users to discover and join organizations based on their email domain.
* Enabling membership in multiple organizations.
* Securely de-provisioning users when they leave an organization.

These capabilities are built-in, allowing you to deliver a secure and seamless user management experience from day one.

## Enforce user roles and permissions

[Section titled “Enforce user roles and permissions”](#enforce-user-roles-and-permissions)

While your product may offer a wide range of features, not all users should have identical access or capabilities. For example, in a project management tool, you might allow some users to create projects, while others may have permission only to view them.

Managing user permissions can be complex. Scalekit simplifies this by providing the necessary roles and permissions your application needs to make authorization decisions at runtime.

When a user [completes the login flow](/authenticate/fsa/complete-login/#decoding-token-claims), the access token issued by Scalekit contains their assigned roles. Your application can inspect this token to control access to different features. By default, Scalekit assigns an `admin` role to the organization creator and a `member` role to all other users, providing a solid foundation for your authorization logic.

## Modify user memberships

[Section titled “Modify user memberships”](#modify-user-memberships)

Scalekit tracks how users belong to organizations through a `memberships` property on each User object. This property contains an array of membership objects that define the user’s relationship to each organization they belong to.

Each membership object includes these key properties:

* `organization_id`: Identifies which organization the user belongs to
* `roles`: Specifies the user’s roles (assigned by your application) within that organization
* `status`: Indicates whether the membership is active, pending invite or invite expired

The memberships property enables users to belong to multiple organizations while maintaining clear role and status information for each relationship.

```json
1
{
2
    "memberships": [
3
      {
4
        "join_time": "2025-06-27T10:57:43.720Z",
5
        "membership_status": "ACTIVE",
6
        "metadata": {
7
          "department": "engineering",
8
          "location": "nyc-office"
9
        },
10
        "name": "string",
11
        "organization_id": "org_1234abcd5678efgh",
12
        "primary_identity_provider": "OKTA",
13
        "roles": [
14
          {
15
            "id": "role_admin",
16
            "name": "Admin"
17
          }
18
        ]
19
      },
20
      {
21
        "join_time": "2025-07-15T14:30:22.451Z",
22
        "membership_status": "ACTIVE",
23
        "metadata": {
24
          "department": "product",
25
          "location": "sf-office"
26
        },
27
        "name": "Jane Smith",
28
        "organization_id": "org_9876zyxw5432vuts",
29
        "primary_identity_provider": "GOOGLE",
30
        "roles": [
31
          {
32
            "id": "role_prod_manager",
33
            "name": "Product Manager"
34
          }
35
        ]
36
      }
37
    ],
38
}
```

#### Migrating from a 1-to-1 model

[Section titled “Migrating from a 1-to-1 model”](#migrating-from-a-1-to-1-model)

In a 1-to-1 data model, each user is associated with a single organization. The user’s identity is tied to that specific organization, and they cannot belong to multiple organizations with the same identity. This model is common in applications that were not originally built with multi-tenancy in mind, or where each customer’s data and user base are kept entirely separate.

For example, many traditional enterprise software applications like **Slack**, **QuickBooks**, or **Adobe Creative Suite** use this model - each customer purchases their own license and has their own separate user accounts that cannot be shared across different customer organizations.

#### Migrating from a 1-to-many model

[Section titled “Migrating from a 1-to-many model”](#migrating-from-a-1-to-many-model)

If your application allows a single user to be part of multiple organizations, their profile in Scalekit will also be shared across those organizations. While the user’s core profile is consistent, each organization membership stores distinct information like roles, status, and metadata.

If you already have a membership table that links users and organizations, you can add the Scalekit `user_id` to that table. When you update a user’s profile, the changes will apply across all their organization memberships.

| Aspect              | 1-to-1                          | 1-to-many                       |
| ------------------- | ------------------------------- | ------------------------------- |
| **User belongs to** | One organization                | Multiple organizations          |
| **Email address**   | Tied to one org                 | Unique across environment       |
| **Authentication**  | Per-organization                | Across all orgs                 |
| **Example apps**    | Adobe Creative, QuickBooks      | Slack, GitHub, Figma            |
| **Scalekit use**    | Simpler setup, less flexibility | Full multi-tenancy capabilities |

---
# DOCUMENT BOUNDARY
---

# Set up environment & SDK

> Create your account, install SDK, set up AI tools, and verify your setup to start building with Scalekit

Create a Scalekit account, install the SDK, configure your credentials, and verify the setup. This prepares your environment for adding authentication to your application.

Before you begin, create a Scalekit account if you haven’t already. After creating your account, a Scalekit workspace is automatically set up for you with dedicated development and production environments.

[Create a Scalekit account](https://app.scalekit.com/ws/signup)

1. ## Get your API credentials

   [Section titled “Get your API credentials”](#get-your-api-credentials)

   Scalekit uses the OAuth 2.0 client credentials flow for secure API authentication.

   Navigate to **Dashboard > Developers > Settings > API credentials** and copy these values:

   .env

   ```sh
   SCALEKIT_ENVIRONMENT_URL= # Example: https://acme.scalekit.dev or https://auth.acme.com (if custom domain is set)
   SCALEKIT_CLIENT_ID= # Example: skc_1234567890abcdef
   SCALEKIT_CLIENT_SECRET= # Example: test_abcdef1234567890
   ```

   Your workspace includes two environment URLs:

   Environment URLs

   ```md
   https://{your-subdomain}.scalekit.dev  (Development)
   https://{your-subdomain}.scalekit.com  (Production)
   ```

   View your environment URLs in **Dashboard > Developers > Settings**.

2. ## Install and initialize the SDK

   [Section titled “Install and initialize the SDK”](#install-and-initialize-the-sdk)

   Choose your preferred language and install the Scalekit SDK:

   * Node.js

     ```bash
     npm install @scalekit-sdk/node
     ```

   * Python

     ```sh
     pip install scalekit-sdk-python
     ```

   * Go

     ```sh
     go get -u github.com/scalekit-inc/scalekit-sdk-go
     ```

   * Java

     ```groovy
     /* Gradle users - add the following to your dependencies in build file */
     implementation "com.scalekit:scalekit-sdk-java:2.1.3"
     ```

     ```xml
     
     
         com.scalekit
         scalekit-sdk-java
         2.1.3
     
     ```

   After installation, initialize the SDK with your credentials:

   * Node.js

     Initialize SDK

     ```js
     import { Scalekit } from '@scalekit-sdk/node';


     // Initialize the Scalekit client with your credentials
     const scalekit = new Scalekit(
       process.env.SCALEKIT_ENVIRONMENT_URL,
       process.env.SCALEKIT_CLIENT_ID,
       process.env.SCALEKIT_CLIENT_SECRET
     );
     ```

   * Python

     Initialize SDK

     ```python
     from scalekit import ScalekitClient
     import os


     # Initialize the Scalekit client with your credentials
     scalekit_client = ScalekitClient(
       env_url=os.getenv('SCALEKIT_ENVIRONMENT_URL'),
       client_id=os.getenv('SCALEKIT_CLIENT_ID'),
       client_secret=os.getenv('SCALEKIT_CLIENT_SECRET')
     )
     ```

   * Go

     Initialize SDK

     ```go
     import (
       "os"
       "github.com/scalekit-inc/scalekit-sdk-go"
     )


     // Initialize the Scalekit client with your credentials
     scalekitClient := scalekit.NewScalekitClient(
       os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
       os.Getenv("SCALEKIT_CLIENT_ID"),
       os.Getenv("SCALEKIT_CLIENT_SECRET"),
     )
     ```

   * Java

     Initialize SDK

     ```java
     import com.scalekit.ScalekitClient;


     // Initialize the Scalekit client with your credentials
     ScalekitClient scalekitClient = new ScalekitClient(
       System.getenv("SCALEKIT_ENVIRONMENT_URL"),
       System.getenv("SCALEKIT_CLIENT_ID"),
       System.getenv("SCALEKIT_CLIENT_SECRET")
     );
     ```

   SDK features

   All official SDKs include automatic retries, error handling, typed models, and auth helper methods to simplify your integration.

3. ## Verify your setup

   [Section titled “Verify your setup”](#verify-your-setup)

   Test your configuration by listing organizations in your workspace. This confirms your credentials work correctly.

   * cURL

     Authenticate with client credentials

     ```bash
     # Get an access token
     curl https:///oauth/token \
       -X POST \
       -H 'Content-Type: application/x-www-form-urlencoded' \
       -d 'client_id=' \
       -d 'client_secret=' \
       -d 'grant_type=client_credentials'
     ```

     This returns an access token:

     ```json
     {
       "access_token": "eyJhbGciOiJSUzI1NiIsImInR5cCI6IkpXVCJ9...",
       "token_type": "Bearer",
       "expires_in": 86399,
       "scope": "openid"
     }
     ```

     Use the token to access the Scalekit API

     List organizations

     ```sh
     curl -L '/api/v1/organizations?page_size=5' \
       -H 'Authorization: Bearer '
     ```

   * Node.js

     Create a file `verify.js` with the following code:

     verify.js

     ```javascript
     8 collapsed lines
     import { ScalekitClient } from '@scalekit-sdk/node';


     const scalekit = new ScalekitClient(
       process.env.SCALEKIT_ENVIRONMENT_URL,
       process.env.SCALEKIT_CLIENT_ID,
       process.env.SCALEKIT_CLIENT_SECRET,
     );


     const { organizations } = await scalekit.organization.listOrganization({
       pageSize: 5,
     });


     console.log(`Name of the first organization: ${organizations[0].display_name}`);
     ```

     Run the verification script:

     Run verification

     ```bash
     node verify.js
     ```

   * Python

     Create a file `verify.py` with the following code:

     verify.py

     ```python
     9 collapsed lines
     from scalekit import ScalekitClient
     import os


     # Initialize the SDK client
     scalekit_client = ScalekitClient(
       os.getenv('SCALEKIT_ENVIRONMENT_URL'),
       os.getenv('SCALEKIT_CLIENT_ID'),
       os.getenv('SCALEKIT_CLIENT_SECRET')
     )


     org_list = scalekit_client.organization.list_organizations(page_size=5)


     print(f'Name of the first organization: {org_list[0].display_name}')
     ```

     Run the verification script:

     Run verification

     ```bash
     python verify.py
     ```

   * Go

     Create a file `verify.go` with the following code:

     verify.go

     ```go
     18 collapsed lines
     package main


     import (
       "context"
       "fmt"
       "os"
       "github.com/scalekit-inc/scalekit-sdk-go"
     )


     func main() {
       ctx := context.Background()


       scalekitClient := scalekit.NewScalekitClient(
         os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
         os.Getenv("SCALEKIT_CLIENT_ID"),
         os.Getenv("SCALEKIT_CLIENT_SECRET"),
       )


       organizations, err := scalekitClient.Organization.ListOrganizations(ctx, &scalekit.ListOrganizationsParams{
         PageSize: 5,
       })


     4 collapsed lines
       if err != nil {
         panic(err)
       }


       fmt.Printf("Name of the first organization: %s\n", organizations[0].DisplayName)
     }
     ```

   * Java

     Create a file `Verify.java` with the following code:

     Verify.java

     ```java
     7 collapsed lines
     import com.scalekit.ScalekitClient;
     import com.scalekit.models.ListOrganizationsResponse;


     public class Verify {
       public static void main(String[] args) {
         ScalekitClient scalekitClient = new ScalekitClient(
           System.getenv("SCALEKIT_ENVIRONMENT_URL"),
           System.getenv("SCALEKIT_CLIENT_ID"),
           System.getenv("SCALEKIT_CLIENT_SECRET")
         );


         ListOrganizationsResponse organizations = scalekitClient.organizations().listOrganizations(5, "");
         System.out.println("Name of the first organization: " + organizations.getOrganizations()[0].getDisplayName());
       }
     }
     ```

   If you see organization data, your setup is complete! You’re now ready to implement authentication in your application.

## Set up Scalekit MCP Server Optional

[Section titled “Set up Scalekit MCP Server ”](#set-up-scalekit-mcp-server-)

Scalekit’s Model Context Protocol (MCP) server connects your AI coding assistants to Scalekit. Manage environments, organizations, users, and authentication through natural language queries in your MCP client.

The MCP server provides AI assistants with tools for environment management, organization and user management, authentication connection setup, role administration, and admin portal access. It uses OAuth 2.1 authentication to securely connect your AI tools to your Scalekit workspace.

Building your own MCP server?

If you’re building your own MCP server and need to add OAuth-based authorization, check out our guide: [Add auth to your MCP server](/authenticate/mcp/quickstart/).

### Configure your MCP client

[Section titled “Configure your MCP client”](#configure-your-mcp-client)

Use the most common client configs below. For the full list of supported MCP hosts and editor setups, see the [Scalekit MCP server guide](/dev-kit/ai-assisted-development/scalekit-mcp-server/).

* Claude Code

  Run this command in your terminal:

  Terminal

  ```bash
  claude mcp add --transport http scalekit https://mcp.scalekit.com/
  ```

* Cursor

  Edit `~/.cursor/mcp.json`, or open **Cursor Settings → MCP → Add New Global MCP Server** and paste the config:

  \~/.cursor/mcp.json

  ```json
  {
    "mcpServers": {
      "scalekit": {
        "url": "https://mcp.scalekit.com/"
      }
    }
  }
  ```

* Codex

  Run this command in your terminal:

  Terminal

  ```bash
  codex mcp add scalekit --url https://mcp.scalekit.com/
  ```

* OpenCode

  Edit `opencode.json` in your project root:

  opencode.json

  ```json
  {
    "mcp": {
      "scalekit": {
        "type": "remote",
        "url": "https://mcp.scalekit.com/"
      }
    }
  }
  ```

After configuration, your MCP client will initiate an OAuth authorization workflow to securely connect to Scalekit’s MCP server.

Note

For Claude Desktop, VS Code, Windsurf, Gemini CLI, Kiro, Warp, Zed, and other hosts, use the full [Scalekit MCP server guide](/dev-kit/ai-assisted-development/scalekit-mcp-server/).

## Configure code editors for Scalekit documentation

[Section titled “Configure code editors for Scalekit documentation”](#configure-code-editors-for-scalekit-documentation)

In-code editor chat features are powered by models that understand your codebase and project context. These models search the web for relevant information to help you. However, they may not always have the latest information. Follow the instructions below to configure your code editors to explicitly index for up-to-date information.

### Set up Cursor

[Section titled “Set up Cursor”](#set-up-cursor)

[Play](https://youtube.com/watch?v=oMMG1k_9fmU)

To enable Cursor to access up-to-date Scalekit documentation:

1. Open Cursor settings (Cmd/Ctrl + ,)
2. Navigate to **Indexing & Docs** section
3. Click on **Add**
4. Add `https://docs.scalekit.com/llms-full.txt` to the indexable URLs
5. Click on **Save**

Once configured, use `@Scalekit Docs` in your chat to ask questions about Scalekit features, APIs, and integration guides. Cursor will search the latest documentation to provide accurate, up-to-date answers.

### Use Windsurf

[Section titled “Use Windsurf”](#use-windsurf)

![Windsurf editor with the Cascade chat panel answering a question using indexed Scalekit documentation](/.netlify/images?url=_astro%2Fwindsurf.CfsQQlGb.png\&w=1357\&h=818\&dpl=6ae471fc143c9a4c27550463)

Windsurf enables `@docs` mentions within the Cascade chat to search for the best answers to your questions.

* Full Documentation

  ```plaintext
  @docs:https://docs.scalekit.com/llms-full.txt
  
  ```

  Costs more tokens.

* Specific Section

  ```plaintext
  @docs:https://docs.scalekit.com/your-specific-section-or-file
  
  ```

  Costs less tokens.

* Let AI decide

  ```plaintext
  @docs:https://docs.scalekit.com/llms.txt
  
  ```

  Costs tokens as per the model decisions.

## Use a coding agent

[Section titled “Use a coding agent”](#use-a-coding-agent)

Using a coding agent?Copy SaaSKit prompt

Change the skill name if you are adding a different product:

| Product              | Example prompt                                           |
| -------------------- | -------------------------------------------------------- |
| AgentKit             | `Use integrate-agentkit to connect this agent to Gmail.` |
| MCP authentication   | `Use add-mcp-oauth to add OAuth 2.1 to this MCP server.` |
| Single sign-on (SSO) | `Use implement-sso to add SAML and OIDC SSO.`            |
| SCIM                 | `Use implement-scim to add user provisioning.`           |

See the [Build with AI guide](/dev-kit/build-with-ai/) for the full setup.

[Play](https://youtube.com/watch?v=ZDAI32I6s-I)

---
# DOCUMENT BOUNDARY
---

# Complete login with code exchange

> Process authentication callbacks and handle redirect flows after users authenticate with Scalekit

Once users have successfully verified their identity using their chosen login method, Scalekit will have gathered the necessary user information for your app to complete the login process. However, your app must provide a callback endpoint where Scalekit can exchange an authorization code to return your app the user details.

1. ## Validate the `state` parameter recommended

   [Section titled “Validate the state parameter ”](#validate-the-state-parameter-)

   Before exchanging the authorization code, your application must validate the `state` parameter returned by Scalekit. Compare it with the value you stored in the user’s session before redirecting them. This critical step prevents Cross-Site Request Forgery (CSRF) attacks, ensuring the authentication response corresponds to a request initiated by the same user.

   * Node.js

     Validate state in Express.js

     ```javascript
     const { state } = req.query;


     // Assumes you are using a session middleware like express-session
     const storedState = req.session.oauthState;
     delete req.session.oauthState; // State should be used only once


     if (!state || state !== storedState) {
       console.error('Invalid state parameter');
       return res.redirect('/login?error=invalid_state');
     }
     ```

   * Python

     Validate state in Flask

     ```python
     from flask import session, request, redirect


     state = request.args.get('state')


     # Retrieve and remove stored state from session
     stored_state = session.pop('oauth_state', None)


     if not state or state != stored_state:
         print('Invalid state parameter')
         return redirect('/login?error=invalid_state')
     ```

   * Go

     Validate state in Gin

     ```go
     stateParam := c.Query("state")


     // Assumes you are using a session library like gin-contrib/sessions
     session := sessions.Default(c)
     storedState := session.Get("oauth_state")
     session.Delete("oauth_state") // State should be used only once
     session.Save()


     if stateParam == "" || stateParam != storedState {
         log.Println("Invalid state parameter")
         c.Redirect(http.StatusFound, "/login?error=invalid_state")
         return
     }
     ```

   * Java

     Validate state in Spring

     ```java
     // Assumes HttpSession is injected into your controller method
     String storedState = (String) session.getAttribute("oauth_state");
     session.removeAttribute("oauth_state"); // State should be used only once


     if (state == null || !state.equals(storedState)) {
         System.err.println("Invalid state parameter");
         return new RedirectView("/login?error=invalid_state");
     }
     ```

2. ## Exchange authorization code for tokens

   [Section titled “Exchange authorization code for tokens”](#exchange-authorization-code-for-tokens)

   Once the `state` is validated, your app can safely exchange the authorization code for tokens. The Scalekit SDK simplifies this process with the `authenticateWithCode` method, which handles the secure server-to-server request.

   * Node.js

     Express.js callback handler

     ```javascript
     1
     app.get('/auth/callback', async (req, res) => {
     2
       const { code, error, error_description, state } = req.query;
     3


     4
       //  Add state validation here (see previous step)
     11 collapsed lines
     5


     6
       // Handle errors first
     7
       if (error) {
     8
         console.error('Authentication error:', error);
     9
         return res.redirect('/login?error=auth_failed');
     10
       }
     11


     12
       if (!code) {
     13
         return res.redirect('/login?error=missing_code');
     14
       }
     15


     16
       try {
     17
         // Exchange code for user data
     18
         const authResult = await scalekit.authenticateWithCode(
     19
           code,
     20
           'https://yourapp.com/auth/callback'
     21
         );
     22


     23
         const { user, accessToken, refreshToken } = authResult;
     11 collapsed lines
     24


     25
         // TODO: Store user session (next guide covers this)
     26
         // req.session.user = user;
     27


     28
         res.redirect('/dashboard');
     29


     30
       } catch (error) {
     31
         console.error('Token exchange failed:', error);
     32
         res.redirect('/login?error=exchange_failed');
     33
       }
     34
     });
     ```

   * Python

     Flask callback handler

     ```python
     1
     @app.route('/auth/callback')
     2
     def auth_callback():
     3
         code = request.args.get('code')
     4
         error = request.args.get('error')
     9 collapsed lines
     5
         state = request.args.get('state')
     6


     7
         # TODO: Add state validation here (see previous step)
     8


     9
         # Handle errors first
     10
         if error:
     11
             print(f'Authentication error: {error}')
     12
             return redirect('/login?error=auth_failed')
     13


     14
         if not code:
     15
             return redirect('/login?error=missing_code')
     16


     17
         try:
     18
             # Exchange code for user data
     19
             options = CodeAuthenticationOptions()
     20
             auth_result = scalekit.authenticate_with_code(
     21
                 code,
     22
                 'https://yourapp.com/auth/callback',
     23
                 options
     24
             )
     25


     26
             user = auth_result.user
     27
             # access_token = auth_result.access_token
     28
             # refresh_token = auth_result.refresh_token
     6 collapsed lines
     29


     30
             # TODO: Store user session (next guide covers this)
     31
             # session['user'] = user
     32


     33
             return redirect('/dashboard')
     34


     35
         except Exception as e:
     36
             print(f'Token exchange failed: {e}')
     37
             return redirect('/login?error=exchange_failed')
     ```

   * Go

     Gin callback handler

     ```go
     1
     func authCallbackHandler(c *gin.Context) {
     2
         code := c.Query("code")
     3
         errorParam := c.Query("error")
     13 collapsed lines
     4
         stateParam := c.Query("state")
     5


     6
         // TODO: Add state validation here (see previous step)
     7


     8
         // Handle errors first
     9
         if errorParam != "" {
     10
             log.Printf("Authentication error: %s", errorParam)
     11
             c.Redirect(http.StatusFound, "/login?error=auth_failed")
     12
             return
     13
         }
     14


     15
         if code == "" {
     16
             c.Redirect(http.StatusFound, "/login?error=missing_code")
     17
             return
     18
         }
     19


     20
         // Exchange code for user data
     21
         options := scalekit.AuthenticationOptions{}
     22
         authResult, err := scalekitClient.AuthenticateWithCode(
     23
             c.Request.Context(), code,
     7 collapsed lines
     24
             "https://yourapp.com/auth/callback",
     25
             options,
     26
         )
     27


     28
         if err != nil {
     29
             log.Printf("Token exchange failed: %v", err)
     30
             c.Redirect(http.StatusFound, "/login?error=exchange_failed")
     31
             return
     32
         }
     33


     34
         user := authResult.User
     35
         // accessToken := authResult.AccessToken
     36
         // refreshToken := authResult.RefreshToken
     37


     38
         // TODO: Store user session (next guide covers this)
     39
         // session.Set("user", user)
     40


     41
         c.Redirect(http.StatusFound, "/dashboard")
     42
     }
     ```

   * Java

     Spring callback handler

     ```java
     1
     @GetMapping("/auth/callback")
     2
     public Object authCallback(
     3
         @RequestParam(required = false) String code,
     4
         @RequestParam(required = false) String error,
     5
         @RequestParam(required = false) String state,
     10 collapsed lines
     6
         HttpSession session
     7
     ) {
     8
         // TODO: Add state validation here (see previous step)
     9


     10
         // Handle errors first
     11
         if (error != null) {
     12
             System.err.println("Authentication error: " + error);
     13
             return new RedirectView("/login?error=auth_failed");
     14
         }
     15


     16
         if (code == null) {
     17
             return new RedirectView("/login?error=missing_code");
     18
         }
     19


     20
         try {
     21
             // Exchange code for user data
     22
             AuthenticationOptions options = new AuthenticationOptions();
     23
             AuthenticationResponse authResult = scalekit
     24
                 .authentication()
     25
                 .authenticateWithCode(code, "https://yourapp.com/auth/callback", options);
     26


     27
             var user = authResult.getIdTokenClaims();
     28
             // String accessToken = authResult.getAccessToken();
     29
             // String refreshToken = authResult.getRefreshToken();
     30


     6 collapsed lines
     31
             // TODO: Store user session (next guide covers this)
     32
             // session.setAttribute("user", user);
     33


     34
             return new RedirectView("/dashboard");
     35


     36
         } catch (Exception e) {
     37
             System.err.println("Token exchange failed: " + e.getMessage());
     38
             return new RedirectView("/login?error=exchange_failed");
     39
         }
     40
     }
     ```

   The authorization `code` can be redeemed only once and expires in approx \~10 minutes. Reuse or replay attempts typically return errors like `invalid_grant`. If this occurs, start a new login flow to obtain a fresh `code` and `state`.

   The `authResult` object returned contains:

   ```js
     {
       user: {
         email: "john.doe@example.com",
         emailVerified: true,
         givenName: "John",
         name: "John Doe",
         id: "usr_74599896446906854"
       },
       idToken: "eyJhbGciO..", // Decode for full user details


       accessToken: "eyJhbGciOi..",
       refreshToken: "rt_8f7d6e5c4b3a2d1e0f9g8h7i6j..",
       expiresIn: 299 // in seconds
     }
   ```

   | Key            | Description                                                   |
   | -------------- | ------------------------------------------------------------- |
   | `user`         | Common user details with email, name, and verification status |
   | `idToken`      | JWT containing verified full user identity claims             |
   | `accessToken`  | Short-lived token that determines current access              |
   | `refreshToken` | Long-lived token to obtain new access tokens                  |

3. ## Decoding token claims

   [Section titled “Decoding token claims”](#decoding-token-claims)

   The `idToken` and `accessToken` are JSON Web Tokens (JWT) that contain user claims. These tokens can be decoded to retrieve comprehensive user and access information.

   * Node.js

     Decode ID token

     ```javascript
     // Use a library like 'jsonwebtoken'
     const jwt = require('jsonwebtoken');


     // The idToken from the authResult object
     const { idToken } = authResult;


     // Decode the token without verifying its signature
     const decoded = jwt.decode(idToken);


     console.log('Decoded claims:', decoded);
     ```

   * Python

     Decode ID token

     ```python
     # Use a library like 'PyJWT'
     import jwt


     # The id_token from the auth_result object
     id_token = auth_result.id_token


     # Decode the token without verifying its signature
     decoded = jwt.decode(id_token, options={"verify_signature": False})
     print(f'Decoded claims: {decoded}')
     ```

   * Go

     Decode ID token

     ```go
     1
     // Use a library like 'github.com/golang-jwt/jwt/v5'
     2
     import (
     3
       "fmt"
     4
       "github.com/golang-jwt/jwt/v5"
     5
     )
     6


     7
     // The IdToken from the authResult object
     8
     idToken := authResult.IdToken
     9
     token, _, err := new(jwt.Parser).ParseUnverified(idToken, jwt.MapClaims{})
     10
     if err != nil {
     11
       fmt.Printf("Error parsing token: %v\n", err)
     12
       return
     13
     }
     14


     15
     if claims, ok := token.Claims.(jwt.MapClaims); ok {
     16
       fmt.Printf("Decoded claims: %+v\n", claims)
     17
     }
     ```

   * Java

     Decode ID token

     ```java
     1
     // Use a library like 'com.auth0:java-jwt'
     2
     import com.auth0.jwt.JWT;
     3
     import com.auth0.jwt.interfaces.DecodedJWT;
     4
     import com.auth0.jwt.interfaces.Claim;
     5
     import com.auth0.jwt.exceptions.JWTDecodeException;
     6
     import java.util.Map;
     7


     8
     try {
     9
         // The idToken from the authResult object
     10
         String idToken = authResult.getIdToken();
     11


     12
         // Decode the token without verifying its signature
     13
         DecodedJWT decodedJwt = JWT.decode(idToken);
     14
         Map claims = decodedJwt.getClaims();
     15


     16
         System.out.println("Decoded claims: " + claims);
     17
     } catch (JWTDecodeException exception){
     18
         // Invalid token
     19
         System.err.println("Failed to decode ID token: " + exception.getMessage());
     20
     }
     ```

   The decoded token claims contain:

   * Decoded ID token

     ID token decoded

     ```json
     1
     {
     2
       "iss": "https://scalekit-z44iroqaaada-dev.scalekit.cloud", // Issuer: Scalekit environment URL (must match your environment)
     3
       "aud": ["skc_58327482062864390"], // Audience: Your client ID (must match for validation)
     4
       "azp": "skc_58327482062864390", // Authorized party: Usually same as aud
     5
       "sub": "usr_63261014140912135", // Subject: User's unique identifier
     6
       "oid": "org_59615193906282635", // Organization ID: User's organization
     7
       "exp": 1742975822, // Expiration: Unix timestamp (validate token hasn't expired)
     8
       "iat": 1742974022, // Issued at: Unix timestamp when token was issued
     9
       "at_hash": "ec_jU2ZKpFelCKLTRWiRsg", // Access token hash: For token binding validation
     10
       "c_hash": "6wMreK9kWQQY6O5R0CiiYg", // Authorization code hash: For code binding validation
     11
       "amr": ["conn_123"], // Authentication method reference: Connection ID used for auth
     12
       "email": "john.doe@example.com", // User's email address
     13
       "email_verified": true, // Email verification status
     14
       "name": "John Doe", // User's full name (optional)
     15
       "given_name": "John", // User's first name (optional)
     16
       "family_name": "Doe", // User's last name (optional)
     17
       "picture": "https://...", // Profile picture URL (optional)
     18
       "locale": "en", // User's locale preference (optional)
     19
       "sid": "ses_65274187031249433", // Session ID: Links token to user session
     20
       "client_id": "skc_58327482062864390", // Client ID: Your application identifier
     21
       "xoid": "ext_org_123", // External organization ID (if mapped)
     22
     }
     ```

   * Decoded access token

     Decoded access token

     ```json
     1
     {
     2
         "iss": "https://login.devramp.ai", // Issuer: Scalekit environment URL (must match your environment)
     3
         "aud": ["prd_skc_7848964512134X699"], // Audience: Your client ID (must match for validation)
     4
         "sub": "usr_8967800122X995270", // Subject: User's unique identifier
     5
         "oid": "org_89678001X21929734", // Organization ID: User's organization
     6
         "exp": 1758265247, // Expiration: Unix timestamp (validate token hasn't expired)
     7
         "iat": 1758264947, // Issued at: Unix timestamp when token was issued
     8
         "nbf": 1758264947, // Not before: Unix timestamp (token valid from this time)
     9
         "jti": "tkn_90928731115292X63", // JWT ID: Unique token identifier
     10
         "sid": "ses_90928729571723X24", // Session ID: Links token to user session
     11
         "client_id": "prd_skc_7848964512134X699", // Client ID: Your application identifier
     12
         "roles": ["admin"], // Roles: User roles within organization (optional, for authorization)
     13
         "permissions": ["workspace_data:write", "workspace_data:read"], // Permissions: resource:action format (optional, for granular access control)
     14
         "scope": "openid profile email", // OAuth scopes granted (optional)
     15
         "xoid": "ext_org_123", // External organization ID (if mapped)
     16
         "xuid": "ext_usr_456" // External user ID (if mapped)
     17
     }
     ```

   ID token claims reference

   ID tokens contain cryptographically signed claims about a user’s profile information. The Scalekit SDK automatically validates ID tokens when you use `authenticateWithCode`. If you need to manually verify or access custom claims, use the claim reference below.

   | Claim            | Presence | Description                                     |
   | ---------------- | -------- | ----------------------------------------------- |
   | `iss`            | Always   | Issuer identifier (Scalekit environment URL)    |
   | `aud`            | Always   | Intended audience (your client ID)              |
   | `sub`            | Always   | Subject identifier (user’s unique ID)           |
   | `oid`            | Always   | Organization ID of the user                     |
   | `exp`            | Always   | Expiration time (Unix timestamp)                |
   | `iat`            | Always   | Issuance time (Unix timestamp)                  |
   | `at_hash`        | Always   | Access token hash for validation                |
   | `c_hash`         | Always   | Authorization code hash for validation          |
   | `azp`            | Always   | Authorized presenter (usually same as `aud`)    |
   | `amr`            | Always   | Authentication method reference (connection ID) |
   | `email`          | Always   | User’s email address                            |
   | `email_verified` | Optional | Email verification status                       |
   | `name`           | Optional | User’s full name                                |
   | `family_name`    | Optional | User’s surname or last name                     |
   | `given_name`     | Optional | User’s given name or first name                 |
   | `locale`         | Optional | User’s locale (BCP 47 language tag)             |
   | `picture`        | Optional | URL of user’s profile picture                   |
   | `sid`            | Always   | Session identifier                              |
   | `client_id`      | Always   | Your application’s client ID                    |

   Validate ID tokens without an SDK

   Use this flow when you validate Scalekit ID tokens in a language without an official SDK (for example, Ruby on Rails or PHP). The Scalekit SDK validates tokens automatically when you call `authenticateWithCode`.

   | Parameter             | Value                                                             |
   | --------------------- | ----------------------------------------------------------------- |
   | OpenID configuration  | `https:///.well-known/openid-configuration` |
   | Issuer (`iss`)        | Your Scalekit environment URL                                     |
   | JWKS URI (`jwks_uri`) | `https:///keys`                             |
   | Signing algorithm     | `RS256`                                                           |

   1. Fetch `/.well-known/openid-configuration` and read `issuer` and `jwks_uri`.
   2. Fetch the JWKS document from `jwks_uri`.
   3. Verify the JWT signature with **RS256** using the key whose `kid` matches the token header.
   4. Validate `iss`, `aud`, and `exp`. The `aud` claim may be a string or an array of strings. Your application’s client ID (`skc_...`) must appear in `aud` — reject the token if it does not.

   Audience may be a string or array

   OIDC allows `aud` as either a single string or an array. When `aud` is an array, check that your client ID is included. Ruby’s `verify_aud: true` with a string `aud` option handles both shapes in recent `jwt` gem versions; in other languages, normalize `aud` to a list before comparing.

   Ruby on Rails example

   ```ruby
   1
   require "jwt"
   2
   require "net/http"
   3
   require "json"
   4


   5
   ENV_URL = ENV.fetch("SCALEKIT_ENVIRONMENT_URL")
   6
   CLIENT_ID = ENV.fetch("SCALEKIT_CLIENT_ID")
   7


   8
   config = JSON.parse(Net::HTTP.get(URI("#{ENV_URL}/.well-known/openid-configuration")))
   9
   jwks = JSON.parse(Net::HTTP.get(URI(config["jwks_uri"])))
   10


   11
   decoded, = JWT.decode(
   12
     id_token, nil, true,
   13
     algorithms: ["RS256"], jwks: jwks,
   14
     iss: config["issuer"], verify_iss: true,
   15
     aud: CLIENT_ID, verify_aud: true
   16
   )
   ```

   Confirm endpoints before wiring validation into your app:

   ```bash
   curl -s "https:///.well-known/openid-configuration" | jq '{issuer, jwks_uri}'
   curl -s "https:///keys" | jq '.keys[] | {kid, alg, use}'
   ```

   See [ID token claims](/guides/idtoken-claims/) for the full claim list.

   Access token claims reference

   Access tokens contain authorization information including roles and permissions. Use these claims to make authorization decisions in your application.

   **Roles** group related permissions together and define what users can do in your system. Common examples include Admin, Manager, Editor, and Viewer. Roles can inherit permissions from other roles, creating hierarchical access levels.

   **Permissions** represent specific actions users can perform, formatted as `resource:action` patterns like `projects:create` or `tasks:read`. Use permissions for granular access control when you need precise control over individual capabilities.

   Scalekit automatically assigns the `admin` role to the first user in each organization and the `member` role to subsequent users. Your application uses the role and permission information from Scalekit to make final authorization decisions at runtime.

   | Claim         | Presence | Description                                      |
   | ------------- | -------- | ------------------------------------------------ |
   | `iss`         | Always   | Issuer identifier (Scalekit environment URL)     |
   | `aud`         | Always   | Intended audience (your client ID)               |
   | `sub`         | Always   | Subject identifier (user’s unique ID)            |
   | `oid`         | Always   | Organization ID of the user                      |
   | `exp`         | Always   | Expiration time (Unix timestamp)                 |
   | `iat`         | Always   | Issuance time (Unix timestamp)                   |
   | `nbf`         | Always   | Not before time (Unix timestamp)                 |
   | `jti`         | Always   | JWT ID (unique token identifier)                 |
   | `sid`         | Always   | Session identifier                               |
   | `client_id`   | Always   | Client identifier for the application            |
   | `roles`       | Optional | Array of role names assigned to the user         |
   | `permissions` | Optional | Array of permissions in `resource:action` format |
   | `scope`       | Optional | Space-separated list of OAuth scopes granted     |

   Scalekit can include the following metadata claims in access tokens when this feature is enabled for your environment. Self-serve configuration in the Scalekit Dashboard is coming soon. Until then, request access in the [Scalekit Slack community](https://join.slack.com/t/scalekit-community/shared_invite/zt-3gsxwr4hc-0tvhwT2b_qgVSIZQBQCWRw).

   | Claim                 | Source                | Description                                                                  |
   | --------------------- | --------------------- | ---------------------------------------------------------------------------- |
   | `user_metadata`       | User record           | Custom key-value pairs attached to the user                                  |
   | `membership_metadata` | User’s org membership | Custom key-value pairs attached to the user’s membership in the organization |
   | `org_metadata`        | Organization record   | Custom key-value pairs attached to the organization                          |

   Empty metadata is omitted. Scalekit adds these claims only when the corresponding metadata object is non-empty.

4. ## Verifying access tokens optional

   [Section titled “Verifying access tokens ”](#verifying-access-tokens-)

   The Scalekit SDK provides methods to validate tokens automatically. When you use the SDK’s `validateAccessToken` method, it:

   1. Verifies the token signature using Scalekit’s public keys
   2. Checks the token hasn’t expired (`exp` claim)
   3. Validates the issuer (`iss` claim) matches your environment
   4. Ensures the audience (`aud` claim) matches your client ID

   If you need to manually verify tokens, fetch the public signing keys from the JSON Web Key Set (JWKS) endpoint:

   JWKS endpoint

   ```sh
   https:///keys
   ```

   For example, if your Scalekit Environment URL is `https://your-environment.scalekit.com`, the keys can be found at `https://your-environment.scalekit.com/keys`.

   Important claims to validate

   When validating tokens manually, pay attention to these claims:

   * **`iss` (Issuer)**: Must match your Scalekit environment URL
   * **`aud` (Audience)**: Must match your application’s client ID
   * **`exp` (Expiration Time)**: Ensure the token has not expired
   * **`sub` (Subject)**: Uniquely identifies the user
   * **`oid` (Organization ID)**: Identifies which organization the user belongs to

An `IdToken` contains comprehensive profile information about the user. You can save this in your database for app use cases, using [your own identifier](/fsa/guides/organization-identifiers/). Now, let’s utilize *access and refresh tokens* to manage user access and maintain active sessions.

## Common login scenarios

[Section titled “Common login scenarios”](#common-login-scenarios)

Customize the login flow by passing different parameters when creating the authorization URL. These scenarios help you route users to specific organizations, force re-authentication, or direct users to signup.

Include state in production code

The routing examples below omit `state` so the parameter focus stays visible. In production, always set `options.state` to a cryptographically random value, store it server-side before redirecting, and validate it on callback. See [Validate the `state` parameter](#validate-the-state-parameter-) above and [Initiate user login](/authenticate/fsa/implement-login/).

How do I route users to a specific organization?

For multi-tenant applications, you can route users directly to their organization’s authentication method using `organizationId`. This is useful when you already know the user’s organization.

* Node.js

  Express.js

  ```javascript
  const orgId = getOrganizationFromRequest(req)
  const redirectUri = 'https://your-app.com/auth/callback'
  const options = {
    scopes: ['openid', 'profile', 'email', 'offline_access'],
    organizationId: orgId,
  }
  const url = scalekit.getAuthorizationUrl(redirectUri, options)
  return res.redirect(url)
  ```

* Python

  Flask

  ```python
  from scalekit import AuthorizationUrlOptions


  org_id = get_org_from_request(request)
  redirect_uri = 'https://your-app.com/auth/callback'
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email', 'offline_access']
  options.organization_id = org_id
  url = scalekit_client.get_authorization_url(redirect_uri, options)
  return redirect(url)
  ```

* Go

  Gin

  ```go
  orgID := getOrgFromRequest(c)
  redirectUri := "https://your-app.com/auth/callback"
  options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, OrganizationId: orgID}
  url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  c.Redirect(http.StatusFound, url.String())
  ```

* Java

  Spring

  ```java
  String orgId = getOrgFromRequest(request);
  String redirectUri = "https://your-app.com/auth/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  options.setScopes(Arrays.asList("openid","profile","email","offline_access"));
  options.setOrganizationId(orgId);
  URL url = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options);
  return new RedirectView(url.toString());
  ```

How do I route users based on email domain?

If you don’t know the organization ID beforehand, you can use `loginHint` to let Scalekit determine the correct authentication method from the user’s email domain. This is common for enterprise logins where the email domain is associated with a specific SSO connection. The domain must be registered to the organization either manually from the Scalekit Dashboard or through the admin portal when [onboarding an enterprise customer](/sso/guides/onboard-enterprise-customers/).

* Node.js

  Express.js

  ```javascript
  const redirectUri = 'https://your-app.com/auth/callback'
  const options = {
      scopes: ['openid', 'profile', 'email', 'offline_access'],
      loginHint: userEmail
    }
  const url = scalekit.getAuthorizationUrl(redirectUri, options)
  return res.redirect(url)
  ```

* Python

  Flask

  ```python
  redirect_uri = 'https://your-app.com/auth/callback'
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email', 'offline_access']
  options.login_hint = user_email
  url = scalekit_client.get_authorization_url(redirect_uri, options)
  return redirect(url)
  ```

* Go

  Gin

  ```go
  redirectUri := "https://your-app.com/auth/callback"
  options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, LoginHint: userEmail}
  url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  c.Redirect(http.StatusFound, url.String())
  ```

* Java

  Spring

  ```java
  String redirectUri = "https://your-app.com/auth/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  options.setScopes(Arrays.asList("openid","profile","email","offline_access"));
  options.setLoginHint(userEmail);
  URL url = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options);
  return new RedirectView(url.toString());
  ```

How do I route users to a specific SSO connection?

When you know the exact enterprise connection a user should use, you can pass its `connectionId` for the highest routing precision. This bypasses any other routing logic.

* Node.js

  Express.js

  ```javascript
  const redirectUri = 'https://your-app.com/auth/callback'
  const options = {
      scopes: ['openid', 'profile', 'email', 'offline_access'],
      connectionId: 'conn_123...'
    }
  const url = scalekit.getAuthorizationUrl(redirectUri, options)
  return res.redirect(url)
  ```

* Python

  Flask

  ```python
  redirect_uri = 'https://your-app.com/auth/callback'
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email', 'offline_access']
  options.connection_id = 'conn_123...'
  url = scalekit_client.get_authorization_url(redirect_uri, options)
  return redirect(url)
  ```

* Go

  Gin

  ```go
  redirectUri := "https://your-app.com/auth/callback"
  options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, ConnectionId: "conn_123..."}
  url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  c.Redirect(http.StatusFound, url.String())
  ```

* Java

  Spring

  ```java
  String redirectUri = "https://your-app.com/auth/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  options.setScopes(Arrays.asList("openid","profile","email","offline_access"));
  options.setConnectionId("conn_123...");
  URL url = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options);
  return new RedirectView(url.toString());
  ```

How do I force users to re-authenticate?

You can require users to authenticate again, even if they have an active session, by setting `prompt: 'login'`. This is useful for high-security actions that require recent authentication.

* Node.js

  Express.js

  ```javascript
  const redirectUri = 'https://your-app.com/auth/callback'
  const options = {
      scopes: ['openid', 'profile', 'email', 'offline_access'],
      prompt: 'login'
    }
  return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options))
  ```

* Python

  Flask

  ```python
  redirect_uri = 'https://your-app.com/auth/callback'
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email', 'offline_access']
  options.prompt = 'login'
  return redirect(scalekit_client.get_authorization_url(redirect_uri, options))
  ```

* Go

  Gin

  ```go
  redirectUri := "https://your-app.com/auth/callback"
  options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, Prompt: "login"}
  url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  c.Redirect(http.StatusFound, url.String())
  ```

* Java

  Spring

  ```java
  String redirectUri = "https://your-app.com/auth/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  options.setScopes(Arrays.asList("openid","profile","email","offline_access"));
  options.setPrompt("login");
  return new RedirectView(scalekitClient.authentication().getAuthorizationUrl(redirectUri, options).toString());
  ```

How do I let users choose an account or organization?

To show the organization or account chooser, set `prompt: 'select_account'`. This is helpful when a user is part of multiple organizations and needs to select which one to sign into.

* Node.js

  Express.js

  ```javascript
  const redirectUri = 'https://your-app.com/auth/callback'
  const options = {
      scopes: ['openid', 'profile', 'email', 'offline_access'],
      prompt: 'select_account'
    }
  return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options))
  ```

* Python

  Flask

  ```python
  redirect_uri = 'https://your-app.com/auth/callback'
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email', 'offline_access']
  options.prompt = 'select_account'
  return redirect(scalekit_client.get_authorization_url(redirect_uri, options))
  ```

* Go

  Gin

  ```go
  redirectUri := "https://your-app.com/auth/callback"
  options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, Prompt: "select_account"}
  url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  c.Redirect(http.StatusFound, url.String())
  ```

* Java

  Spring

  ```java
  String redirectUri = "https://your-app.com/auth/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  options.setScopes(Arrays.asList("openid","profile","email","offline_access"));
  options.setPrompt("select_account");
  return new RedirectView(scalekitClient.authentication().getAuthorizationUrl(redirectUri, options).toString());
  ```

How do I send users directly to signup?

To send users directly to the signup form instead of the login page, use `prompt: 'create'`.

* Node.js

  Express.js

  ```javascript
  const redirectUri = 'https://your-app.com/auth/callback'
  const options = {
      scopes: ['openid', 'profile', 'email', 'offline_access'],
      prompt: 'create'
  }
  return res.redirect(scalekit.getAuthorizationUrl(redirectUri, options))
  ```

* Python

  Flask

  ```python
  redirect_uri = 'https://your-app.com/auth/callback'
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email', 'offline_access']
  options.prompt = 'create'
  return redirect(scalekit_client.get_authorization_url(redirect_uri, options))
  ```

* Go

  Gin

  ```go
  redirectUri := "https://your-app.com/auth/callback"
  options := scalekitClient.AuthorizationUrlOptions{Scopes: []string{"openid","profile","email","offline_access"}, Prompt: "create"}
  url, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  c.Redirect(http.StatusFound, url.String())
  ```

* Java

  Spring

  ```java
  String redirectUri = "https://your-app.com/auth/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  options.setScopes(Arrays.asList("openid","profile","email","offline_access"));
  options.setPrompt("create");
  return new RedirectView(scalekitClient.authentication().getAuthorizationUrl(redirectUri, options).toString());
  ```

How do I redirect users back to the page they requested after authentication?

When users bookmark specific pages or their session expires, redirect them to their original destination after authentication. Store the intended path in a secure cookie before redirecting to Scalekit, then read it after the callback.

**Step 1: Capture the intended destination**

Before redirecting to Scalekit, store the user’s requested path in a secure cookie:

* Node.js

  Express.js

  ```javascript
  app.get('/login', (req, res) => {
    const nextPath = typeof req.query.next === 'string' ? req.query.next : '/'
    // Only allow internal paths to prevent open redirects
    const safe = nextPath.startsWith('/') && !nextPath.startsWith('//') ? nextPath : '/'
    res.cookie('sk_return_to', safe, { httpOnly: true, secure: true, sameSite: 'lax', path: '/' })
    // Build authorization URL and redirect to Scalekit
  })
  ```

* Python

  Flask

  ```python
  @app.route('/login')
  def login():
      next_path = request.args.get('next', '/')
      safe = next_path if next_path.startswith('/') and not next_path.startswith('//') else '/'
      resp = make_response()
      resp.set_cookie('sk_return_to', safe, httponly=True, secure=True, samesite='Lax', path='/')
      return resp
  ```

* Go

  Gin

  ```go
  func login(c *gin.Context) {
    nextPath := c.Query("next")
    if nextPath == "" || !strings.HasPrefix(nextPath, "/") || strings.HasPrefix(nextPath, "//") {
      nextPath = "/"
    }
    cookie := &http.Cookie{Name: "sk_return_to", Value: nextPath, HttpOnly: true, Secure: true, Path: "/"}
    http.SetCookie(c.Writer, cookie)
  }
  ```

* Java

  Spring

  ```java
  @GetMapping("/login")
  public void login(HttpServletRequest request, HttpServletResponse response) {
    String nextPath = Optional.ofNullable(request.getParameter("next")).orElse("/");
    boolean safe = nextPath.startsWith("/") && !nextPath.startsWith("//");
    Cookie cookie = new Cookie("sk_return_to", safe ? nextPath : "/");
    cookie.setHttpOnly(true); cookie.setSecure(true); cookie.setPath("/");
    response.addCookie(cookie);
  }
  ```

**Step 2: Redirect after callback**

After exchanging the authorization code, read the cookie and redirect to the stored path:

* Node.js

  Express.js

  ```javascript
  app.get('/auth/callback', async (req, res) => {
    // ... exchange code ...
    const raw = req.cookies.sk_return_to || '/'
    const safe = raw.startsWith('/') && !raw.startsWith('//') ? raw : '/'
    res.clearCookie('sk_return_to', { path: '/' })
    res.redirect(safe || '/dashboard')
  })
  ```

* Python

  Flask

  ```python
  def callback():
      # ... exchange code ...
      raw = request.cookies.get('sk_return_to', '/')
      safe = raw if raw.startswith('/') and not raw.startswith('//') else '/'
      resp = redirect(safe or '/dashboard')
      resp.delete_cookie('sk_return_to', path='/')
      return resp
  ```

* Go

  Gin

  ```go
  func callback(c *gin.Context) {
    // ... exchange code ...
    raw, _ := c.Cookie("sk_return_to")
    if raw == "" || !strings.HasPrefix(raw, "/") || strings.HasPrefix(raw, "//") {
      raw = "/"
    }
    http.SetCookie(c.Writer, &http.Cookie{Name: "sk_return_to", Value: "", MaxAge: -1, Path: "/"})
    c.Redirect(http.StatusFound, raw)
  }
  ```

* Java

  Spring

  ```java
  public RedirectView callback(HttpServletRequest request, HttpServletResponse response) {
    // ... exchange code ...
    String raw = getCookie(request, "sk_return_to").orElse("/");
    boolean ok = raw.startsWith("/") && !raw.startsWith("//");
    Cookie clear = new Cookie("sk_return_to", ""); clear.setPath("/"); clear.setMaxAge(0);
    response.addCookie(clear);
    return new RedirectView(ok ? raw : "/dashboard");
  }
  ```

Never redirect to external origins

Allow only same-origin paths (e.g., `/billing`). Do not accept absolute URLs or protocol-relative URLs. This blocks open redirect attacks.

How do I configure access token lifetime?

Access tokens have a default expiration time, but you can adjust this based on your security requirements. Shorter lifetimes provide better security by limiting the window of exposure if a token is compromised, while longer lifetimes reduce the frequency of token refresh operations.

To configure the access token lifetime:

1. Navigate to the **Scalekit Dashboard**
2. Go to **Authentication** > **Session Policy**
3. Adjust the **Access Token Lifetime** setting to your preferred duration

The `expiresIn` value in the authentication response reflects this configured lifetime in seconds. When the access token expires, use the refresh token to obtain a new access token without requiring the user to re-authenticate.

What is the routing precedence for login?

Scalekit applies connection selection in this order: `connectionId` (or `connection_id`) → `organizationId` → `loginHint` (domain extraction). Prefer the highest confidence signal you have.

Why should I always send a state parameter?

Include a cryptographically strong `state` parameter and validate it on callback to prevent CSRF and session fixation attacks. See [our CSRF protection guide](/guides/security/authentication-best-practices/) for details.

---
# DOCUMENT BOUNDARY
---

# Initiate user signup or login

> Create authorization URLs and redirect users to Scalekit's hosted login page

Login initiation begins your authentication flow. You redirect users to Scalekit’s hosted login page by creating an authorization URL with appropriate parameters. When users visit this URL, Scalekit’s authorization server validates the request, displays the login interface, and handles authentication through your configured connection methods (SSO, social providers, Magic Link or Email OTP).

Authorization URL format

```sh
/oauth/authorize?
  response_type=code& # always `code` for authorization code flow
  client_id=& # Dashboard > Developers > Settings > API Credentials
  redirect_uri=& # Dashboard > Authentication > Redirect URLs > Allowed Callback URLs
  scope=openid+profile+email+offline_access& # Permissions requested. Include `offline_access` for refresh tokens
  state= # prevent CSRF attacks
```

The authorization request includes several parameters that control authentication behavior:

* **Required parameters** ensure Scalekit can identify your application and return the user securely
* **Optional parameters** enable organization routing and pre-populate fields
* **Security parameters** prevent unauthorized access attempts

Understand each parameter and how it controls the authorization flow:

| Query parameter   | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `response_type`   | Set to `code` for authorization code flow Required Indicates the expected response type                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `client_id`       | Your application’s public identifier from the dashboard Required Scalekit uses this to identify and validate your application                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `redirect_uri`    | Your application’s callback URL where Scalekit returns the authorization code Required Must be registered in your dashboard settings                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `scope`           | Space-separated list of permissions Required Always include `openid profile email`. Add `offline_access` to request refresh tokens for extended sessions                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `state`           | Random string generated by your application Recommended Scalekit returns this unchanged. Use it to prevent CSRF attacks and maintain request state                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `prompt`          | Value to control the authentication flow Optional Use `login` to force re-authentication even when the user has a valid session. Send it only when you need to re-verify identity — including it on routine logins re-prompts users unnecessarily, a common cause of users appearing logged out Use `create` to trigger the sign-up page Use `select_account` to force the organization selector, even if the user has an active session with an organization. The selector appears only when the user belongs to multiple organizations Use `none` to check silently whether a session exists, without showing the login or sign-up page |
| `organization_id` | Skip routing the user to the hosted login page and route them to the enterprise SSO connection configured for the organization Optional                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `connection_id`   | Skip routing the user to the hosted login page and route them to a specific enterprise SSO connection Optional                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `login_hint`      | Used for [Home Realm Discovery](/authenticate/auth-methods/enterprise-sso/#identify-and-enforce-sso-for-organization-users). Scalekit extracts the email domain from `login_hint` and routes the user to the matching organization’s SSO connection based on configured domain rules Optional                                                                                                                                                                                                                                                                                                                                             |
| `provider`        | Skip routing user to hosted login page and direct user to a specific social connection. Supported values: `google`, `microsoft`, `github`, `gitlab`, `linkedin`, `salesforce`, and `facebook` Optional                                                                                                                                                                                                                                                                                                                                                                                                                                    |

## Set up login flow

[Section titled “Set up login flow”](#set-up-login-flow)

1. #### Add `state` parameter recommended

   [Section titled “Add state parameter ”](#add-state-parameter-)

   Always generate a cryptographically secure random string for the `state` parameter and store it temporarily (session, local storage, cache, etc).

   This can be used to validate that the state value returned in the callback matches the original value you sent. This prevents **CSRF (Cross-Site Request Forgery)** attacks where an attacker tricks users into approving unauthorized authentication requests.

   * Node.js

     Generate and store state

     ```javascript
     // Generate secure random state
     const state = require('crypto').randomBytes(32).toString('hex');
     // Store it temporarily (session, local storage, cache, etc)
     sessionStorage.oauthState = state;
     ```

   * Python

     Generate and store state

     ```python
     import os
     import secrets


     # Generate secure random state
     state = secrets.token_hex(32)
     # Store it temporarily (session, local storage, cache, etc)
     session['oauth_state'] = state
     ```

   * Go

     Generate and store state

     ```go
     import (
         "crypto/rand"
         "encoding/hex"
     )


     // Generate secure random state
     b := make([]byte, 32)
     rand.Read(b)
     state := hex.EncodeToString(b)
     // Store it temporarily (session, local storage, cache, etc)
     // Example for Go: use a storage library
     // session.Set("oauth_state", state)
     ```

   * Java

     Generate and store state

     ```java
     import java.security.SecureRandom;
     import java.util.Base64;


     // Generate secure random state
     SecureRandom sr = new SecureRandom();
     byte[] randomBytes = new byte[32];
     sr.nextBytes(randomBytes);
     String state = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes);
     // Store it temporarily (session, local storage, cache, etc)
     // Example for Java: use any storage library
     // session.setAttribute("oauth_state", state);
     ```

2. #### Redirect to the authorization URL

   [Section titled “Redirect to the authorization URL”](#redirect-to-the-authorization-url)

   Use the Scalekit SDK to generate the authorization URL. This method constructs the URL locally without making network requests. Redirect users to this URL to start authentication.

   * Node.js

     Express.js

     ```diff
     4 collapsed lines
     import { Scalekit } from '@scalekit-sdk/node';


     const scalekit = new Scalekit(/* your credentials */);


     // Basic authorization URL for general login
     const redirectUri = 'https://yourapp.com/auth/callback';
     const options = {
       scopes: ['openid', 'profile', 'email', 'offline_access'],
       state: sessionStorage.oauthState,
     };


     const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options);


     // Redirect user to Scalekit's hosted login page
     res.redirect(authorizationUrl);
     ```

   * Python

     Flask

     ```python
     3 collapsed lines
     from scalekit import ScalekitClient, AuthorizationUrlOptions


     scalekit = ScalekitClient(/* your credentials */)


     # Basic authorization URL for general login
     redirect_uri = 'https://yourapp.com/auth/callback'
     options = AuthorizationUrlOptions()
     options.scopes = ['openid', 'profile', 'email', 'offline_access']
     options.state = session['oauth_state']


     authorization_url = scalekit.get_authorization_url(redirect_uri, options)


     # Redirect user to Scalekit's hosted login page
     return redirect(authorization_url)
     ```

   * Go

     Gin

     ```go
     4 collapsed lines
     import "github.com/scalekit-inc/scalekit-sdk-go"


     scalekit := scalekit.NewScalekitClient(/* your credentials */)


     // Basic authorization URL for general login
     redirectUri := "https://yourapp.com/auth/callback"
     options := scalekit.AuthorizationUrlOptions{
         Scopes: []string{"openid", "profile", "email", "offline_access"},
         State: "your_generated_state", // Add this line
     }


     authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options)


     // Redirect user to Scalekit's hosted login page
     c.Redirect(http.StatusFound, authorizationUrl.String())
     ```

   * Java

     Spring

     ```java
     4 collapsed lines
     import com.scalekit.ScalekitClient;
     import com.scalekit.internal.http.AuthorizationUrlOptions;


     ScalekitClient scalekit = new ScalekitClient(/* your credentials */);


     // Basic authorization URL for general login
     String redirectUri = "https://yourapp.com/auth/callback";
     AuthorizationUrlOptions options = new AuthorizationUrlOptions();
     options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access"));
     options.setState("your_generated_state"); // Add this line


     URL authorizationUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options);


     // Redirect user to Scalekit's hosted login page
     return new RedirectView(authorizationUrl.toString());
     ```

   Scalekit will try to verify the user’s identity and redirect them to your application’s callback URL. If the user is a new user, Scalekit will automatically create a new user account.

## Dedicated sign-up flow

[Section titled “Dedicated sign-up flow”](#dedicated-sign-up-flow)

To keep sign-up separate from login, use the `prompt: 'create'` parameter to send users straight to the dedicated sign-up page.

* Node.js

  Express.js

  ```diff
  const redirectUri = 'http://localhost:3000/api/callback';
  const options = {
    scopes: ['openid', 'profile', 'email', 'offline_access'],
    prompt: 'create', // explicitly takes you to sign up flow
  };
  4 collapsed lines


  const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options);


  res.redirect(authorizationUrl);
  ```

* Python

  Flask

  ```diff
  from scalekit import AuthorizationUrlOptions


  redirect_uri = 'http://localhost:3000/api/callback'
  options = AuthorizationUrlOptions()
  options.scopes=['openid', 'profile', 'email', 'offline_access']
  options.prompt='create'  # optional: explicitly takes you to sign up flow


  4 collapsed lines
  authorization_url = scalekit.get_authorization_url(redirect_uri, options)


  # For web frameworks like Flask/Django:
  # return redirect(authorization_url)
  ```

* Go

  Gin

  ```diff
  redirectUri := "http://localhost:3000/api/callback"
  options := scalekit.AuthorizationUrlOptions{
      Scopes: []string{"openid", "profile", "email", "offline_access"},
      +Prompt: "create", // explicitly takes you to sign up flow
  }


  8 collapsed lines
  authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  if err != nil {
      // handle error appropriately
      panic(err)
  }


  // For web frameworks like Gin:
  // c.Redirect(http.StatusFound, authorizationUrl.String())
  ```

* Java

  Spring

  ```diff
  4 collapsed lines
  import com.scalekit.internal.http.AuthorizationUrlOptions;
  import java.net.URL;
  import java.util.Arrays;


  String redirectUri = "http://localhost:3000/api/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access"));
  +options.setPrompt("create");


  URL authorizationUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options);
  ```

After the user authenticates either in signup or login flows:

1. Scalekit generates an authorization code
2. Makes a callback to your registered allowed callback URL
3. Your backend exchanges the code for tokens by making a server-to-server request

This approach keeps sensitive operations server-side and protects your application’s credentials.

Let’s take a look at how to complete the login in the next step.

---
# DOCUMENT BOUNDARY
---

# Production readiness checklist

> A focused checklist for delivering a production-ready authentication system that's secure, reliable, and compliant

Before launching your authentication system to production, you need to ensure that every aspect of your implementation is secure, tested, and ready for real users. This checklist is organized in the order teams typically implement features when going live, starting with defining your requirements and moving through core flows to advanced features.

Use this checklist systematically to verify that your authentication implementation meets production standards. Each section addresses critical aspects of a production-ready authentication system, from security hardening to user experience testing.

## Define your auth surface

[Section titled “Define your auth surface”](#define-your-auth-surface)

Determine which authentication methods and features you need at launch. This prevents enabling features you don’t need and helps focus your testing efforts.

* \[ ] Decide which login methods to enable (email/password, magic links, social logins, passkeys)
* \[ ] Test all enabled authentication methods from initiation to completion
* \[ ] Verify social login integrations with your configured providers (Google, Microsoft, GitHub, etc.)
* \[ ] Test passkey authentication flows (if enabled)
* \[ ] Verify auth method selection UI works correctly
* \[ ] Test fallback scenarios when auth methods fail
* \[ ] Determine if you’re supporting enterprise customers at launch (SSO, SCIM, admin portal)
* \[ ] Configure proper CORS settings (restrict allowed origins to your domains)

## Core authentication flows

[Section titled “Core authentication flows”](#core-authentication-flows)

Verify that your core authentication flows work correctly and handle errors gracefully. These are the essential flows every application needs.

* \[ ] Verify production environment configuration (environment URL, client ID, and client secret match your production environment, not dev or staging)
* \[ ] Enable HTTPS for all authentication endpoints (prevents token interception)
* \[ ] Test login initiation with authorization URL
* \[ ] Validate redirect URLs match your dashboard configuration exactly
* \[ ] Test authentication completion and code exchange
* \[ ] Validate `state` parameter in callbacks to prevent CSRF attacks
* \[ ] Verify session token storage with `httpOnly`, `secure`, and `sameSite` flags as required
* \[ ] Configure token lifetimes appropriate for your security requirements
* \[ ] Test session timeout and automatic token refresh
* \[ ] Verify logout functionality clears sessions completely
* \[ ] Test error handling for expired tokens, invalid codes, and network failures
* \[ ] Test the complete flow end-to-end in a staging environment

## Network and firewall configuration

[Section titled “Network and firewall configuration”](#network-and-firewall-configuration)

If you’re enabling enterprise SSO or SCIM provisioning for your customers, verify network access early to avoid deployment blockers.

* \[ ] Verify customer firewalls allow Scalekit domains
* \[ ] Test authentication from customer’s network environment
* \[ ] Confirm no proxy servers block Scalekit endpoints

**Domains to whitelist for customer VPNs and firewalls**

If your customers deploy Scalekit behind a corporate firewall or VPN, they need to whitelist these Scalekit domains:

| Domain                            | Purpose                                                                                                                 |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `.scalekit.com` | Your Scalekit environment URL (admin portal and authentication; replace this with your actual Scalekit environment URL) |
| `cdn.scalekit.com`                | Content delivery network for static assets                                                                              |
| `docs.scalekit.com`               | Documentation portal                                                                                                    |
| `fonts.googleapis.com`            | Font resources                                                                                                          |

Replace `.scalekit.com` with your actual Scalekit environment URL from the Scalekit dashboard.

## Enterprise auth

[Section titled “Enterprise auth”](#enterprise-auth)

If you’re supporting enterprise customers, configure SSO, SCIM provisioning, and the admin portal.

### SSO flows

[Section titled “SSO flows”](#sso-flows)

* \[ ] Test SSO integrations with your target identity providers (Okta, Azure AD, Google Workspace)
* \[ ] Configure SSO user attribute mapping (email, name, groups)
* \[ ] Set up admin portal for enterprise customers to configure their SSO
* \[ ] Test both SP-initiated and IdP-initiated SSO flows
* \[ ] Verify SSO error handling for misconfigured connections
* \[ ] Test SSO with different user scenarios (new users, existing users, deactivated users)
* \[ ] Register all organization domains for [JIT provisioning](/authenticate/manage-users-orgs/jit-provisioning/) (enables automatic user creation)
* \[ ] Configure consistent user identifiers across all SSO connections (email, userPrincipalName, etc.)
* \[ ] Set appropriate default roles for JIT-provisioned users based on your security requirements
* \[ ] Enable “Sync user attributes during login” to keep user profiles updated from the identity provider
* \[ ] Monitor JIT activity and regularly review automatically provisioned users for security
* \[ ] Plan for manual invitations for contractors and external users with non-matching domains

### SCIM provisioning

[Section titled “SCIM provisioning”](#scim-provisioning)

* \[ ] Configure webhook endpoints to receive SCIM events
* \[ ] Verify webhook security with signature validation
* \[ ] Test user provisioning flow (create users automatically)
* \[ ] Test user deprovisioning flow (deactivate/delete users automatically)
* \[ ] Test user updates (profile changes, role updates)
* \[ ] Set up group-based role assignment and synchronization
* \[ ] Test error scenarios (duplicate users, invalid data)

### Admin portal

[Section titled “Admin portal”](#admin-portal)

* \[ ] Configure admin portal access for enterprise customers
* \[ ] Test admin portal SSO configuration flows
* \[ ] Verify admin portal user management features

## Customization

[Section titled “Customization”](#customization)

Ensure your authentication experience matches your brand identity and custom requirements.

* \[ ] Brand your login page with your logo, colors, and styling
* \[ ] Customize email templates for sign-up, password reset, and invitations
* \[ ] Configure custom domain for authentication pages (if applicable)
* \[ ] Set up your preferred email provider in **Dashboard > Customization > Emails**
* \[ ] Test email deliverability and check spam folders
* \[ ] Configure custom user attributes (if needed)
* \[ ] Set up auth flow interceptors (if using)
* \[ ] Configure webhooks for auth events (if using)
* \[ ] Verify webhook security with signature validation
* \[ ] Review and rotate API credentials (store in environment variables, never commit to code)

## User and organization management

[Section titled “User and organization management”](#user-and-organization-management)

Configure how users and organizations are managed in your application.

* \[ ] Configure user profile fields you need to collect during sign-up
* \[ ] Set up organization management (workspaces, teams, tenants)
* \[ ] Test organization creation flow
* \[ ] Test adding users to organizations
* \[ ] Test removing users from organizations
* \[ ] Test user invitation flow and email templates
* \[ ] Set allowed email domains for organization sign-ups (if applicable)
* \[ ] Verify organization switching works for users in multiple organizations
* \[ ] Test user and organization deletion flows
* \[ ] Review [user management settings](/authenticate/fsa/user-management-settings) in your dashboard

If you’re implementing role-based access control (RBAC), verify these authorization items:

* \[ ] Define and create roles and permissions
* \[ ] Configure default roles for new users
* \[ ] Test role assignment to users
* \[ ] Test role assignment to organization members
* \[ ] Verify permission checks in application code
* \[ ] Test access control for different role levels
* \[ ] Validate permission enforcement at API endpoints

## MCP authentication

[Section titled “MCP authentication”](#mcp-authentication)

If you’re implementing MCP authentication for AI agents, verify these items.

* \[ ] Test MCP server authentication flow
* \[ ] Verify OAuth consent screen for MCP clients
* \[ ] Test token exchange for MCP connections
* \[ ] Verify custom auth handlers (if using)
* \[ ] Test MCP session management
* \[ ] Review [MCP troubleshooting](/authenticate/mcp/troubleshooting/) documentation

## Monitoring, logs, and incident readiness

[Section titled “Monitoring, logs, and incident readiness”](#monitoring-logs-and-incident-readiness)

Set up monitoring to track authentication activity and troubleshoot issues quickly.

* \[ ] Set up authentication logs monitoring in **Dashboard > Auth Logs**
* \[ ] Configure alerts for suspicious activity (multiple failed login attempts, unusual locations)
* \[ ] Set up webhook event monitoring and logging
* \[ ] Create dashboards for key metrics (sign-ups, logins, failures, session durations)
* \[ ] Set up error tracking for authentication failures
* \[ ] Configure log retention policies
* \[ ] Test webhook delivery and retry mechanisms
* \[ ] Review [auth logs](/guides/dashboard/auth-logs) documentation
* \[ ] Configure [webhook best practices](/guides/webhooks-best-practices) for reliable event handling

---
# DOCUMENT BOUNDARY
---

# Listen for account events

> Get a webhook when an AgentKit connected account changes. Verify the signature in Python or Node.js, and ask users to reconnect when an account expires.

Scalekit sends a webhook to your server when a connected account changes: when a user finishes authorizing, when a token can’t be refreshed, or when an account expires or is disconnected. Listen for these events to ask users to reconnect before their next tool call fails, instead of polling every account’s status.

## Before you start

[Section titled “Before you start”](#before-you-start)

* A server with a public HTTPS URL that can receive `POST` requests. While you develop, a tunnel to your local server works.
* The Scalekit SDK installed, as in the [Quickstart](/agentkit/quickstart/). It verifies webhook signatures.
* The **Admin** or **Developer** role, to create an endpoint and read its signing secret. See [Team members and roles](/agentkit/team-members/).

## Events

[Section titled “Events”](#events)

| Event                                       | Sent when                                                               |
| ------------------------------------------- | ----------------------------------------------------------------------- |
| `connected_account.created`                 | A connected account is created                                          |
| `connected_account.updated`                 | A connected account’s details are updated                               |
| `connected_account.deleted`                 | A connected account is deleted                                          |
| `connected_account.magic_link_generated`    | An authorization link is created for the account                        |
| `connected_account.oauth_tokens_fetched`    | The app returned tokens after the user approved access                  |
| `connected_account.oauth_succeeded`         | The user finished authorizing, including user verification when it’s on |
| `connected_account.token_refresh_succeeded` | Scalekit refreshed the account’s access token                           |
| `connected_account.token_refresh_failed`    | Scalekit couldn’t refresh the access token                              |
| `connected_account.status_updated`          | The account’s status changed, for example from `ACTIVE` to `EXPIRED`    |

To know when a user needs to act, `connected_account.status_updated` is the one to handle. It’s sent only when the status actually changes, not when an account is created, and it carries the old and new status.

`connected_account.token_refresh_failed` arrives before the status changes, so its `status` often still reads `ACTIVE`, and it doesn’t say why the refresh failed. Use it as an early warning, and wait for `status_updated` before you ask the user to reconnect.

## Payloads

[Section titled “Payloads”](#payloads)

Every event has the same envelope. The account’s details are in `data`:

connected_account.status_updated

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_101652975398683158",
4
  "type": "connected_account.status_updated",
5
  "occurred_at": "2026-09-26T06:31:34.895815554Z",
6
  "environment_id": "env_88640229614813449",
7
  "object": "ConnectedAccount",
8
  "data": {
9
    "id": "ca_133400349586228019",
10
    "identifier": "user_123",
11
    "connection_id": "conn_133400101014995480",
12
    "connection_name": "github-connect",
13
    "provider": "GITHUB",
14
    "authorization_type": "OAUTH",
15
    "status": "EXPIRED",
16
    "old_status": "ACTIVE"
17
  }
18
}
```

| Field                  | Meaning                                                                                                                                                           |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                   | The event’s ID. The same event delivered twice has the same ID                                                                                                    |
| `type`                 | The event name from the table above                                                                                                                               |
| `occurred_at`          | When the change happened                                                                                                                                          |
| `data.id`              | The connected account’s ID                                                                                                                                        |
| `data.identifier`      | Your app’s ID for the user                                                                                                                                        |
| `data.connection_name` | The connection, as named in **AgentKit** > **Connections**. It can be missing from `status_updated`, so read it as optional and fall back to `data.connection_id` |
| `data.status`          | The status after the change: `ACTIVE`, `PENDING_AUTH`, `PENDING_VERIFICATION`, `EXPIRED` or `DISCONNECTED`                                                        |
| `data.old_status`      | The status before the change. Only in `status_updated`                                                                                                            |

Events other than `status_updated` have no `old_status`. They also include `token_expires_at` and `last_used_at`, which are empty strings when Scalekit has no value. In webhooks, `status` is a string, such as `ACTIVE`, in both Python and Node.js.

For every event’s full schema, see [Connected account events](/agentkit/reference/events/) in the REST reference.

1. ## Add a webhook endpoint

   [Section titled “Add a webhook endpoint”](#add-a-webhook-endpoint)

   In the Scalekit dashboard, select the environment, then open **Developers** > **Webhooks** and select **Add Webhook**. Enter a **Display Name** and your **Endpoint URL**, choose the events under **Choose events to subscribe**, and save.

   Each environment has its own endpoints. Add one in Production too when you go live.

2. ## Copy the signing secret

   [Section titled “Copy the signing secret”](#copy-the-signing-secret)

   Open the endpoint and select **Show** under **Signing secret**. It starts with `whsec_`. Store it with your other server secrets, for example as `SCALEKIT_WEBHOOK_SECRET`, and never expose it to a browser.

3. ## Verify each request and handle the event

   [Section titled “Verify each request and handle the event”](#verify-each-request-and-handle-the-event)

   Scalekit signs every request with the secret. Verify the signature against the raw request body before you trust the payload, so a forged request can’t prompt the wrong user to reconnect. The SDK checks the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers, and rejects requests older than five minutes.

   * Python

     webhooks.py

     ```python
     1
     import json
     2
     import os
     3


     4
     from fastapi import FastAPI, HTTPException, Request
     5
     from scalekit import ScalekitClient
     6


     7
     scalekit_client = ScalekitClient(
     8
         env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
     9
         client_id=os.environ["SCALEKIT_CLIENT_ID"],
     10
         client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
     11
     )
     12
     app = FastAPI()
     13
     seen_event_ids = set()  # use your database in production
     14


     15
     @app.post("/webhooks/scalekit")
     16
     async def scalekit_webhook(request: Request):
     17
         body = await request.body()
     18
         headers = {k.lower(): v for k, v in request.headers.items()}
     19
         try:
     20
             scalekit_client.verify_webhook_payload(
     21
                 os.environ["SCALEKIT_WEBHOOK_SECRET"], headers, body
     22
             )
     23
         except Exception:
     24
             raise HTTPException(status_code=400, detail="invalid signature")
     25


     26
         event = json.loads(body)
     27
         if event["id"] in seen_event_ids:
     28
             return {"ok": True}
     29
         seen_event_ids.add(event["id"])
     30


     31
         data = event["data"]
     32
         if event["type"] == "connected_account.status_updated":
     33
             # connection_name can be missing; fall back to connection_id
     34
             connection = data.get("connection_name") or data.get("connection_id")
     35
             if data.get("status") in ("EXPIRED", "DISCONNECTED"):
     36
                 # Ask this user to reconnect, for example with a new authorization link
     37
                 print(f"{data.get('identifier')}: reconnect {connection}")
     38
             elif data.get("status") == "ACTIVE":
     39
                 print(f"{data.get('identifier')}: {connection} is connected")
     40
         return {"ok": True}
     ```

   * Node.js

     webhooks.ts

     ```ts
     1
     import express from 'express';
     2
     import { ScalekitClient } from '@scalekit-sdk/node';
     3


     4
     const scalekit = new ScalekitClient(
     5
       process.env.SCALEKIT_ENVIRONMENT_URL!,
     6
       process.env.SCALEKIT_CLIENT_ID!,
     7
       process.env.SCALEKIT_CLIENT_SECRET!,
     8
     );
     9
     const app = express();
     10
     const seenEventIds = new Set(); // use your database in production
     11


     12
     // Read the raw body: the signature covers the exact bytes Scalekit sent
     13
     app.post('/webhooks/scalekit', express.raw({ type: 'application/json' }), (req, res) => {
     14
       const body = req.body.toString('utf8');
     15
       try {
     16
         scalekit.verifyWebhookPayload(
     17
           process.env.SCALEKIT_WEBHOOK_SECRET!,
     18
           req.headers as Record,
     19
           body,
     20
         );
     21
       } catch {
     22
         return res.status(400).send('invalid signature');
     23
       }
     24


     25
       const event = JSON.parse(body);
     26
       if (seenEventIds.has(event.id)) return res.json({ ok: true });
     27
       seenEventIds.add(event.id);
     28


     29
       const { data } = event;
     30
       if (event.type === 'connected_account.status_updated') {
     31
         // connection_name can be missing; fall back to connection_id
     32
         const connection = data?.connection_name ?? data?.connection_id;
     33
         if (data?.status === 'EXPIRED' || data?.status === 'DISCONNECTED') {
     34
           // Ask this user to reconnect, for example with a new authorization link
     35
           console.log(`${data?.identifier}: reconnect ${connection}`);
     36
         } else if (data?.status === 'ACTIVE') {
     37
           console.log(`${data?.identifier}: ${connection} is connected`);
     38
         }
     39
       }
     40
       res.json({ ok: true });
     41
     });
     42


     43
     app.listen(3000);
     ```

   To ask the user to reconnect, create an authorization link for `data.identifier` and `data.connection_name`, as shown in [Authorize a user](/agentkit/tools/authorize/).

## Respond quickly and handle repeats

[Section titled “Respond quickly and handle repeats”](#respond-quickly-and-handle-repeats)

* **Return a `2xx` status fast.** Do slow work, such as sending an email, after you respond or in a background job. Scalekit retries deliveries that fail or time out.
* **Verify before you queue.** Verify the signature in the handler that receives the request, then queue the verified event. Don’t verify in a queue worker: the SDK rejects requests more than 5 minutes old, and a queued event can wait longer than that.
* **Expect the same event more than once.** Retries and resends repeat an event with the same `id`. Store the IDs you’ve handled, and skip a repeat.
* **Don’t rely on order.** Events can arrive out of order. Compare `occurred_at`, or read the account’s current status (`get_connected_account_details` in Python, `getConnectedAccount` in Node.js) before you act on an old event.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

In your Development environment, create an authorization link for a test user and complete it. Your endpoint receives events including `connected_account.magic_link_generated`, `connected_account.oauth_succeeded` and a `connected_account.status_updated` whose `status` is `ACTIVE`.

The endpoint’s page in **Developers** > **Webhooks** lists every delivery and its result. Resend a delivery from there to test your handler again.

## Common problems

[Section titled “Common problems”](#common-problems)

### The SDK raises `Missing required headers`

[Section titled “The SDK raises Missing required headers”](#the-sdk-raises-missing-required-headers)

The `webhook-id`, `webhook-timestamp` and `webhook-signature` headers didn’t reach the SDK. In Python, pass the headers with lowercase keys. Check that a proxy in front of your server doesn’t strip them.

### The SDK raises `Invalid signature` or `Invalid Signature`

[Section titled “The SDK raises Invalid signature or Invalid Signature”](#the-sdk-raises-invalid-signature-or-invalid-signature)

Python raises `Invalid signature`, and Node.js raises `Invalid Signature`. The body you verified isn’t byte-for-byte what Scalekit sent, or the secret is from another endpoint or environment. Verify the raw body before any JSON parsing, and copy the secret again from the endpoint’s page.

### The SDK raises `Message timestamp too old`

[Section titled “The SDK raises Message timestamp too old”](#the-sdk-raises-message-timestamp-too-old)

The request is more than five minutes old, or your server’s clock is off. Sync the clock, and don’t queue requests before you verify them.

### My endpoint receives nothing

[Section titled “My endpoint receives nothing”](#my-endpoint-receives-nothing)

Check that the endpoint is in the environment where the change happened, that it subscribes to the event, and that its URL is reachable from the internet. The delivery list on the endpoint’s page shows failed attempts and their responses.

## Next

[Section titled “Next”](#next)

[Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and why accounts expire.

[Authorize a user](/agentkit/tools/authorize/)Send the user a new authorization link to reconnect.

---
# DOCUMENT BOUNDARY
---

# Brand the Hosted Connect Widget

> Make the Hosted Connect Widget your users see when they connect an account look like your product: name, logo, colors, font, text, domain and consent screen.

When a user opens an [authorization link](/agentkit/tools/authorize/), they land on the [Hosted Connect Widget](/agentkit/hosted-connect-widget/), then go to the app’s consent screen to grant access. This page covers everything you can change so that flow looks like your product, and where to change it. For what users see and do in the widget, see [Hosted Connect Widget](/agentkit/hosted-connect-widget/).

## What your users see

[Section titled “What your users see”](#what-your-users-see)

The widget shows:

* Your logo next to the app’s icon, and a headline that names your application: “Acme wants to connect to your Gmail”.
* A short overview and a description of the app the user is connecting.
* A **Connect** button, and links to your terms of service and privacy policy.
* A “Powered by Scalekit” badge, which some plans let you remove.

After the user clicks **Connect**, the app’s own consent screen shows the name of the OAuth app that asks for access. The page’s address is your environment’s domain, or your custom domain if you set one up.

| What you change                                                        | Where                                           | Applies to                          |
| ---------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------- |
| Name, logo, favicon, font, colors, corners, background and legal links | **Customize** > **Branding**                    | Every connection in the environment |
| App name, overview and description in the widget                       | **Connections** > your connection > **Widgets** | One connection                      |
| “Powered by Scalekit” badge                                            | **Customize** > **Branding**                    | Every connection in the environment |
| The domain in authorization links                                      | **Customize** > **Custom Domain**               | The environment                     |
| The app name on the consent screen                                     | Your own OAuth app, set on the connection       | One connection                      |

## Brand the widget

[Section titled “Brand the widget”](#brand-the-widget)

In the AgentKit dashboard, go to **Customize** > **Branding**. The preview on the right updates as you change a setting. Save to apply your changes to the widget.

| Setting                                                         | What it changes                                | Accepts                                                                                                                                                  |
| --------------------------------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Application Name**                                            | Your name in the headline                      | Text. Without a name, the headline reads “Connect to your Gmail”                                                                                         |
| **Logo**                                                        | The logo next to the app’s icon                | Upload a PNG or JPG, or enter a URL for any format, including SVG. At least 36 px high                                                                   |
| **Favicon**                                                     | The browser tab icon                           | Upload a PNG or JPG, or enter a URL for any format. Square images work best                                                                              |
| **Font**                                                        | The font on the page                           | Upload a WOFF2, WOFF or TTF file of up to 2 MB, or enter an `https://` URL that points directly at one. If the font can’t load, the default font is used |
| **Primary Button Background** and **Primary Button Text Color** | The **Connect** button                         | Colors                                                                                                                                                   |
| **Border radius**                                               | How rounded buttons and boxes are              | **None**, **Small**, **Medium** or **Large**                                                                                                             |
| **Border color**                                                | The border of the connect card                 | A color                                                                                                                                                  |
| **Background Color**                                            | The page background                            | A color                                                                                                                                                  |
| **Background Style**                                            | The page background, with an image or gradient | A CSS `background` shorthand value                                                                                                                       |
| **Terms of Service URL** and **Privacy Policy URL**             | The legal links under the button               | URLs. The page shows only the links you set                                                                                                              |

Branding belongs to the environment, so set it in Development and in Production. If you also use SaaSKit in the same environment, its **Branding** page edits the same settings: the name, logo, colors and font you set here also apply to your login pages.

## Describe each connection

[Section titled “Describe each connection”](#describe-each-connection)

The widget describes the app with the connector’s default name and description. To write your own, for example to say what your agent does with the user’s account:

1. Go to **AgentKit** > **Connections** and open the connection.
2. Open the **Widgets** tab.
3. Set the **App name** (shown in the headline in place of the connector’s name), a **Short Overview** and a **Description**, then save.

## Remove “Powered by Scalekit”

[Section titled “Remove “Powered by Scalekit””](#remove-powered-by-scalekit)

Whether you can remove the badge depends on your plan; see [pricing](https://www.scalekit.com/pricing). If your plan includes it, turn on **Remove “Powered by Scalekit”** in **Customize** > **Branding**. You can remove it in Production environments only.

## Use your own domain

[Section titled “Use your own domain”](#use-your-own-domain)

Authorization links use your environment’s domain, such as `yourapp.scalekit.com`. To use your own, such as `connect.yourapp.com`, [set up a custom domain](/agentkit/advanced/custom-domain/). Custom domains work in Production environments, and availability depends on your plan. Authorization links you create after the domain is verified use it.

## Show your app on the consent screen

[Section titled “Show your app on the consent screen”](#show-your-app-on-the-consent-screen)

By default, OAuth connectors that offer Scalekit’s credentials ask for consent in Scalekit’s name. To show your app’s name, logo and terms on the consent screen instead, [use your own OAuth app](/agentkit/advanced/bring-your-own-oauth/) for the connection.

Provider consent screen

The consent screen belongs to the app the user connects, such as Google or Slack. You change what it shows in that app’s developer console, not in Scalekit.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

Create an authorization link for a test user and open it in a private browser window. The widget shows your logo, name and colors, and the address bar shows your custom domain if you set one up.

## Next

[Section titled “Next”](#next)

[Set up a custom domain](/agentkit/advanced/custom-domain/)Serve authorization links from your own domain.

[Use your own OAuth app](/agentkit/advanced/bring-your-own-oauth/)Show your app's name on the provider's consent screen.

---
# DOCUMENT BOUNDARY
---

# Use your own OAuth app

> Use your own OAuth app for an AgentKit connection, so users see your app's name on the consent screen and your app gets its own quota at the provider.

Many OAuth connectors come with Scalekit’s own OAuth app credentials, so you can create a connection and test it without registering an app with the provider. The connection form offers **Use Scalekit credentials** when a connector supports it. Connectors without Scalekit credentials need your own OAuth app from the start, as described in [Configure connections](/agentkit/connections/#set-up-an-oauth-connection).

You can go live on Scalekit’s credentials. Your users then see Scalekit’s name and branding on the consent screen, not yours.

**Bring your own credentials** lets you replace Scalekit’s shared OAuth credentials with your own. Once configured, users see your app name, logo, and terms on every OAuth consent screen.

## What changes when you use your own credentials

[Section titled “What changes when you use your own credentials”](#what-changes-when-you-use-your-own-credentials)

* **Consent screens** display your application’s name and branding
* **Rate limits and quotas** are tied to your OAuth app, not Scalekit’s shared pool
* **Provider relationship** is direct, and your OAuth app appears in provider dashboards and audit logs
* **Compliance**: useful if your organization requires a direct relationship with each OAuth provider

Nothing changes in your code or the Scalekit SDK. The switch is purely a dashboard configuration on the connection.

## Configure your credentials

[Section titled “Configure your credentials”](#configure-your-credentials)

1. ### Copy the redirect URI from Scalekit

   [Section titled “Copy the redirect URI from Scalekit”](#copy-the-redirect-uri-from-scalekit)

   Go to **AgentKit** > **Connections** and click **Edit** on the connection you want to update. Select **Use your own credentials**. The form expands and displays a **Redirect URI**. Copy it.

2. ### Register your OAuth app with the provider

   [Section titled “Register your OAuth app with the provider”](#register-your-oauth-app-with-the-provider)

   In the provider’s developer console, create a new OAuth app (or use an existing one). Add the Redirect URI you copied in the previous step to the list of authorized redirect URIs.

   Redirect URI must match exactly

   The URI must match character-for-character. A mismatch will cause OAuth flows to fail with a redirect_uri_mismatch error.

   The provider gives you a **Client ID** and **Client Secret** after registration.

   Many provider consoles create the app in an internal, development or testing mode by default. In that mode only some accounts can authorize it, so it is not enough for customer consent. For example, a Google app in the **Testing** publishing status works only for the accounts listed as test users on its OAuth consent screen. Add each account you test with, and allow a few minutes for a new test user to take effect.

3. ### Enter your credentials and save

   [Section titled “Enter your credentials and save”](#enter-your-credentials-and-save)

   Back in Scalekit Dashboard, enter the **Client ID** and **Client Secret** from your OAuth app and click **Save**.

   All new OAuth flows for this connection will now use your credentials.

   Saving credentials only wires your app into Scalekit. Before customers can connect, promote the app to allow external accounts and validate it end-to-end — see the [AgentKit launch checklist](/agentkit/advanced/launch-checklist/).

## Existing connected accounts

[Section titled “Existing connected accounts”](#existing-connected-accounts)

Existing connected accounts are not affected immediately

Switching credentials does not re-authorize users who are already active. They continue using the previous credentials until they re-authorize. If you need all users to see your branding immediately, generate new authorization links and prompt them to re-authorize.

## Next

[Section titled “Next”](#next)

[Set up a custom domain](/agentkit/advanced/custom-domain/)Serve authorization pages from your own domain.

[AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect.

---
# DOCUMENT BOUNDARY
---

# Set up a custom domain

> Serve your AgentKit Production environment from your own domain, such as auth.yourapp.com: add a CNAME record, verify it, and Scalekit provisions SSL.

Custom domains enable you to offer a fully branded experience. By default, Scalekit assigns a unique endpoint URL, but you can replace it via CNAME configuration. The custom domain also applies to the authorization server URL shown on the OAuth consent screen during MCP authentication; users will see your branded domain instead of the auto-generated `yourapp.scalekit.com`.

| Before                         | After                      |
| ------------------------------ | -------------------------- |
| `https://yourapp.scalekit.com` | `https://auth.yourapp.com` |

* **Environment:** CNAME configuration is available only for production environments. Availability also depends on your plan; see [pricing](https://www.scalekit.com/pricing).
* **SSL:** After successful CNAME configuration, an SSL certificate for your custom domain is automatically provisioned

## Set up your custom domain

[Section titled “Set up your custom domain”](#set-up-your-custom-domain)

![Scalekit Settings Custom Domain tab showing the subdomain URL field and a CNAME record in the DNS configuration table](/.netlify/images?url=_astro%2F1.BktW9U-H.png\&w=2786\&h=1746\&dpl=6ae471fc143c9a4c27550463)

To set up your custom domain:

1. Go to your domain’s DNS registrar
2. Add a new record to your DNS settings and select **CNAME** as the record type
3. Switch to production environment in the Scalekit dashboard
4. Copy the **Name** (your desired subdomain) from **Customize** > **Custom Domain** in the Scalekit dashboard and paste it into the **Name/Label/Host** field in your DNS registrar
5. Copy the **Value** from **Customize** > **Custom Domain** and paste it into the **Destination/Target/Value** field in your DNS registrar
6. Save the record in your DNS registrar
7. In the Scalekit dashboard, click **Verify**

CNAME record changes can take up to 72 hours to propagate, although they typically happen much sooner.

## Troubleshoot CNAME verification

[Section titled “Troubleshoot CNAME verification”](#troubleshoot-cname-verification)

If there are any issues during the CNAME verification step:

* Double-check your DNS configuration to ensure all values are correctly entered
* Once the CNAME changes take effect, Scalekit will automatically provision an SSL certificate for your custom domain. This process can take up to 24 hours

You can click on the **Check** button in the Scalekit dashboard to verify SSL certification status. If SSL provisioning takes longer than 24 hours, please contact us at [](mailto:support@scalekit.com)

## DNS registrar guides

[Section titled “DNS registrar guides”](#dns-registrar-guides)

For detailed instructions on adding a CNAME record in specific registrars:

* [GoDaddy: Add a CNAME record](https://www.godaddy.com/help/add-a-cname-record-19236)
* [Namecheap: How to create a CNAME record](https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain)

## Next

[Section titled “Next”](#next)

[Brand the connect experience](/agentkit/advanced/branding/)Add your name, logo and colors to the page users connect from.

[AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect.

---
# DOCUMENT BOUNDARY
---

# AgentKit launch checklist

> Check your AgentKit integration before real users connect: Production credentials and connections, your own OAuth apps, user verification, events and security.

Work through this list in your Production environment before real users connect their accounts. Each item says how to confirm it’s done and links to the steps.

Development and Production share nothing, so most of the work is setting up again in Production what you built in Development.

## Set up Production

[Section titled “Set up Production”](#set-up-production)

* \[ ] **Create a Production environment.** The environment switcher at the top left lists it. See [Create a Production environment](/agentkit/environments/#create-a-production-environment).
* \[ ] **Deploy Production credentials.** Your production servers use the Production `SCALEKIT_ENVIRONMENT_URL`, `SCALEKIT_CLIENT_ID` and `SCALEKIT_CLIENT_SECRET`, and no Development values remain. The secret’s **Last used** time updates when your server calls Scalekit. See [API credentials](/agentkit/api-credentials/).
* \[ ] **Keep the client secret on your server.** It isn’t in browser or mobile code, logs or your repository.

## Recreate your connections

[Section titled “Recreate your connections”](#recreate-your-connections)

* \[ ] **Create every connection your agent uses in Production.** Each one appears in **AgentKit** > **Connections** in Production. See [Set up a connection](/agentkit/connections/).
* \[ ] **Match connection names in your code.** The `connection_name` values your code sends match the Production connection names exactly.
* \[ ] **Create your custom connectors and Virtual MCP servers again, if you use them.** They belong to one environment, so create them with your Production credentials. See [Create a connector](/agentkit/bring-your-own-connector/create-connector/) and [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/).

## Publish your own OAuth apps

[Section titled “Publish your own OAuth apps”](#publish-your-own-oauth-apps)

Skip this section for connections that use Scalekit’s credentials.

A provider creates a new OAuth app in a development or testing mode that only authorizes the account that created it. The connection works while you test with your own account, then fails for your customers. For example, Airtable returns “This OAuth application cannot be used outside of development”.

* \[ ] **Add the Production redirect URI to each app.** Copy it from the connection in Production. It differs from Development’s, and it must match exactly. See [Use your own OAuth app](/agentkit/advanced/bring-your-own-oauth/).
* \[ ] **Publish each app** out of development or testing mode, and complete the profile the provider requires first, such as a logo, terms of service and privacy policy.
* \[ ] **Recheck the credentials after publishing.** Publishing can change the client ID. The client ID, client secret and scopes in Scalekit match the provider’s.
* \[ ] **Authorize with an account outside your own organization.** It connects without an “unverified app” or “needs admin approval” screen. See [Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/).

## Connect users safely

[Section titled “Connect users safely”](#connect-users-safely)

* \[ ] **Use a stable, internal ID as each user’s `identifier`.** It comes from your authenticated session, never from the request body, and isn’t an email address or shared across users. See [Choose an identifier](/agentkit/concepts/#choose-an-identifier).
* \[ ] **Turn on user verification.** In Production, **AgentKit** > **Settings** > **User Verification** is set to **Custom user verifier**, your authorization links pass `user_verify_url`, and your verify route confirms the user. See [Verify users](/agentkit/user-verification/).
* \[ ] **Send authorization links from your server.** A test user can open one, authorize, and reach `ACTIVE`. See [Authorize a user](/agentkit/tools/authorize/).

## Handle accounts that stop working

[Section titled “Handle accounts that stop working”](#handle-accounts-that-stop-working)

* \[ ] **Check status before tool calls, and send a new link when it isn’t `ACTIVE`.** Revoke a test user’s access at the provider, and your app asks them to reconnect. See [Manage connected accounts](/agentkit/connected-accounts/).
* \[ ] **Listen for account events.** A webhook endpoint in Production receives `connected_account.status_updated`, and your handler verifies the signature. See [Listen for account events](/agentkit/account-events/).
* \[ ] **Delete connected accounts when your users delete their accounts.** Your account-deletion flow deletes each of the user’s connected accounts, so Scalekit no longer holds their tokens. See [Delete a connected account](/agentkit/connected-accounts/#delete-a-connected-account).

## Run agents safely

[Section titled “Run agents safely”](#run-agents-safely)

* \[ ] **If you use Virtual MCP, mint a session token for each run, on your server.** Tokens aren’t stored between runs or sent to a browser. See [Mint session tokens](/agentkit/mcp/session-tokens/).
* \[ ] **Check `mcp_server_url` before you hand a Virtual MCP server to a client.** The value you store for each server isn’t empty. See [Check it worked](/agentkit/mcp/configure-mcp-server/#check-it-worked).
* \[ ] **Handle tool call errors and rate limits.** Your agent reads the error a failed call returns, and retries with backoff when it gets a `429`. See [Errors and rate limits](/agentkit/reference/errors/).
* \[ ] **Monitor tool calls in AgentKit > Logs.** Someone on your team checks **AgentKit** > **Logs** for failed calls after launch. See [What Scalekit stores](/agentkit/security/#what-scalekit-stores).
* \[ ] **Leave Store connector error details off** in **AgentKit** > **Logs**, unless you’re debugging a connector. With it on, logs keep error responses that can include data from the app. See [What Scalekit stores](/agentkit/security/#what-scalekit-stores).
* \[ ] **Review what your app is responsible for.** See [Security and compliance](/agentkit/security/).

## Prepare your workspace

[Section titled “Prepare your workspace”](#prepare-your-workspace)

* \[ ] **Give each teammate the least access they need.** Only people who manage members and billing are **Admin**. See [Team members and roles](/agentkit/team-members/).
* \[ ] **Choose a plan for Production.** **Developers** > **Billing** shows its plan and your tool call usage. See [Billing](/agentkit/billing/).

## Optional

[Section titled “Optional”](#optional)

* \[ ] **Brand the Hosted Connect Widget** in Production with your name, logo and colors. See [Brand the Hosted Connect Widget](/agentkit/advanced/branding/).
* \[ ] **Serve Production from your own domain,** such as `auth.yourapp.com`, so users see it on authorization pages. See [Set up a custom domain](/agentkit/advanced/custom-domain/).
* \[ ] **Bring your own encryption key** if your security review requires it. See [Manage encryption keys](/agentkit/encryption-keys/).

## Next

[Section titled “Next”](#next)

[Security and compliance](/agentkit/security/)What Scalekit protects, and what your app is responsible for.

[Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/)Find the error your user saw and how to fix it.

---
# DOCUMENT BOUNDARY
---

# Migrate from Composio to Scalekit

> Move an agent from Composio to Scalekit AgentKit: map concepts, recreate connections, have users re-authorize, then move tool calls, MCP and custom tools.

This guide maps Composio concepts to their Scalekit AgentKit equivalents and walks through each migration step: SDK setup, authentication, tool execution, and MCP. Use it as a reference while porting your agent code.

Users must re-authorize

OAuth refresh tokens are bound to the OAuth client that issued them. After migration, each user must complete the OAuth consent flow once through Scalekit to create a new connected account. Existing Composio tokens cannot be transferred.

## Concept mapping

[Section titled “Concept mapping”](#concept-mapping)

| Composio                             | Scalekit                                      | Notes                                                                                                                                |
| ------------------------------------ | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| Toolkit (e.g. `GITHUB`)              | **Connector** (e.g. `github`)                 | Scalekit uses lowercase slugs                                                                                                        |
| Tool (e.g. `GITHUB_CREATE_ISSUE`)    | **Tool** (e.g. `github_issue_create`)         | Same concept, lowercase naming                                                                                                       |
| Auth config                          | **Connection**                                | OAuth app credentials, scopes, redirect URIs                                                                                         |
| Connected account                    | **Connected account**                         | Per-user credential record                                                                                                           |
| `user_id` / entity ID                | **`identifier`**                              | Your app’s unique user ID, passed per API call                                                                                       |
| Connect Link                         | **Authorization link**                        | OAuth redirect URL for user consent                                                                                                  |
| Session (`composio.create()`)        | **`identifier`** on each call                 | No session to create: each call names the user                                                                                       |
| Provider package (`composio_openai`) | `@scalekit-sdk/node` or `scalekit-sdk-python` | One SDK works with every framework                                                                                                   |
| `session.tools()`                    | `listScopedTools()`                           | Get tools a user is authorized to call                                                                                               |
| `session.tools.execute()`            | `executeTool()`                               | Execute a tool on behalf of a user                                                                                                   |
| `session.mcp.url`                    | **Virtual MCP server URL + session token**    | Static server URL with a short-lived bearer token per agent run                                                                      |
| Custom tool (in-memory)              | **Custom tool** (API Proxy)                   | Defined in your app code using `actions.request()`                                                                                   |
| `executeToolRequest` (proxy)         | `actions.request()`                           | Proxied REST API call                                                                                                                |
| Trigger                              | **Trigger** (beta)                            | Subscribe in the dashboard; events arrive at your endpoint as signed webhooks. See [How triggers work](/agentkit/triggers/overview/) |
| `COMPOSIO_SEARCH_TOOLS`              | `searchTools()`                               | Rank tools by relevance to a task, or filter `listScopedTools()` by connection name                                                  |
| `COMPOSIO_REMOTE_WORKBENCH`          | Your own runtime                              | Run code where your agent runs                                                                                                       |

## 1. Set up Scalekit

[Section titled “1. Set up Scalekit”](#1-set-up-scalekit)

1. **Create a Scalekit account**

   Sign up at [app.scalekit.com](https://app.scalekit.com) and copy your API credentials from **Dashboard > Developers > Settings > API Credentials**.

2. **Set environment variables**

   Add them to a `.env` file that you don’t commit. The code in step 4 loads it.

   .env

   ```bash
   SCALEKIT_CLIENT_ID=your_client_id
   SCALEKIT_CLIENT_SECRET=your_client_secret
   SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com
   ```

3. **Install the SDK**

   * Python

     ```bash
     pip install scalekit-sdk-python python-dotenv
     ```

   * Node.js

     ```bash
     npm install @scalekit-sdk/node dotenv
     ```

4. **Initialize the client**

   Scalekit uses a single client instance. There is no session object — you pass `identifier` on each API call.

   * Python

     ```python
     import os
     from scalekit import ScalekitClient
     from dotenv import load_dotenv


     load_dotenv()  # reads .env into the environment


     scalekit_client = ScalekitClient(
         client_id=os.getenv("SCALEKIT_CLIENT_ID"),
         client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
         env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
     )
     actions = scalekit_client.actions
     ```

   * Node.js

     ```typescript
     import 'dotenv/config'; // reads .env into process.env
     import { ScalekitClient } from '@scalekit-sdk/node';


     const scalekit = new ScalekitClient(
       process.env.SCALEKIT_ENVIRONMENT_URL!,
       process.env.SCALEKIT_CLIENT_ID!,
       process.env.SCALEKIT_CLIENT_SECRET!
     );
     ```

## 2. Configure connections

[Section titled “2. Configure connections”](#2-configure-connections)

In Composio, auth configs are created programmatically or via the dashboard. In Scalekit, you configure **connections** in the dashboard.

For each Composio toolkit your agent uses (Gmail, Slack, GitHub, etc.), create a corresponding connection in **AgentKit** > **Connections** > **Add connection**. See [Configure a connection](/agentkit/connections/) for the full walkthrough.

| Composio auth type           | Scalekit equivalent                                                |
| ---------------------------- | ------------------------------------------------------------------ |
| OAuth 2.0 (Composio managed) | OAuth 2.0 (use Scalekit credentials to start, then bring your own) |
| OAuth 2.0 (custom)           | OAuth 2.0 (bring your own credentials)                             |
| API key                      | API key (user provides during connected account creation)          |
| Bearer token                 | Bearer token                                                       |
| Basic auth                   | Basic auth                                                         |

Scalekit credentials for quick testing

Many OAuth connectors offer a **Use Scalekit credentials** option that lets you skip OAuth app registration, in development and in production. Switch to your own credentials when you want your app’s name on the consent screen. See [Bring your own OAuth](/agentkit/advanced/bring-your-own-oauth/).

## 3. Migrate authentication

[Section titled “3. Migrate authentication”](#3-migrate-authentication)

Both platforms create per-user records (connected accounts) and generate OAuth links. The SDK methods differ.

#### Create a connected account and authorize

[Section titled “Create a connected account and authorize”](#create-a-connected-account-and-authorize)

* Python

  **Before (Composio):**

  ```python
  # Composio handles auth in-chat or via connect link
  session = composio.create(user_id="user_123")
  # Auth is triggered automatically when a tool requires it
  ```

  **After (Scalekit):**

  ```python
  # Create or retrieve the connected account
  response = actions.get_or_create_connected_account(
      connection_name="gmail",
      identifier="user_123",
  )
  connected_account = response.connected_account


  # Generate an authorization link if the account is not yet active
  if connected_account.status != "ACTIVE":
      link_response = actions.get_authorization_link(
          connection_name="gmail",
          identifier="user_123",
      )
      auth_url = link_response.link
      # Redirect or send auth_url to the user
  ```

* Node.js

  **Before (Composio):**

  ```typescript
  // Composio handles auth in-chat or via connect link
  const session = await composio.create("user_123");
  // Auth is triggered automatically when a tool requires it
  ```

  **After (Scalekit):**

  ```typescript
  1
  import { ConnectorStatus } from '@scalekit-sdk/node';
  2


  3
  // Create or retrieve the connected account
  4
  const response = await scalekit.actions.getOrCreateConnectedAccount({
  5
    connectionName: 'gmail',
  6
    identifier: 'user_123',
  7
  });
  8


  9
  const connectedAccount = response.connectedAccount;
  10


  11
  // Generate an authorization link if the account is not yet active
  12
  if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
  13
    const linkResponse = await scalekit.actions.getAuthorizationLink({
  14
      connectionName: 'gmail',
  15
      identifier: 'user_123',
  16
    });
  17
    const authUrl = linkResponse.link;
  18
    // Redirect or send authUrl to the user
  19
  }
  ```

**Key difference:** Composio can trigger auth in-chat automatically. With Scalekit, your app explicitly creates the connected account and sends the authorization link to the user. Once the user completes the OAuth flow, the connected account becomes `ACTIVE` and your agent can execute tools.

#### Check connected account status

[Section titled “Check connected account status”](#check-connected-account-status)

Composio tracks two statuses (`ACTIVE` and `INACTIVE`). Scalekit uses more granular states:

| Scalekit status        | Meaning                                                         |
| ---------------------- | --------------------------------------------------------------- |
| `PENDING_AUTH`         | User hasn’t completed authentication                            |
| `PENDING_VERIFICATION` | Authentication complete; user verification still required       |
| `ACTIVE`               | Credentials valid, ready for tool calls                         |
| `EXPIRED`              | Credentials expired or were revoked; re-authentication required |
| `DISCONNECTED`         | Account was disconnected                                        |

Check status before executing tools. If the account is not `ACTIVE`, generate a new authorization link.

## 4. Migrate tool calls

[Section titled “4. Migrate tool calls”](#4-migrate-tool-calls)

#### List available tools

[Section titled “List available tools”](#list-available-tools)

* Python

  **Before (Composio):**

  ```python
  session = composio.create(user_id="user_123")
  tools = session.tools()  # all tools the user is authorized for
  ```

  **After (Scalekit):**

  ```python
  tools_response, _ = scalekit_client.actions.tools.list_scoped_tools(
      identifier="user_123",
      # Required: list every connection whose tools the agent should see
      filter={"connection_names": ["gmail"]},
      page_size=100,
  )
  ```

* Node.js

  **Before (Composio):**

  ```typescript
  const session = await composio.create("user_123");
  const tools = await session.tools();  // all tools the user is authorized for
  ```

  **After (Scalekit):**

  ```typescript
  const { tools } = await scalekit.tools.listScopedTools('user_123', {
    // Required: list every connection whose tools the agent should see
    filter: { connectionNames: ['gmail'] },
    pageSize: 100,
  });
  ```

#### Execute a tool

[Section titled “Execute a tool”](#execute-a-tool)

* Python

  **Before (Composio):**

  ```python
  session = composio.create(user_id="user_123")
  tools = session.tools()
  # Framework handles execution via the agent loop, or:
  # composio.tools.execute(tool_name="GMAIL_FETCH_MAILS", params={...})
  ```

  **After (Scalekit):**

  ```python
  result = actions.execute_tool(
      tool_name="gmail_fetch_mails",
      identifier="user_123",
      connection_name="gmail",
      tool_input={"query": "is:unread", "max_results": 5},
  )
  print(result.data)
  ```

* Node.js

  **Before (Composio):**

  ```typescript
  const session = await composio.create("user_123");
  const tools = await session.tools();
  // Framework handles execution via the agent loop
  ```

  **After (Scalekit):**

  ```typescript
  const result = await scalekit.actions.executeTool({
    toolName: 'gmail_fetch_mails',
    identifier: 'user_123',
    connector: 'gmail',
    toolInput: { query: 'is:unread', max_results: 5 },
  });
  console.log(result.data);
  ```

**Key differences:**

* Composio tool names are uppercase (`GMAIL_FETCH_MAILS`); Scalekit uses lowercase (`gmail_fetch_mails`)
* Composio’s session model means you don’t pass `user_id` on each call. With Scalekit, pass `identifier` and the connection name on every `executeTool` call: `connection_name` in Python, `connector` in Node.js
* Both return structured, LLM-ready output

### Map tool names

[Section titled “Map tool names”](#map-tool-names)

Composio and Scalekit may name tools differently for the same connector. Browse the connector’s tool list in the [Scalekit connector catalog](/agentkit/connectors/) to find the exact tool names. Common patterns:

| Composio tool name    | Scalekit tool name    |
| --------------------- | --------------------- |
| `GMAIL_FETCH_MAILS`   | `gmail_fetch_mails`   |
| `SLACK_SEND_MESSAGE`  | `slack_send_message`  |
| `GITHUB_CREATE_ISSUE` | `github_issue_create` |
| `NOTION_CREATE_PAGE`  | `notion_page_create`  |

Tool input schemas may also differ. Check each tool’s parameters in the connector catalog and update your agent’s tool input accordingly.

## 5. Migrate MCP

[Section titled “5. Migrate MCP”](#5-migrate-mcp)

Both platforms support MCP (Model Context Protocol) for framework-agnostic tool discovery and execution.

**Before (Composio):**

```json
{
  "mcpServers": {
    "composio": {
      "url": "https://backend.composio.dev/v3/mcp/{SERVER_ID}?user_id={USER_ID}",
      "headers": {
        "x-api-key": ""
      }
    }
  }
}
```

**After (Scalekit):**

Scalekit MCP uses Virtual MCP servers:

1. **Create a Virtual MCP server** — define which connections and tools the server exposes (one-time). This gives you a static `mcp_server_url`.
2. **Mint a session token** — before each agent run, call `create_session_token` for the user. Pass it as a bearer auth header.

See [Virtual MCP servers](/agentkit/mcp/overview/) for the full setup.

```json
{
  "mcpServers": {
    "scalekit": {
      "url": "",
      "headers": {
        "Authorization": "Bearer "
      }
    }
  }
}
```

**Key difference:** Composio embeds the user ID in the URL. Scalekit uses a static server URL shared across all users, with a short-lived session token per agent run for authentication.

## 6. Migrate custom tools

[Section titled “6. Migrate custom tools”](#6-migrate-custom-tools)

In Scalekit, custom tools use **API Proxy mode** (`actions.request`). The proxy works out of the box for most connectors, with no extra configuration. If a call returns `proxy not enabled for provider`, see [Call any API](/agentkit/tools/custom-tools/#proxy-not-enabled-for-provider). You define the tool contract in your application code and call the provider’s REST endpoint through Scalekit, which injects the user’s credentials automatically.

| Composio approach                                | Scalekit approach                                              |
| ------------------------------------------------ | -------------------------------------------------------------- |
| `@composio.tools.custom_tool` decorator          | Define the tool in your app code                               |
| In-memory, lost on restart                       | Lives in your codebase                                         |
| `executeToolRequest` for authenticated API calls | `actions.request()` — works out of the box for most connectors |

* Python

  ```python
  response = actions.request(
      connection_name="gmail",
      identifier="user_123",
      method="GET",
      path="/gmail/v1/users/me/messages",
  )
  ```

* Node.js

  ```typescript
  const response = await scalekit.actions.request({
    connectionName: 'gmail',
    identifier: 'user_123',
    method: 'GET',
    path: '/gmail/v1/users/me/messages',
  });
  ```

## 7. Add custom connectors

[Section titled “7. Add custom connectors”](#7-add-custom-connectors)

If your agent connects to an API or MCP server that isn’t in Scalekit’s built-in catalog, you can add your own connector. Custom connectors support OAuth 2.0, API keys, bearer tokens, and other auth types. Once created, they work exactly like built-in connectors — same connected account flow, same `actions.request()` proxy, same MCP tool calling.

See [Add your own connector](/agentkit/bring-your-own-connector/overview/) for the full walkthrough.

## Checklist

[Section titled “Checklist”](#checklist)

* \[ ] Scalekit account created, API credentials saved as environment variables
* \[ ] Scalekit SDK installed
* \[ ] Connections created in the Scalekit Dashboard for each connector
* \[ ] Connected account creation and authorization link flow ported
* \[ ] Tool names updated from uppercase to lowercase
* \[ ] `executeTool` calls updated with `identifier` and the connection name (`connection_name` in Python, `connector` in Node.js)
* \[ ] Tool input schemas verified against the Scalekit connector catalog
* \[ ] Virtual MCP server created and session token flow implemented (if using MCP)
* \[ ] Custom tools ported to `actions.request()` (if applicable)
* \[ ] Agent tested end-to-end with a test user
* \[ ] Users re-authorized through Scalekit’s OAuth flow

## Next

[Section titled “Next”](#next)

[Quickstart](/agentkit/quickstart/)Make your first tool call as a user.

[Choose a framework](/agentkit/examples/)Build the same agent in another framework.

---
# DOCUMENT BOUNDARY
---

# Control which tools each group can use in Agent Gateway

> Create groups, grant them all tools, an allow-list or a deny-list on each connection, check what a user gets and follow recipes for common setups.

This guide is for the admin who decides which tools people’s AI clients can use through Agent Gateway. By the end, you’ll have groups for your teams, a grant on each connection that says which tools each group gets, and a way to check what any user can use.

Available on request

Agent Gateway is available on request. [Contact us](/support/contact-us/) to enable it for your workspace.

## Before you start

[Section titled “Before you start”](#before-you-start)

* Agent Gateway is enabled for your environment, and you’ve added at least one connection. See the [Agent Gateway quickstart](/agentkit/agent-gateway/quickstart/).
* The people you want to group are Gateway users. See [Add users](/agentkit/agent-gateway/users-and-access/#add-users).

## How access works

[Section titled “How access works”](#how-access-works)

Three things decide which tools a person sees in their AI client:

| Piece              | What it is                                                                                                                       | Where you set it                  |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| **Group**          | A set of Gateway users, such as `Sales` or `Contractors`                                                                         | **Groups**                        |
| **Grant**          | Gives one group access to one connection, with all of its tools, only the tools you allow or all tools except the ones you block | The connection’s **Policies** tab |
| **Preview Access** | Shows the tools one user can use on a connection today                                                                           | The connection’s **Policies** tab |

A person belongs to the built-in **Everyone** group, plus every directory and manual group they’re a member of. On each connection, they get the tools from the grants of all their groups combined, minus any tool one of their groups blocks.

## Create groups

[Section titled “Create groups”](#create-groups)

1. In Agent Gateway, open **Groups**.

   ![Gateway groups page listing the built-in Everyone group and two manual groups, Contractors and Sales, with their source and member count](/.netlify/images?url=_astro%2Fgateway-groups-list.BygjYJWc.webp\&w=1600\&h=520\&dpl=6ae471fc143c9a4c27550463)

   Each group has a source:

   | Source        | Members                                                                                                                                        | Editable                                |
   | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
   | **Built-in**  | The **Everyone** group: every signed-in user                                                                                                   | No                                      |
   | **Directory** | A group synced from your identity provider through [SCIM](/agentkit/agent-gateway/users-and-access/#sign-users-in-with-your-identity-provider) | No: change it in your identity provider |
   | **Manual**    | The users you add                                                                                                                              | Yes                                     |

2. Select **Create Manual Group**, enter a **Name**, such as `Sales`, and an optional **Description**, then select **Create Group**.

   ![Create manual group dialog with the name Sales and the description Account executives and sales engineers](/.netlify/images?url=_astro%2Fgateway-create-group.CCna7Xu1.webp\&w=1056\&h=760\&dpl=6ae471fc143c9a4c27550463)

3. To add people, open the group’s **…** menu and select **Edit members**. Search for a user by email or name and select them. They’re added straight away. To remove someone, select the delete icon next to their name.

   ![Sales group drawer with a search field for adding a member and one current member, alex@example.com](/.netlify/images?url=_astro%2Fgateway-group-members.Ckks0zQ4.webp\&w=1488\&h=616\&dpl=6ae471fc143c9a4c27550463)

To delete a manual group, open its **…** menu and select **Delete**. This removes its members, its grants and any tool settings tied to them, and can’t be undone.

## Grant a group access to a connection

[Section titled “Grant a group access to a connection”](#grant-a-group-access-to-a-connection)

1. In Agent Gateway, open **Connections**, select the connection and open its **Policies** tab. The table lists each group with a grant and its access.

   ![Policies tab of a GitHub connection with two grants: Everyone with All tools and Contractors with a deny-list, plus Preview Access and Add Group buttons](/.netlify/images?url=_astro%2Fgateway-connection-policies.DRKhTtrT.webp\&w=1600\&h=587\&dpl=6ae471fc143c9a4c27550463)

2. Select **Add Group** and choose the group.

3. Choose its **Access**:

   | Access                  | The group can use                                   | Use it to                                                                           |
   | ----------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------- |
   | **All tools**           | Every tool on the connection                        | Give a team the whole app                                                           |
   | **Only specific tools** | Only the tools you select (an allow-list)           | Give the least access a team needs                                                  |
   | **All tools except…**   | Every tool except the ones you select (a deny-list) | Keep a few risky tools away from a group, even if another of its groups allows them |

4. For an allow-list or a deny-list, select the tools. The count above the list shows how many you’ve selected.

   ![Edit Group Access drawer for the Sales group with Only specific tools selected and four Gmail read tools checked, 4 of 38 selected](/.netlify/images?url=_astro%2Fgateway-grant-allow-list.CEdavZPx.webp\&w=1488\&h=1752\&dpl=6ae471fc143c9a4c27550463)

   ![Add Group Access drawer for the Contractors group with All tools except selected and githubpat_repo_delete checked under Blocked tools](/.netlify/images?url=_astro%2Fgateway-grant-deny-list.BtjMBjbO.webp\&w=1488\&h=1752\&dpl=6ae471fc143c9a4c27550463)

5. Select **Add Grant**.

To change a grant, open its **…** menu, select **Edit**, change the access or the tools and select **Save Changes**. To remove it, select **Revoke** and confirm. Revoking removes the grant and its tool selection.

![Revoke Contractors confirmation: This removes the grant and any per-tool refinements tied to it, with Cancel and Revoke buttons](/.netlify/images?url=_astro%2Fgateway-revoke-grant.BhVsp76i.webp\&w=1060\&h=390\&dpl=6ae471fc143c9a4c27550463)

## How grants combine

[Section titled “How grants combine”](#how-grants-combine)

* **A connection with no grants is open to every user.** Once a connection has a grant, only the groups with a grant can use it.
* **New connections start with `Everyone: All tools`.** A connection you add after enabling the Gateway gets that grant, so every user can use it until you edit or revoke it.
* **A person gets the tools from all their groups’ grants together.** If **Everyone** has **All tools**, each user has every tool their groups don’t block. An allow-list for another group doesn’t take tools away from its members.
* **A blocked tool stays blocked.** If any of a person’s groups has a deny-list that blocks a tool, they can’t use it, even if **Everyone** or another group allows it.
* **A tool missing from an allow-list isn’t blocked.** Another of the person’s groups can still grant it.

## Check what a user gets

[Section titled “Check what a user gets”](#check-what-a-user-gets)

On the connection’s **Policies** tab, select **Preview Access** and pick a user. You see the groups that contribute to their access and every tool they can use on this connection today.

![Preview Access for priya@example.com on a GitHub connection: contributing groups Everyone and Contractors, and 112 permitted tools](/.netlify/images?url=_astro%2Fgateway-preview-access.EYp3PoDL.webp\&w=1488\&h=1752\&dpl=6ae471fc143c9a4c27550463)

A user with no permitted tools has no grant on this connection through any of their groups:

![Preview Access for priya@example.com on a Gmail connection: contributing groups Everyone and Contractors, 0 permitted tools, and No tools permitted under the current policy set](/.netlify/images?url=_astro%2Fgateway-preview-no-access.pay2_peh.webp\&w=1488\&h=676\&dpl=6ae471fc143c9a4c27550463)

After each change, check Preview Access for one person in each group the change affects.

## Recipes

[Section titled “Recipes”](#recipes)

### Limit a connection to one team

[Section titled “Limit a connection to one team”](#limit-a-connection-to-one-team)

For example, only the sales team should use Gmail.

1. On the Gmail connection’s **Policies** tab, add a grant for `Sales` with **All tools**, or with **Only specific tools** for less.
2. Revoke the **Everyone** grant.
3. In **Preview Access**, check one member of `Sales`, who gets the tools, and one person outside it, who gets none.

Add the `Sales` grant before you revoke **Everyone**. If you revoke first, the connection has no grants and is open to every user again.

### Keep contractors read-only on a shared app

[Section titled “Keep contractors read-only on a shared app”](#keep-contractors-read-only-on-a-shared-app)

For example, everyone keeps full GitHub access, but contractors can only read.

1. On the GitHub connection, keep **Everyone: All tools**.
2. Add a grant for `Contractors` with **All tools except…** and select the tools that change something, such as `githubpat_repo_delete`, `githubpat_issue_create` and `githubpat_pull_request_merge`.
3. In **Preview Access**, check a contractor. The tools you blocked are missing, and everything else is there.

An allow-list for `Contractors` wouldn’t work here: contractors are also in **Everyone**, which already grants every tool. Only a deny-list takes tools away.

### Block one risky tool for everyone

[Section titled “Block one risky tool for everyone”](#block-one-risky-tool-for-everyone)

For example, nobody should delete repositories through an AI client.

1. On the connection’s **Policies** tab, open the **Everyone** grant’s **…** menu and select **Edit**.
2. Choose **All tools except…**, select the tool, such as `githubpat_repo_delete`, and select **Save Changes**.

Every user loses the tool, whatever their other groups allow.

### Roll out a new connection to a pilot group

[Section titled “Roll out a new connection to a pilot group”](#roll-out-a-new-connection-to-a-pilot-group)

A new connection starts with **Everyone: All tools**, so every user sees its tools as soon as you add it.

1. Right after you add the connection, add a grant for your pilot group, such as `Pilot`, then revoke the **Everyone** grant.
2. When the pilot ends, add **Everyone** back with **All tools**, or with a deny-list for the tools you want to keep restricted.

## Next

[Section titled “Next”](#next)

See what each plan includes in [Plans and limits](/agentkit/agent-gateway/plans-and-limits/).

---
# DOCUMENT BOUNDARY
---

# Connect AI clients to Agent Gateway

> Add the Agent Gateway MCP endpoint URL to Claude, ChatGPT, Cursor, VS Code, Claude Code, Windsurf, Grok Bot or any MCP client, then sign in once.

Each person on your team connects their own AI client to the Gateway’s MCP endpoint URL, then signs in with their own account. This page shows how to add the URL to each client, and what happens the first time the client connects.

Available on request

Agent Gateway is available on request. [Contact us](/support/contact-us/) to enable it for your workspace.

## Before you start

[Section titled “Before you start”](#before-you-start)

* **The MCP endpoint URL.** Copy it from **Settings** in Agent Gateway, or from the last step of the [Quickstart](/agentkit/agent-gateway/quickstart/). If your environment has a custom domain, the URL uses it.
* **A Gateway user account** for the person connecting. You’re added when you enable the Gateway. [Add more users](/agentkit/agent-gateway/users-and-access/#add-users) under **Identity**.

## Add the URL to your client

[Section titled “Add the URL to your client”](#add-the-url-to-your-client)

The dashboard shows the same steps under **Connect your agent** after setup. In the examples below, replace `` with your URL.

### Claude

[Section titled “Claude”](#claude)

1. Open Claude, then go to **Settings** > **Connectors**.
2. Select **Add custom connector**.
3. Paste the MCP endpoint URL and select **Add**.

### ChatGPT

[Section titled “ChatGPT”](#chatgpt)

1. Open ChatGPT, then go to **Settings** > **Apps** > **Advanced settings**, and turn on **Developer mode**.
2. Add a new connector and paste the MCP endpoint URL.

### Cursor

[Section titled “Cursor”](#cursor)

In the dashboard, pick **Cursor** under **Connect your agent** and select **Open in Cursor**. Cursor opens and adds the Gateway as an MCP server named `gateway`.

### VS Code

[Section titled “VS Code”](#vs-code)

1. Open the Command Palette: `⇧⌘P` on macOS, `Ctrl+Shift+P` on Windows and Linux.
2. Run **MCP: Add Server**, choose **HTTP** and paste the MCP endpoint URL.
3. Name the server, for example `gateway`, and choose **Global** or **Workspace**.

### Claude Code

[Section titled “Claude Code”](#claude-code)

Run this command in your terminal:

```bash
claude mcp add gateway --transport http 
```

### Windsurf and other MCP clients

[Section titled “Windsurf and other MCP clients”](#windsurf-and-other-mcp-clients)

Add the Gateway to the client’s MCP server configuration as an HTTP server:

```json
{
  "mcpServers": {
    "gateway": {
      "type": "http",
      "url": ""
    }
  }
}
```

The client must support remote MCP servers that sign in with OAuth.

### Grok Bot

[Section titled “Grok Bot”](#grok-bot)

1. In a Grok Bot chat, ask it to add the Gateway as a custom server, for example: “Add a custom MCP server called gateway at ``”.
2. Grok Bot shows a connect card. Select **Authorize** and sign in.

## Sign in the first time

[Section titled “Sign in the first time”](#sign-in-the-first-time)

The first time a client connects, it opens a browser window:

1. **Sign in** with your Gateway user account.
2. **Approve the consent screen.** It lets the client act on your behalf through the Gateway.

The client then lists the tools from the Gateway’s connections that your groups allow.

## Connect your app accounts

[Section titled “Connect your app accounts”](#connect-your-app-accounts)

Each person connects their own account for each app, such as their own Gmail or Outlook. The first time a tool needs an app you haven’t connected, the client returns a link instead of a result. Open the link, sign in to the app and approve access, then ask the client to try again.

## Review and revoke access

[Section titled “Review and revoke access”](#review-and-revoke-access)

Each user has their own account pages at `/ui/agents/get-started`. **Get started** there shows the MCP endpoint URL and the setup for each client, so you can send teammates that link. The same pages let each user review their own access:

* **Connected apps** lists the apps you’ve connected. Select **Disconnect** to stop agents from using an app. An agent asks you to connect it again the next time it needs it.
* **Authorized agents** lists the AI clients you’ve approved. Select **Revoke** to stop a client from getting new tokens. A token it already has keeps working until it expires.

As an admin, you set how long access tokens last with **Access token lifetime** in **Settings**.

## Common problems

[Section titled “Common problems”](#common-problems)

### The client can’t register with the Gateway

[Section titled “The client can’t register with the Gateway”](#the-client-cant-register-with-the-gateway)

Most AI clients register themselves the first time they connect. In **Settings** > **Advanced configuration**, keep **Allow dynamic client registration** turned on. Some clients identify themselves with a metadata URL instead; for those, turn on **Allow Client ID Metadata Document (CIMD)**.

### The client connects but shows no tools, or fewer than expected

[Section titled “The client connects but shows no tools, or fewer than expected”](#the-client-connects-but-shows-no-tools-or-fewer-than-expected)

Tools appear only for connections your groups can use. Open the connection under **Connections**, then its **Policies** tab, and select **Preview Access** to see which tools a user gets. See [Access policies](/agentkit/agent-gateway/access-policies/).

### Requests return 404

[Section titled “Requests return 404”](#requests-return-404)

The Gateway is disabled for the environment. Open **Settings** and select **Re-enable Gateway**. Users, registered clients and logs are kept while it’s disabled.

---
# DOCUMENT BOUNDARY
---

# Agent Gateway plans and limits

> What the Free, Growth and Enterprise plans of Agent Gateway include: users, tool calls, MCP connectors, access control, approvals, audit logs and support.

Agent Gateway has three plans: Free, Growth and Enterprise. Each plan sets how many users, tool calls and MCP connectors you get, which access controls you can use and what support comes with it. See your workspace’s plan and usage under **Billing** in Agent Gateway.

Available on request

Agent Gateway is available on request. [Contact us](/support/contact-us/) to enable it for your workspace.

## Compare plans

[Section titled “Compare plans”](#compare-plans)

|                                                                            | Free                              | Growth                                             | Enterprise                                                           |
| -------------------------------------------------------------------------- | --------------------------------- | -------------------------------------------------- | -------------------------------------------------------------------- |
| Price                                                                      | $0/month                          | $99/month                                          | Custom                                                               |
| Users                                                                      | Up to 5, with no additional users | 5 included, then $10 per additional user per month | Custom                                                               |
| Tool calls                                                                 | 10,000 per month                  | 100,000 per month, then $0.50 per 1,000            | Custom                                                               |
| MCP connectors                                                             | 5                                 | Unlimited, including custom MCP connectors         | Unlimited, including custom MCP connectors                           |
| Access control                                                             | Basic RBAC                        | Advanced RBAC and ABAC                             | Advanced RBAC and ABAC, plus custom policies such as approval chains |
| Approvals for sensitive actions                                            | —                                 | Included                                           | Included                                                             |
| Identity provider integration (Okta, Microsoft Entra ID, Google Workspace) | —                                 | Included                                           | Included                                                             |
| Audit log retention                                                        | 7 days                            | Contact us                                         | Custom                                                               |
| VPC or on-premises deployment                                              | —                                 | —                                                  | Included                                                             |
| SIEM and SOC export                                                        | —                                 | —                                                  | Included                                                             |
| HIPAA BAA and custom DPA                                                   | —                                 | —                                                  | Included                                                             |
| Uptime and support SLA                                                     | —                                 | —                                                  | 99.99% uptime SLA and a support SLA                                  |
| Support                                                                    | Community and email               | Private Slack                                      | Dedicated rollout and a customer success manager                     |

Every plan works with your team’s existing AI clients. Prices and plan details are on the [Agent Gateway pricing page](https://www.scalekit.com/pricing/agent-gateway), which is the reference if anything here differs.

## Choose a plan

[Section titled “Choose a plan”](#choose-a-plan)

* **Start on Free** to try the Gateway with a small team and up to five connectors.
* **Move to Growth** when you need more users, tool calls or connectors, sign-in through your identity provider, or approvals.
* **Talk to us about Enterprise** for custom policies, SIEM export, an uptime SLA, a HIPAA BAA or a self-hosted deployment. [Contact us](/support/contact-us/).

---
# DOCUMENT BOUNDARY
---

# Manage Agent Gateway users and access

> Add people to Agent Gateway, sign them in with SSO and SCIM, group them, and choose which connections and tools each group can use.

Agent Gateway decides what each person’s AI client can do in two steps: who can sign in, and which tools each group can use on each connection. You manage both in the dashboard, under **Identity**, **Groups** and each connection’s **Policies** tab.

Available on request

Agent Gateway is available on request. [Contact us](/support/contact-us/) to enable it for your workspace.

## Add users

[Section titled “Add users”](#add-users)

Only Gateway users can sign in to the Gateway. You become one when you enable it. To add someone else:

1. In Agent Gateway, open **Identity**, then the **Users** tab.
2. Select **Create User** and enter their email address.

They get access straight away, with no invitation email. They can then [connect their AI client](/agentkit/agent-gateway/connect-ai-clients/).

### Sign users in with your identity provider

[Section titled “Sign users in with your identity provider”](#sign-users-in-with-your-identity-provider)

To manage users from your company’s identity provider, such as Okta or Microsoft Entra ID, use the other two tabs in **Identity**:

* **SSO**: users sign in to the Gateway through your identity provider.
* **SCIM**: your identity provider syncs its users and groups to the Gateway. Synced groups appear under **Groups**.

## Group users and choose their tools

[Section titled “Group users and choose their tools”](#group-users-and-choose-their-tools)

**Groups** decide which connections and tools a person can use. Every user is in the built-in **Everyone** group. You can add manual groups, and groups synced from your identity provider through SCIM appear there too. On each connection’s **Policies** tab, you grant groups all tools, only the tools you allow or all tools except the ones you block.

[Access policies](/agentkit/agent-gateway/access-policies/) walks through creating groups and grants with screenshots, explains how grants combine and has recipes for common setups.

## Next

[Section titled “Next”](#next)

Create groups and choose which tools each one can use in [Access policies](/agentkit/agent-gateway/access-policies/).

---
# DOCUMENT BOUNDARY
---

# API credentials

> Find your AgentKit environment URL, client ID and client secret in the Scalekit dashboard, load them as environment variables, and rotate a secret safely.

Your code authenticates to Scalekit with three values: the environment URL, a client ID and a client secret. This page shows where to find them, how to load them, and how to rotate a secret without downtime.

## Before you start

[Section titled “Before you start”](#before-you-start)

* A Scalekit account. Signing up creates a Development environment. See [Environments and regions](/agentkit/environments/).
* The **Admin** or **Developer** role. Members can’t see API credentials. See [Team members and roles](/agentkit/team-members/).

## Get your credentials

[Section titled “Get your credentials”](#get-your-credentials)

Each environment has its own credentials. Check that the environment switcher at the top left shows the environment you want, then:

1. Open **Developers** > **Settings** > **API Credentials**.

2. Copy the **Environment URL** and the **Client ID** from **Environment details**.

3. Under **Client secrets**, select **Generate new secret**, then **Copy to clipboard**.

   The secret is shown once. Scalekit stores only a hash of it, so if you lose it, generate a new one. New environments start with no secret.

## Load them in your code

[Section titled “Load them in your code”](#load-them-in-your-code)

Your code reads these environment variables. Keep them in a `.env` file that you don’t commit, or in your platform’s secret manager. To load a `.env` file, install `python-dotenv` (Python) or `dotenv` (Node.js):

.env

```bash
SCALEKIT_ENVIRONMENT_URL=https://.scalekit.dev
SCALEKIT_CLIENT_ID=
SCALEKIT_CLIENT_SECRET=
```

* Python

  ```python
  import os


  from dotenv import load_dotenv
  from scalekit import ScalekitClient


  load_dotenv()  # reads .env into the environment


  scalekit_client = ScalekitClient(
      env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
      client_id=os.environ["SCALEKIT_CLIENT_ID"],
      client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
  )
  actions = scalekit_client.actions
  ```

* Node.js

  ```ts
  import 'dotenv/config'; // reads .env into process.env
  import { ScalekitClient } from '@scalekit-sdk/node';


  const scalekit = new ScalekitClient(
    process.env.SCALEKIT_ENVIRONMENT_URL!,
    process.env.SCALEKIT_CLIENT_ID!,
    process.env.SCALEKIT_CLIENT_SECRET!,
  );
  ```

* cURL

  ```bash
  set -a; source .env; set +a # exports the .env values to this shell


  TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
    -d grant_type=client_credentials \
    -d client_id="$SCALEKIT_CLIENT_ID" \
    -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token)
  ```

Use these credentials only on your server. Never put the client secret in browser or mobile code, where anyone can read it.

## Rotate a client secret

[Section titled “Rotate a client secret”](#rotate-a-client-secret)

An environment can have two secrets at a time, so you can switch to a new one before the old one stops working:

1. In **Developers** > **Settings** > **API Credentials**, select **Generate new secret** and copy it.

2. Deploy the new secret to every service that uses the old one.

3. Check that the old secret’s **Last used** time stops updating.

4. Select **Delete** on the old secret and confirm. It stops working immediately.

Rotate right away if a secret may have leaked, for example after it was committed to a repository or shown in logs.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

Run the cURL example: it prints an access token. With an SDK, any call such as `actions.get_connected_account` succeeds instead of returning `401`. The secret’s **Last used** time updates in the dashboard.

## Common problems

[Section titled “Common problems”](#common-problems)

### `401` or `invalid_client`

[Section titled “401 or invalid_client”](#401-or-invalid_client)

The client ID, secret and environment URL don’t all come from the same environment, or the secret was deleted. Copy all three again from the environment you’re calling.

### **Generate new secret** is disabled

[Section titled “Generate new secret is disabled”](#generate-new-secret-is-disabled)

The environment already has two secrets. Delete the one you no longer use, then generate a new one.

### **Delete** is disabled

[Section titled “Delete is disabled”](#delete-is-disabled)

It’s the environment’s only secret. Generate a new one first, so your code always has a working secret.

### I can’t see **API Credentials**

[Section titled “I can’t see API Credentials”](#i-cant-see-api-credentials)

Your role doesn’t include API credentials. Ask an Admin to give you the **Developer** role.

## Next

[Section titled “Next”](#next)

[Quickstart](/agentkit/quickstart/)Make your first tool call with these credentials.

[Security and compliance](/agentkit/security/)How Scalekit stores credentials, and what your app is responsible for.

---
# DOCUMENT BOUNDARY
---

# Billing

> Manage your AgentKit subscription in the Scalekit dashboard: add a payment method, change a Production environment's plan, track tool calls and view invoices.

Manage your subscription from the Scalekit dashboard: add a payment method, choose a plan for each Production environment, track usage and view invoices. For plan prices and what each includes, see [pricing](https://www.scalekit.com/pricing).

## How billing works

[Section titled “How billing works”](#how-billing-works)

* **Development is free.** Development environments are always on the Free plan and never billed.
* **Each Production environment has its own plan.** Two Production environments can be on different plans.
* **One payment method and billing address for the workspace.** They apply to every environment.
* **AgentKit usage is measured in tool calls.** Each tool call counts once, whether it succeeds or the app returns an error. Calls rejected because the tool or connected account doesn’t exist, and calls that hit a Scalekit internal error, don’t count. Connected accounts aren’t limited.
* **Going over the Free plan upgrades you.** When a Production environment on the Free plan uses up its tool calls, it moves to the next plan automatically, as long as the workspace has a payment method. Without one, tool calls stop until you add a card.

## Before you start

[Section titled “Before you start”](#before-you-start)

* The **Admin** role. Other roles can’t see billing. See [Team members and roles](/agentkit/team-members/).
* A Production environment selected in the environment switcher. **Billing** appears in the sidebar only in Production.

## Add a payment method

[Section titled “Add a payment method”](#add-a-payment-method)

Open the environment switcher at the top left and select **Enable Production**. Enter a card in the form. The same card pays for every Production environment in the workspace. See [Create a Production environment](/agentkit/environments/#create-a-production-environment).

## Change your plan

[Section titled “Change your plan”](#change-your-plan)

1. With a Production environment selected, open **Developers** > **Billing**. The **Overview** tab shows **Your plan**.

2. Select **Manage plan**, choose a plan, then select **Confirm change**.

The dashboard shows **Plan updated successfully.** The change applies to this environment only.

To move to a lower plan, or to cancel, contact [support](mailto:support@scalekit.com): lower plans show **Contact support to downgrade**. For Enterprise, select **Talk to sales**.

## Track usage

[Section titled “Track usage”](#track-usage)

The **Usage** tab shows the current **Billing period** and, for each metered item such as tool calls, how much you’ve used against your plan’s allowance.

## Update billing details

[Section titled “Update billing details”](#update-billing-details)

On the **Overview** tab, **Billing Information** shows the name, email address, address and billing period for the workspace. Select **Update billing info** or **Update payment details** to change them in the Stripe billing portal.

## View invoices

[Section titled “View invoices”](#view-invoices)

The **Invoices** tab lists this environment’s invoices. You can also select **View past invoices** under **Billing Information**.

## Common problems

[Section titled “Common problems”](#common-problems)

### I don’t see **Billing** in the sidebar

[Section titled “I don’t see Billing in the sidebar”](#i-dont-see-billing-in-the-sidebar)

Either a Development environment is selected, or your role isn’t Admin. Switch to a Production environment, or ask an Admin.

### “You have exceeded the free usage quota”

[Section titled ““You have exceeded the free usage quota””](#you-have-exceeded-the-free-usage-quota)

A Production environment on the Free plan used up its tool calls, and the workspace has no payment method. Tool calls fail until you add one: select **Enable Production** in the environment switcher and enter a card.

### “Your invoice is overdue”

[Section titled ““Your invoice is overdue””](#your-invoice-is-overdue)

A payment failed. Select **Pay Bill** to pay it, and update your card with **Update payment details** so the next one goes through.

### “Add a payment method to see invoices.”

[Section titled ““Add a payment method to see invoices.””](#add-a-payment-method-to-see-invoices)

The workspace has no payment method yet. Add one as shown in [Add a payment method](#add-a-payment-method).

## Next

[Section titled “Next”](#next)

[Environments and regions](/agentkit/environments/)Create and switch between Development and Production.

[Pricing](https://www.scalekit.com/pricing)Plan prices and what each plan includes.

---
# DOCUMENT BOUNDARY
---

# Create a connector

> Create a custom AgentKit connector for an app that isn't in the catalog: pick OAuth, API key, bearer or basic auth, build the definition and manage it by API.

Create a custom connector to bring an unsupported API or MCP server into Scalekit’s secure access model. This guide walks you through building the connector payload, creating the connector, and managing it over its lifecycle - list, update, and delete - with the management API.

[Check out the examples](https://github.com/scalekit-inc/python-connect-demos/tree/main/custom-connectors)

Prerequisites

You need three credentials from your Scalekit environment:

* `SCALEKIT_ENVIRONMENT_URL` - the base URL of your Scalekit environment
* `SCALEKIT_CLIENT_ID` - your environment’s client ID
* `SCALEKIT_CLIENT_SECRET` - your environment’s client secret

Find these in the Scalekit Dashboard under **Developers → Settings → API Credentials**.

## Create a connector

[Section titled “Create a connector”](#create-a-connector)

Create a connector in the Scalekit Dashboard or with the management API. The dashboard provides a guided form for MCP connectors; the management API gives you scriptable control over every connector type and auth pattern.

### Create an MCP connector in the dashboard

[Section titled “Create an MCP connector in the dashboard”](#create-an-mcp-connector-in-the-dashboard)

Add an MCP connector through a guided form - no payload required.

1. In the Scalekit Dashboard, switch to **AgentKit**.

2. Select **Connectors**.

3. Select **Create custom connector**.

4. Complete the **Add MCP connector** form:

   * **Display name**: a name for the connector, such as `Example MCP`.
   * **Description**: a short description of what the connector connects to.
   * **Icon URL** (optional): an icon for the connector. Must start with `https://`. For best results, use an 800×800px SVG image, such as `https://cdn.example.com/icon.svg`.
   * **Server URL**: the base URL of the MCP server. Must start with `https://`, such as `https://app.example.com/mcp`.
   * **Metadata** (optional): key-value pairs for the connector. Values must be plain strings; nested objects are not supported.

   For **Auth type**, choose how users authenticate when they connect their account:

   * **OAuth**: users authorize access through the provider’s OAuth flow, and Scalekit handles the token exchange. Scalekit finds the server’s authorization and token endpoints from its OAuth metadata. When you create a connection, Scalekit registers an OAuth client for you if the server advertises Dynamic Client Registration. If the server doesn’t support it, or registration fails, enter the client ID and client secret of [your own OAuth app](/agentkit/advanced/bring-your-own-oauth/) in the connection, and add the connection’s redirect URI to that app.
   * **Bearer token**: users provide a long-lived token issued by the provider.
   * **API key**: users provide an API key issued by the provider.
   * **No authentication**: the server is public and requires no credentials. Use this only when the server intentionally allows unauthenticated access and exposes no user-specific data or privileged operations, since every user shares the same anonymous access.

5. Select **Save**. The connector is ready to use when you create a connection.

### Create a connector with the management API

[Section titled “Create a connector with the management API”](#create-a-connector-with-the-management-api)

Build the connector payload using the reference and examples that follow, then create the connector with the management API.

Understand the connector payload

Supported auth types are `OAUTH`, `BASIC`, `BEARER`, and `API_KEY`. Use `OAUTH` when the upstream API or MCP server requires a user authorization flow and token exchange. Use `BASIC`, `BEARER`, or `API_KEY` when it accepts static credentials or long-lived tokens.

MCP providers use the same four auth types as REST API providers, with `is_mcp: true` set in each `auth_patterns[]` entry. OAuth MCP connectors use a simplified `oauth_config: {"pkce_enabled": true}`. Scalekit finds the server’s OAuth endpoints from its metadata and registers a client through Dynamic Client Registration when the server advertises it. Otherwise, enter your own OAuth app’s client ID and client secret in the connection. Non-OAuth MCP connectors omit `oauth_config` entirely. MCP connectors can also use `NO_AUTH` for public servers that require no credentials - set `is_mcp: true`, use an empty `fields: []`, and omit `oauth_config`. Use `NO_AUTH` only when the upstream intentionally allows unauthenticated public access and exposes no user-specific data or privileged operations; every user of the connector shares the same anonymous access.

The connector payload uses these common top-level fields:

* `display_name`: Human-readable name for the custom connector
* `description`: Short description of what the connector connects to
* `auth_patterns`: Authentication options supported by the connector
* `proxy_url`: Base URL the proxy should call for the upstream API (mandatory)
* `proxy_enabled`: Whether the proxy is enabled for the connector (mandatory, should be true)

`proxy_url` can also include templated fields when the upstream API requires account-specific values, for example `https://{{domain}}/api`.

Within `auth_patterns`, the most common fields are:

* `type`: The auth type, such as OAUTH, BASIC, BEARER, or API_KEY
* `display_name`: Label shown for that auth option
* `description`: Short explanation of the auth method
* `fields`: Inputs collected for static auth providers such as BASIC, BEARER, and API_KEY. These usually store values such as `username`, `password`, `token`, `api_key`, `domain`, or `version`.
* `account_fields`: Inputs collected for OAUTH connectors when account-scoped values are needed. This is typically used for values tied to a connected account, such as named path parameters.
* `oauth_config`: OAuth-specific configuration, such as authorize and token endpoints
* `auth_header_key_override`: Custom header name when the upstream does not use `Authorization`. For example, some APIs expect auth in a header such as `X-API-Key` instead of the standard `Authorization` header.
* `auth_field_mutations`: Value transformations applied before the credential is sent. This is useful when the upstream expects a prefix, suffix, or default companion value, such as adding a token prefix or setting a fallback password value for Basic auth.
* `is_mcp`: Set to `true` when the upstream is an MCP server. Tells Scalekit to route the connector through MCP tool calling instead of the HTTP proxy.

Below are example payloads for API and MCP connectors across all supported auth patterns.

Stateless MCP servers only

Scalekit connects to **stateless MCP servers** only. Stateful MCP servers that require persistent sticky connections or MCP session IDs are not supported.

* API Connector

  * OAuth

    ```json
    1
    {
    2
      "display_name": "My Asana",
    3
      "description": "Connect to Asana. Manage tasks, projects, teams, and workflow automation",
    4
      "auth_patterns": [
    5
        {
    6
          "type": "OAUTH",
    7
          "display_name": "OAuth 2.0",
    8
          "description": "Authenticate with Asana using OAuth 2.0 for comprehensive project management",
    9
          "fields": [],
    10
          "oauth_config": {
    11
            "authorize_uri": "https://app.asana.com/-/oauth_authorize",
    12
            "token_uri": "https://app.asana.com/-/oauth_token",
    13
            "user_info_uri": "https://app.asana.com/api/1.0/users/me",
    14
            "available_scopes": [
    15
              {
    16
                "scope": "profile",
    17
                "display_name": "Profile",
    18
                "description": "Access user profile information",
    19
                "required": true
    20
              },
    21
              {
    22
                "scope": "email",
    23
                "display_name": "Email",
    24
                "description": "Access user email address",
    25
                "required": true
    26
              }
    27
            ]
    28
          }
    29
        }
    30
      ],
    31
      "proxy_url": "https://app.asana.com/api",
    32
      "proxy_enabled": true
    33
    }
    ```

  * Bearer

    ```json
    1
    {
    2
      "display_name": "My Bearer Token Provider",
    3
      "description": "Connect to an API that accepts a static bearer token",
    4
      "auth_patterns": [
    5
        {
    6
          "type": "BEARER",
    7
          "display_name": "Bearer Token",
    8
          "description": "Authenticate with a static bearer token",
    9
          "fields": [
    10
            {
    11
              "field_name": "token",
    12
              "label": "Bearer Token",
    13
              "input_type": "password",
    14
              "hint": "Your long-lived bearer token",
    15
              "required": true
    16
            }
    17
          ]
    18
        }
    19
      ],
    20
      "proxy_url": "https://api.example.com",
    21
      "proxy_enabled": true
    22
    }
    ```

  * Basic

    ```json
    1
    {
    2
      "display_name": "My Freshdesk",
    3
      "description": "Connect to Freshdesk. Manage tickets, contacts, companies, and customer support workflows",
    4
      "auth_patterns": [
    5
        {
    6
          "type": "BASIC",
    7
          "display_name": "Basic Auth",
    8
          "description": "Authenticate with Freshdesk using Basic Auth with username and password for comprehensive helpdesk management",
    9
          "fields": [
    10
            {
    11
              "field_name": "domain",
    12
              "label": "Freshdesk Domain",
    13
              "input_type": "text",
    14
              "hint": "Your Freshdesk domain (e.g., yourcompany.freshdesk.com)",
    15
              "required": true
    16
            },
    17
            {
    18
              "field_name": "username",
    19
              "label": "API Key",
    20
              "input_type": "text",
    21
              "hint": "Your Freshdesk API Key",
    22
              "required": true
    23
            }
    24
          ]
    25
        }
    26
      ],
    27
      "proxy_url": "https://{{domain}}/api",
    28
      "proxy_enabled": true
    29
    }
    ```

  * API Key

    ```json
    1
    {
    2
      "display_name": "My Attention",
    3
      "description": "Connect to Attention for AI insights, conversations, teams, and workflows",
    4
      "auth_patterns": [
    5
        {
    6
          "type": "API_KEY",
    7
          "display_name": "API Key",
    8
          "description": "Authenticate with Attention using an API Key",
    9
          "fields": [
    10
            {
    11
              "field_name": "api_key",
    12
              "label": "Integration Token",
    13
              "input_type": "password",
    14
              "hint": "Your Attention API Key",
    15
              "required": true
    16
            }
    17
          ]
    18
        }
    19
      ],
    20
      "proxy_url": "https://api.attention.tech",
    21
      "proxy_enabled": true
    22
    }
    ```

* MCP Connector

  ```json
  1
  {
  2
    "display_name": "My Asana",
  3
    "description": "Connect to Asana. Manage tasks, projects, teams, and workflow automation",
  4
    "auth_patterns": [
  5
      {
  6
        "type": "OAUTH",
  7
        "display_name": "OAuth 2.0",
  8
        "description": "Authenticate with Asana using OAuth 2.0 for comprehensive project management",
  9
        "fields": [],
  10
        "oauth_config": {
  11
          "authorize_uri": "https://app.asana.com/-/oauth_authorize",
  12
          "token_uri": "https://app.asana.com/-/oauth_token",
  13
          "user_info_uri": "https://app.asana.com/api/1.0/users/me",
  14
          "available_scopes": [
  15
            {
  16
              "scope": "profile",
  17
              "display_name": "Profile",
  18
              "description": "Access user profile information",
  19
              "required": true
  20
            },
  21
            {
  22
              "scope": "email",
  23
              "display_name": "Email",
  24
              "description": "Access user email address",
  25
              "required": true
  26
            }
  27
          ]
  28
        }
  29
      }
  30
    ],
  31
    "proxy_url": "https://app.asana.com/api",
  32
    "proxy_enabled": true
  33
  }
  ```

* OAuth

  ```json
  1
  {
  2
    "display_name": "My Bearer Token Provider",
  3
    "description": "Connect to an API that accepts a static bearer token",
  4
    "auth_patterns": [
  5
      {
  6
        "type": "BEARER",
  7
        "display_name": "Bearer Token",
  8
        "description": "Authenticate with a static bearer token",
  9
        "fields": [
  10
          {
  11
            "field_name": "token",
  12
            "label": "Bearer Token",
  13
            "input_type": "password",
  14
            "hint": "Your long-lived bearer token",
  15
            "required": true
  16
          }
  17
        ]
  18
      }
  19
    ],
  20
    "proxy_url": "https://api.example.com",
  21
    "proxy_enabled": true
  22
  }
  ```

* Bearer

  ```json
  1
  {
  2
    "display_name": "My Freshdesk",
  3
    "description": "Connect to Freshdesk. Manage tickets, contacts, companies, and customer support workflows",
  4
    "auth_patterns": [
  5
      {
  6
        "type": "BASIC",
  7
        "display_name": "Basic Auth",
  8
        "description": "Authenticate with Freshdesk using Basic Auth with username and password for comprehensive helpdesk management",
  9
        "fields": [
  10
          {
  11
            "field_name": "domain",
  12
            "label": "Freshdesk Domain",
  13
            "input_type": "text",
  14
            "hint": "Your Freshdesk domain (e.g., yourcompany.freshdesk.com)",
  15
            "required": true
  16
          },
  17
          {
  18
            "field_name": "username",
  19
            "label": "API Key",
  20
            "input_type": "text",
  21
            "hint": "Your Freshdesk API Key",
  22
            "required": true
  23
          }
  24
        ]
  25
      }
  26
    ],
  27
    "proxy_url": "https://{{domain}}/api",
  28
    "proxy_enabled": true
  29
  }
  ```

* Basic

  ```json
  1
  {
  2
    "display_name": "My Attention",
  3
    "description": "Connect to Attention for AI insights, conversations, teams, and workflows",
  4
    "auth_patterns": [
  5
      {
  6
        "type": "API_KEY",
  7
        "display_name": "API Key",
  8
        "description": "Authenticate with Attention using an API Key",
  9
        "fields": [
  10
          {
  11
            "field_name": "api_key",
  12
            "label": "Integration Token",
  13
            "input_type": "password",
  14
            "hint": "Your Attention API Key",
  15
            "required": true
  16
          }
  17
        ]
  18
      }
  19
    ],
  20
    "proxy_url": "https://api.attention.tech",
  21
    "proxy_enabled": true
  22
  }
  ```

* API Key

  * OAuth

    ```json
    1
    {
    2
      "display_name": "GitHub MCP",
    3
      "description": "Connect to GitHub MCP",
    4
      "auth_patterns": [
    5
        {
    6
          "description": "Authenticate with GitHub MCP using browser OAuth.",
    7
          "display_name": "OAuth 2.1/DCR",
    8
          "fields": [],
    9
          "is_mcp": true,
    10
          "oauth_config": {
    11
            "pkce_enabled": true
    12
          },
    13
          "type": "OAUTH"
    14
        }
    15
      ],
    16
      "proxy_url": "https://api.githubcopilot.com/mcp/",
    17
      "proxy_enabled": true
    18
    }
    ```

  * Bearer

    ```json
    1
    {
    2
      "display_name": "Apify MCP",
    3
      "description": "Connect to Apify MCP to run web scraping, browser automation, and data extraction Actors directly from your AI workflows.",
    4
      "auth_patterns": [
    5
        {
    6
          "description": "Authenticate with Apify using your API Token.",
    7
          "display_name": "Apify Token",
    8
          "fields": [
    9
            {
    10
              "field_name": "token",
    11
              "hint": "Your Apify API Token",
    12
              "input_type": "password",
    13
              "label": "Apify Token",
    14
              "required": true
    15
            }
    16
          ],
    17
          "is_mcp": true,
    18
          "type": "BEARER"
    19
        }
    20
      ],
    21
      "proxy_url": "https://mcp.apify.com",
    22
      "proxy_enabled": true
    23
    }
    ```

  * Basic

    ```json
    1
    {
    2
      "display_name": "My Internal MCP",
    3
      "description": "Connect to an internal MCP server that authenticates with a username and password",
    4
      "auth_patterns": [
    5
        {
    6
          "type": "BASIC",
    7
          "display_name": "Basic Auth",
    8
          "description": "Authenticate with a username and password",
    9
          "is_mcp": true,
    10
          "fields": [
    11
            {
    12
              "field_name": "username",
    13
              "label": "Username",
    14
              "input_type": "text",
    15
              "hint": "Your username",
    16
              "required": true
    17
            },
    18
            {
    19
              "field_name": "password",
    20
              "label": "Password",
    21
              "input_type": "password",
    22
              "hint": "Your password",
    23
              "required": true
    24
            }
    25
          ]
    26
        }
    27
      ],
    28
      "proxy_url": "https://mcp.internal.example.com",
    29
      "proxy_enabled": true
    30
    }
    ```

  * API Key

    ```json
    1
    {
    2
      "display_name": "My API Key MCP",
    3
      "description": "Connect to an MCP server that authenticates with a static API key",
    4
      "auth_patterns": [
    5
        {
    6
          "type": "API_KEY",
    7
          "display_name": "API Key",
    8
          "description": "Authenticate with a static API key",
    9
          "is_mcp": true,
    10
          "fields": [
    11
            {
    12
              "field_name": "api_key",
    13
              "label": "API Key",
    14
              "input_type": "password",
    15
              "hint": "Your API key",
    16
              "required": true
    17
            }
    18
          ]
    19
        }
    20
      ],
    21
      "proxy_url": "https://mcp.example.com",
    22
      "proxy_enabled": true
    23
    }
    ```

  * No Auth

    ```json
    {
      "display_name": "Public Docs MCP",
      "description": "Connect to a public MCP server that requires no credentials",
      "auth_patterns": [
        {
          "type": "NO_AUTH",
          "display_name": "No Auth",
          "description": "Public server - no credentials required.",
          "is_mcp": true,
          "fields": []
        }
      ],
      "proxy_url": "https://mcp.example.com",
      "proxy_enabled": true
    }
    ```

* OAuth

  ```json
  1
  {
  2
    "display_name": "GitHub MCP",
  3
    "description": "Connect to GitHub MCP",
  4
    "auth_patterns": [
  5
      {
  6
        "description": "Authenticate with GitHub MCP using browser OAuth.",
  7
        "display_name": "OAuth 2.1/DCR",
  8
        "fields": [],
  9
        "is_mcp": true,
  10
        "oauth_config": {
  11
          "pkce_enabled": true
  12
        },
  13
        "type": "OAUTH"
  14
      }
  15
    ],
  16
    "proxy_url": "https://api.githubcopilot.com/mcp/",
  17
    "proxy_enabled": true
  18
  }
  ```

* Bearer

  ```json
  1
  {
  2
    "display_name": "Apify MCP",
  3
    "description": "Connect to Apify MCP to run web scraping, browser automation, and data extraction Actors directly from your AI workflows.",
  4
    "auth_patterns": [
  5
      {
  6
        "description": "Authenticate with Apify using your API Token.",
  7
        "display_name": "Apify Token",
  8
        "fields": [
  9
          {
  10
            "field_name": "token",
  11
            "hint": "Your Apify API Token",
  12
            "input_type": "password",
  13
            "label": "Apify Token",
  14
            "required": true
  15
          }
  16
        ],
  17
        "is_mcp": true,
  18
        "type": "BEARER"
  19
      }
  20
    ],
  21
    "proxy_url": "https://mcp.apify.com",
  22
    "proxy_enabled": true
  23
  }
  ```

* Basic

  ```json
  1
  {
  2
    "display_name": "My Internal MCP",
  3
    "description": "Connect to an internal MCP server that authenticates with a username and password",
  4
    "auth_patterns": [
  5
      {
  6
        "type": "BASIC",
  7
        "display_name": "Basic Auth",
  8
        "description": "Authenticate with a username and password",
  9
        "is_mcp": true,
  10
        "fields": [
  11
          {
  12
            "field_name": "username",
  13
            "label": "Username",
  14
            "input_type": "text",
  15
            "hint": "Your username",
  16
            "required": true
  17
          },
  18
          {
  19
            "field_name": "password",
  20
            "label": "Password",
  21
            "input_type": "password",
  22
            "hint": "Your password",
  23
            "required": true
  24
          }
  25
        ]
  26
      }
  27
    ],
  28
    "proxy_url": "https://mcp.internal.example.com",
  29
    "proxy_enabled": true
  30
  }
  ```

* API Key

  ```json
  1
  {
  2
    "display_name": "My API Key MCP",
  3
    "description": "Connect to an MCP server that authenticates with a static API key",
  4
    "auth_patterns": [
  5
      {
  6
        "type": "API_KEY",
  7
        "display_name": "API Key",
  8
        "description": "Authenticate with a static API key",
  9
        "is_mcp": true,
  10
        "fields": [
  11
          {
  12
            "field_name": "api_key",
  13
            "label": "API Key",
  14
            "input_type": "password",
  15
            "hint": "Your API key",
  16
            "required": true
  17
          }
  18
        ]
  19
      }
  20
    ],
  21
    "proxy_url": "https://mcp.example.com",
  22
    "proxy_enabled": true
  23
  }
  ```

* No Auth

  ```json
  {
    "display_name": "Public Docs MCP",
    "description": "Connect to a public MCP server that requires no credentials",
    "auth_patterns": [
      {
        "type": "NO_AUTH",
        "display_name": "No Auth",
        "description": "Public server - no credentials required.",
        "is_mcp": true,
        "fields": []
      }
    ],
    "proxy_url": "https://mcp.example.com",
    "proxy_enabled": true
  }
  ```

**Before submitting, review the final payload carefully:**

* `display_name` and `description`
* The selected auth `type`
* Required `fields` and `account_fields`
* OAuth endpoints and scopes, if the connector uses OAuth
* `proxy_url`
* Whether `is_mcp` is set to `true` for MCP providers

Generate an access token

All API requests require a short-lived access token. Generate one using your `SCALEKIT_CLIENT_ID` and `SCALEKIT_CLIENT_SECRET`:

```bash
curl --location "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode "client_id=$SCALEKIT_CLIENT_ID" \
  --data-urlencode "client_secret=$SCALEKIT_CLIENT_SECRET"
```

Use the `access_token` value from the response as `$env_access_token` in the `curl` commands below.

Use the payload for your auth type as the request body in the create request:

* cURL

  Terminal

  ```bash
  # $env_access_token and $SCALEKIT_CLIENT_SECRET are secrets - keep them server-side and out of source control.
  # --fail-with-body makes curl exit non-zero and print the error body on a non-2xx response.
  curl --fail-with-body --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers" \
    --header "Authorization: Bearer $env_access_token" \
    --header "Content-Type: application/json" \
    --data '{...}'
  ```

* Python

  The Python SDK builds the payload with typed request objects and authenticates using your client credentials - no separate access token step is needed. It covers MCP connector auth types: OAuth (via Dynamic Client Registration), Bearer, API key, and No Auth. The example below creates an OAuth MCP connector; swap the `AuthPattern` for the auth type you need.

  create_connector.py

  ```python
  1
  import os
  2
  from scalekit import ScalekitClient
  3
  from dotenv import load_dotenv
  4
  from scalekit.actions.types import AuthPattern, OAuthConfig, CreateCustomProviderRequest
  5
  from scalekit.common.exceptions import ScalekitException
  6
  load_dotenv()
  7


  8
  # Load credentials from the environment. Keep SCALEKIT_CLIENT_SECRET server-side -
  9
  # never commit it or expose it in client-side code.
  10
  scalekit_client = ScalekitClient(
  11
      client_id=os.getenv("SCALEKIT_CLIENT_ID"),
  12
      client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
  13
      env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
  14
  )
  15


  16
  try:
  17
      response = scalekit_client.actions.providers.create_custom_provider(
  18
          CreateCustomProviderRequest(
  19
              display_name="GitHub MCP",
  20
              description="Connect to GitHub MCP",
  21
              proxy_url="https://api.githubcopilot.com/mcp/",
  22
              proxy_enabled=True,
  23
              auth_patterns=[
  24
                  AuthPattern(
  25
                      type="OAUTH",
  26
                      display_name="OAuth 2.1/DCR",
  27
                      description="Authenticate with GitHub MCP using browser OAuth.",
  28
                      is_mcp=True,
  29
                      oauth_config=OAuthConfig(),  # pkce_enabled=True by default
  30
                  )
  31
              ],
  32
              # Optional: icon_src="https://cdn.example.com/icon.svg",
  33
              # Optional: metadata={"team": "platform"},
  34
          )
  35
      )
  36
      print("Created connector:", response.provider.identifier)
  37
  except ScalekitException as err:
  38
      # Handle validation errors, conflicts (duplicate name), auth failures, etc.
  39
      print("Failed to create connector:", err)
  40
      raise
  ```

A successful request returns the created connector. Next, create a connection in the Scalekit Dashboard, then continue with the standard connector flow to authorize users and call tools.

## List connectors

[Section titled “List connectors”](#list-connectors)

List existing connectors before you create one, to confirm whether a connector for the upstream already exists. You also need the list to find a connector’s `identifier` for update and delete requests.

* cURL

  Terminal

  ```bash
  # $env_access_token is a secret - keep it server-side and out of source control.
  curl --fail-with-body --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/providers?filter.provider_type=CUSTOM&page_size=1000" \
    --header "Authorization: Bearer $env_access_token"
  ```

* Python

  The Python SDK lists MCP connectors. For REST connectors, call the API directly:

  Terminal

  ```bash
  curl --fail-with-body --location "$SCALEKIT_ENVIRONMENT_URL/api/v1/providers?filter.provider_type=CUSTOM&page_size=1000" \
    --header "Authorization: Bearer $env_access_token"
  ```

  For MCP connectors:

  list_connectors.py

  ```python
  1
  import os
  2
  from scalekit import ScalekitClient
  3
  from dotenv import load_dotenv
  4
  from scalekit.actions.types import ListProvidersRequest
  5
  from scalekit.v1.providers.providers_pb2 import ProviderType
  6
  from scalekit.common.exceptions import ScalekitException
  7
  load_dotenv()
  8


  9
  # Keep SCALEKIT_CLIENT_SECRET server-side - never commit it or expose it client-side.
  10
  scalekit_client = ScalekitClient(
  11
      client_id=os.getenv("SCALEKIT_CLIENT_ID"),
  12
      client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
  13
      env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
  14
  )
  15


  16
  try:
  17
      response = scalekit_client.actions.providers.list_providers(
  18
          ListProvidersRequest(provider_type=ProviderType.CUSTOM, page_size=1000)
  19
      )
  20
      for provider in response.providers:
  21
          print(provider.identifier, provider.display_name)
  22
  except ScalekitException as err:
  23
      print("Failed to list connectors:", err)
  24
      raise
  ```

## Update a connector

[Section titled “Update a connector”](#update-a-connector)

Use the [List connectors](#list-connectors) API to get the connector `identifier`, then send the updated payload. Include `display_name`, `proxy_url`, and `auth_patterns` on every update, and echo back any other fields you want to keep - omitted fields are not preserved, so read the current connector first and change only what you need.

* cURL

  Terminal

  ```bash
  # $env_access_token and $SCALEKIT_CLIENT_SECRET are secrets - keep them server-side and out of source control.
  curl --fail-with-body --location --request PUT "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers/$PROVIDER_IDENTIFIER" \
    --header "Authorization: Bearer $env_access_token" \
    --header "Content-Type: application/json" \
    --data '{...}'
  ```

* Python

  The Python SDK updates MCP connectors. For REST connectors, call the API directly:

  Terminal

  ```bash
  curl --fail-with-body --location --request PUT "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers/$PROVIDER_IDENTIFIER" \
    --header "Authorization: Bearer $env_access_token" \
    --header "Content-Type: application/json" \
    --data '{...}'
  ```

  For MCP connectors:

  update_connector.py

  ```python
  1
  import os
  2
  from scalekit import ScalekitClient
  3
  from dotenv import load_dotenv
  4
  from scalekit.actions.types import ListProvidersRequest, UpdateCustomProviderRequest
  5
  from scalekit.common.exceptions import ScalekitException
  6
  load_dotenv()
  7


  8
  # Keep SCALEKIT_CLIENT_SECRET server-side - never commit it or expose it client-side.
  9
  scalekit_client = ScalekitClient(
  10
      client_id=os.getenv("SCALEKIT_CLIENT_ID"),
  11
      client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
  12
      env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
  13
  )
  14


  15
  provider_identifier = "prov_..."  # from list_providers
  16


  17
  try:
  18
      # Read the current state, then echo back every field you want to keep.
  19
      current = scalekit_client.actions.providers.list_providers(
  20
          ListProvidersRequest(identifier=provider_identifier)
  21
      ).providers[0]
  22


  23
      response = scalekit_client.actions.providers.update_custom_provider(
  24
          UpdateCustomProviderRequest(
  25
              identifier=current.identifier,
  26
              display_name=current.display_name,
  27
              proxy_url=current.proxy_url,
  28
              description="Updated description",
  29
              auth_patterns=current.auth_patterns,
  30
              icon_src=current.icon_src,
  31
              metadata=dict(current.metadata),
  32
          )
  33
      )
  34
      print("Updated connector:", response.provider.identifier)
  35
  except ScalekitException as err:
  36
      print("Failed to update connector:", err)
  37
      raise
  ```

## Delete a connector

[Section titled “Delete a connector”](#delete-a-connector)

Use the [List connectors](#list-connectors) API to get the connector `identifier`. If the connector is still in use, remove the related connections or connected accounts first.

* cURL

  Terminal

  ```bash
  # $env_access_token is a secret - keep it server-side and out of source control.
  curl --fail-with-body --location --request DELETE "$SCALEKIT_ENVIRONMENT_URL/api/v1/custom-providers/$PROVIDER_IDENTIFIER" \
    --header "Authorization: Bearer $env_access_token"
  ```

* Python

  delete_connector.py

  ```python
  1
  import os
  2
  from scalekit import ScalekitClient
  3
  from dotenv import load_dotenv
  4
  from scalekit.actions.types import DeleteCustomProviderRequest
  5
  from scalekit.common.exceptions import ScalekitException
  6
  load_dotenv()
  7


  8
  # Keep SCALEKIT_CLIENT_SECRET server-side - never commit it or expose it client-side.
  9
  scalekit_client = ScalekitClient(
  10
      client_id=os.getenv("SCALEKIT_CLIENT_ID"),
  11
      client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
  12
      env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
  13
  )
  14


  15
  try:
  16
      scalekit_client.actions.providers.delete_custom_provider(
  17
          DeleteCustomProviderRequest(identifier="prov_...")
  18
      )
  19
      print("Connector deleted.")
  20
  except ScalekitException as err:
  21
      # e.g. not found, or forbidden if the connector is still in use.
  22
      print("Failed to delete connector:", err)
  23
      raise
  ```

## Next

[Section titled “Next”](#next)

[Authorize a user](/agentkit/tools/authorize/)Create a connected account for the connector and send the user an authorization link.

[Call your connector](/agentkit/bring-your-own-connector/making-tool-calls/)Make API or MCP tool calls through your connector.

---
# DOCUMENT BOUNDARY
---

# Call your connector

> Call a custom AgentKit connector as a user: send REST requests through the API proxy with actions.request, or list and run the tools of a custom MCP connector.

Make tool calls to your custom connector once the connector, its connection and the user’s connected account are set up.

The call method depends on the connector type:

* **REST API connectors**: call the API through the proxy with `actions.request()`.
* **MCP connectors**: list the connector’s tools, then call them with `execute_tool`.

Both types use the same connection, connected account, and user authorization model.

## Prerequisites

[Section titled “Prerequisites”](#prerequisites)

Make sure:

* The connector exists and is configured with the right [auth pattern](/agentkit/bring-your-own-connector/create-connector)
* A [connection](/agentkit/connections) is configured for the connector
* The [connected account](/agentkit/connected-accounts) exists
* The user has completed [authorization](/agentkit/tools/authorize)

Create a connection for your connector in the Scalekit Dashboard:

![Connections page showing a custom connector connection alongside built-in connectors](/.netlify/images?url=_astro%2Fcustom-provider-connection.CmpN35cw.png\&w=2604\&h=762\&dpl=6ae471fc143c9a4c27550463)

After the user completes authorization, the connected account appears in the Connected Accounts tab:

![Connected Accounts tab showing an authenticated account for a custom connector](/.netlify/images?url=_astro%2Fcustom-provider-connected-account.CNBQ7XLh.png\&w=2610\&h=624\&dpl=6ae471fc143c9a4c27550463)

## REST API proxy calls

[Section titled “REST API proxy calls”](#rest-api-proxy-calls)

Call a REST API connector through the API proxy, the same way you call any catalog connector. [Call any API](/agentkit/tools/custom-tools/) has the full steps in Python, Node.js and cURL, including the account check and error handling. Two things are specific to a custom connector:

* `path` is relative to the connector’s `proxy_url`, not the app’s public base URL.
* The connector definition controls how Scalekit authenticates the call, so the request looks the same whether the connector uses OAuth, an API key or basic auth.

- Python

  ```python
  response = actions.request(
      connection_name="your-provider-connection",  # from AgentKit > Connections
      identifier="user_123",
      method="GET",
      path="/v1/customers",  # relative to the connector's proxy_url
  )
  print(response.status_code, response.json())
  ```

- Node.js

  ```typescript
  const response = await scalekit.actions.request({
    connectionName: 'your-provider-connection', // from AgentKit > Connections
    identifier: 'user_123',
    method: 'GET',
    path: '/v1/customers', // relative to the connector's proxy_url
  });
  console.log(response.status, response.data);
  ```

## MCP tool calling

[Section titled “MCP tool calling”](#mcp-tool-calling)

An MCP connector’s tools come from the upstream MCP server, and you call them exactly like built-in tools: list the scoped tools for the connection to get their names and input schemas, then call `execute_tool`. [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/) covers both steps in Python, Node.js and cURL, and how to read the result.

* Python

  ```python
  1
  from google.protobuf.json_format import MessageToDict
  2


  3
  page, _ = actions.tools.list_scoped_tools(
  4
      identifier="user_123",
  5
      filter={"connection_names": ["your-mcp-connection"]},
  6
      page_size=100,
  7
  )
  8
  print([MessageToDict(t.tool)["definition"]["name"] for t in page.tools])
  9


  10
  result = actions.execute_tool(
  11
      tool_name="tool_name_from_the_list",
  12
      identifier="user_123",
  13
      connection_name="your-mcp-connection",
  14
      tool_input={"key": "value"},  # match the tool's input_schema
  15
  )
  16
  print(result.data)
  ```

* Node.js

  ```typescript
  const page = await scalekit.tools.listScopedTools('user_123', {
    filter: { connectionNames: ['your-mcp-connection'] },
    pageSize: 100,
  });
  console.log(page.tools.map((t) => t.tool?.definition?.name));


  const result = await scalekit.actions.executeTool({
    toolName: 'tool_name_from_the_list',
    identifier: 'user_123',
    connector: 'your-mcp-connection',
    toolInput: { key: 'value' }, // match the tool's input_schema
  });
  console.log(result.data);
  ```

## Next

[Section titled “Next”](#next)

[Environments and regions](/agentkit/environments/)Create a Production environment and choose a region.

[AgentKit launch checklist](/agentkit/advanced/launch-checklist/)Everything to finish before real users connect.

---
# DOCUMENT BOUNDARY
---

# Set up AgentKit with your coding agent

> Add AgentKit to your codebase with Claude Code, Codex, GitHub Copilot CLI, Cursor or another coding agent: install the authstack plugin and paste one prompt.

Install the authstack plugin into your coding agent and paste one prompt. The agent generates client initialization, connected account management, OAuth authorization, and token handling — no boilerplate required.

## Before you start

[Section titled “Before you start”](#before-you-start)

* A Scalekit account at [app.scalekit.com](https://app.scalekit.com)
* A connector configured under **AgentKit** > **Connections**. The prompt defaults to GitHub (`github-connect`). Name another connector if you want that instead.
* Your API credentials from **Developers** > **Settings** > **API Credentials**

## Pick your coding agent

[Section titled “Pick your coding agent”](#pick-your-coding-agent)

* Recommended: one command

  Terminal

  ```bash
  npx @scalekit-inc/cli setup
  ```

  For repeated use: `npm install -g @scalekit-inc/cli` then `scalekit setup`.

  The CLI installs the authstack plugin for your editor. Then load the shipped skill `integrate-agentkit`. Complete any browser OAuth prompt for the Scalekit MCP server.

  Done: the command exits 0 and `integrate-agentkit` is available.

  Then copy the AgentKit prompt below (or describe your goal naturally).

* Per-tool details

  After the CLI (or if you prefer tool-native flows):

  * Claude Code / Copilot: marketplace + plugin install is handled by the CLI.
  * Cursor / Codex: plugins are installed locally by the CLI.
  * Other agents (Cline, Amp, OpenCode and other agents that support skills): run `npx @scalekit-inc/cli skills install -y` to install the Scalekit skills to `~/.agents/skills`, or `npx skills add scalekit-inc/authstack --skill integrate-agentkit` for this skill only.

  Then copy the AgentKit prompt below.

Using a coding agent?Copy AgentKit prompt

## Verify the setup

[Section titled “Verify the setup”](#verify-the-setup)

1. **Set environment variables** — copy `SCALEKIT_CLIENT_ID`, `SCALEKIT_CLIENT_SECRET`, and `SCALEKIT_ENVIRONMENT_URL` from **Developers** > **Settings** > **API Credentials** in the dashboard. Done: all three names are set in `.env`.
2. **Trigger the authorization flow** — run the generated example and confirm the browser redirects to the connector’s consent page. Done: the consent page opens.
3. **Call a tool** — after consent, run a read-only tool through Scalekit, such as `github_user_get_authenticated` for GitHub. Done: the tool call returns data from the connector.

Review generated code before deploying

Verify that token validation logic, error handling, and environment variable references match your application’s requirements. The generated code is a foundation, not a finished implementation.

## Troubleshooting

[Section titled “Troubleshooting”](#troubleshooting)

The agent generated code for a connector I haven’t configured yet

The plugin uses the connector name you provide in the prompt. If that connector isn’t configured in your Scalekit Dashboard, creating the authorization link fails with `connection not found for the given key`.

Fix: in the [Scalekit Dashboard](https://app.scalekit.com), go to **AgentKit** > **Connections** > **Create Connection**, finish the connection, then re-run the agent prompt with the exact connection name from the dashboard.

I want to swap connectors after the initial generation

Re-run the implementation prompt with the new connector name. The agent updates the connector reference in the client initialization and regenerates the tool calls. Existing connected accounts for the old connector are not affected.

The scaffolded code references an SDK version that doesn’t match my lockfile

The plugin targets the latest stable Scalekit SDK. If your lockfile pins an older version, either upgrade the SDK (`npm install @scalekit-sdk/node@latest` or equivalent) or ask the agent to regenerate using your pinned version by adding “use SDK version X.Y.Z” to the prompt.

---
# DOCUMENT BOUNDARY
---

# How AgentKit works

> Connections, connected accounts, identifiers and tools: the AgentKit model, every connected-account status, and the user verification modes.

This page explains the model behind AgentKit. For what AgentKit is and when to use it, see [What is AgentKit](/agentkit/overview/).

You configure a **connection** once per app. Each user approves access once, which activates their **connected account**. After that, your agent calls **tools** with the user’s **identifier**, and Scalekit makes the API call with that user’s token.

1. Once per app1 · You set up a connectionin the dashboard
2. Once per user2 · Your user approves accessauthorization link
3. Scalekit3 · Scalekit stores tokensconnected account is ACTIVE once any user verification passes
4. Every tool call4 · Your agent calls a toolidentifier + tool name

Gmail, Slack, GitHub…API call with the user's tokenScalekit adds the token, calls the API and returns JSON to your agent.

Steps 1 and 2 happen once. Step 4 happens on every call. Your code never handles an OAuth token.

## Key terms

[Section titled “Key terms”](#key-terms)

| Term               | What it is                                                                                                                                                                                                          | Who creates it                                                                          | In code                            |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ---------------------------------- |
| Connector          | A supported app, such as Gmail, and its library of tools.                                                                                                                                                           | Scalekit, or you for a [custom connector](/agentkit/bring-your-own-connector/overview/) | `gmail`                            |
| Connection         | Your environment’s settings for one connector: an OAuth client and scopes, or the fields for API-key sign-in. All your users share it.                                                                              | You, once, in the dashboard                                                             | `connection_name`                  |
| Connected account  | One user’s link to a connection. It holds their tokens and a status.                                                                                                                                                | Your code creates it. The user activates it by approving access.                        | `connected_account_id`             |
| Identifier         | Your app’s ID for the user. Always pass it with the connection name.                                                                                                                                                | You                                                                                     | `identifier`                       |
| Authorization link | A one-time URL where the user approves access.                                                                                                                                                                      | Scalekit, when your code asks for one                                                   | `get_authorization_link`           |
| User verification  | The check that the person who approved access is the user you meant.                                                                                                                                                | Your server, or Scalekit                                                                | `verify_connected_account_user`    |
| Tool               | One action on a connector, with an input schema.                                                                                                                                                                    | Scalekit for built-in tools, or you for custom tools                                    | `tool_name`                        |
| Virtual MCP server | A URL that exposes chosen tools for one user to any MCP client.                                                                                                                                                     | You, per agent                                                                          | `actions.mcp`                      |
| Session token      | A short-lived bearer token that lets an MCP client call a Virtual MCP server’s tools as one user. [Mint one](/agentkit/mcp/session-tokens/) before each run.                                                        | Your server, per run                                                                    | `actions.mcp.create_session_token` |
| API proxy          | A call to the app’s own API through Scalekit, which adds the user’s credentials. Use it when no [built-in tool](/agentkit/tools/scalekit-optimized-tools/) fits. See [Call any API](/agentkit/tools/custom-tools/). | Your code                                                                               | `actions.request`                  |
| Trigger            | An event in a user’s connected app, such as a new lead, that Scalekit sends to your server as a signed webhook so your agent can act. In beta. See [How triggers work](/agentkit/triggers/overview/).               | You subscribe, in the dashboard                                                         | `trigger_type`                     |

The **In code** column shows Python names. The Node.js SDK uses the camelCase form, such as `connectionName` and `connectedAccountId`. One exception: Node.js `executeTool` takes the connection name as `connector` and the tool’s inputs as `toolInput`. The REST API also names the connection `connector`.

## Connected account statuses

[Section titled “Connected account statuses”](#connected-account-statuses)

| Status                 | Means                                                           | What to do                                               |
| ---------------------- | --------------------------------------------------------------- | -------------------------------------------------------- |
| `ACTIVE`               | Tokens are valid.                                               | Call tools.                                              |
| `PENDING_AUTH`         | The user hasn’t finished approving access.                      | Send a new authorization link.                           |
| `PENDING_VERIFICATION` | The user approved, but user verification hasn’t confirmed them. | Finish your verify step, or check the verification mode. |
| `EXPIRED`              | Tokens expired or were revoked and couldn’t be refreshed.       | Send a new authorization link.                           |
| `DISCONNECTED`         | The account was disconnected.                                   | Reconnect with a new authorization link.                 |

In Python, `status` is a string such as `"ACTIVE"`. In Node.js, it is the numeric `ConnectorStatus` enum: import it from `@scalekit-sdk/node` (2.18.0 or later) and compare with `ConnectorStatus.ACTIVE`. The REST API returns the name as a string.

## Choose an identifier

[Section titled “Choose an identifier”](#choose-an-identifier)

Use your app’s internal user ID. It must be stable (it never changes), unique per user and hard to guess. Don’t use an email address: a user can change their email, and an old address can be reassigned to someone else, so the identifier would no longer point to the same person. An email is also easy to guess, which makes misuse easier.

Pass the identifier together with the connection name. When you pass `connected_account_id` instead, Scalekit ignores the identifier.

## User verification modes

[Section titled “User verification modes”](#user-verification-modes)

Set the mode in **AgentKit** > **Settings** > **User Verification**. New environments start with **None**.

| Mode                 | What happens                                                                                            | Use it for               |
| -------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------ |
| Custom user verifier | Scalekit redirects the user to your verify URL. Your server confirms the user and calls the verify API. | Production (recommended) |
| Scalekit users only  | The person approving access must be signed in to your Scalekit dashboard.                               | Internal testing         |
| None                 | Anyone with the link activates the account.                                                             | Development only         |

[Verify users](/agentkit/user-verification/) shows how to set up the custom user verifier.

## Next

[Section titled “Next”](#next)

[Set up a connection](/agentkit/connections/)Configure the app your agent needs.

[Authorize a user](/agentkit/tools/authorize/)Send the authorization link and wait until the account is ACTIVE.

[Verify users](/agentkit/user-verification/)Set up the custom user verifier before real users connect.

---
# DOCUMENT BOUNDARY
---

# Manage connected accounts

> Manage AgentKit connected accounts: check an account's status, detect when a user must reconnect, list and delete accounts, and request more OAuth scopes.

A **connected account** is the per-user record that holds a user’s credentials and tracks their authorization state for a specific connection. Your code creates it, with `get_or_create_connected_account` or when it requests an authorization link for the user. The user activates it by authorizing.

## Account states

[Section titled “Account states”](#account-states)

| State                  | Meaning                                                                                                    |
| ---------------------- | ---------------------------------------------------------------------------------------------------------- |
| `ACTIVE`               | Last known good: tool calls should work. A call can still move it to `EXPIRED` if the user revoked access. |
| `EXPIRED`              | Tokens expired or were revoked and couldn’t be refreshed; the user must authorize again                    |
| `PENDING_AUTH`         | The user hasn’t finished authorizing, or is authorizing again                                              |
| `PENDING_VERIFICATION` | OAuth complete; user identity verification still required before activation                                |
| `DISCONNECTED`         | Account was manually disconnected                                                                          |

See [Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/#connected-account-status) for what to do in each state.

## Check account status

[Section titled “Check account status”](#check-account-status)

Use `get_or_create_connected_account` as the safe default when a user may be connecting for the first time. When the account already exists and you only need its status, use `get_connected_account_details` in Python, which returns the account without its credentials. In Node.js, use `getConnectedAccount`.

* Python

  ```python
  response = actions.get_or_create_connected_account(
      connection_name="github-connect",
      identifier="user_123"
  )
  connected_account = response.connected_account
  print(f"Status: {connected_account.status}")
  ```

* Node.js

  ```typescript
  const response = await actions.getOrCreateConnectedAccount({
    connectionName: 'github-connect',
    identifier: 'user_123',
  });
  const connectedAccount = response.connectedAccount;
  console.log('Status:', connectedAccount?.status);
  ```

## Handle inactive accounts

[Section titled “Handle inactive accounts”](#handle-inactive-accounts)

When a connected account isn’t `ACTIVE`, generate a new authorization link and send it to the user.

The link opens the [Hosted Connect Widget](/agentkit/hosted-connect-widget/), a Scalekit-hosted page that adapts to the connection’s auth type:

* **OAuth connectors**: presents the provider’s OAuth consent screen
* **API key, basic auth, or other connectors**: presents a form to collect the required credentials

Your code is the same regardless of connector type. Scalekit determines the right flow based on the connection configuration.

* Python

  ```python
  if connected_account.status != "ACTIVE":
      link_response = actions.get_authorization_link(
          connection_name="github-connect",
          identifier="user_123"
      )
      # Redirect or send link_response.link to the user
  ```

* Node.js

  ```typescript
  import { ConnectorStatus } from '@scalekit-sdk/node';


  if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
    const linkResponse = await actions.getAuthorizationLink({
      connectionName: 'github-connect',
      identifier: 'user_123',
    });
    // Redirect or send linkResponse.link to the user
  }
  ```

Customize the widget

By default, the Hosted Connect Widget is served from your Scalekit environment domain. Use a custom domain to serve it from your own domain. See [Custom domain](/agentkit/advanced/custom-domain/).

## Detect when a user must authorize again

[Section titled “Detect when a user must authorize again”](#detect-when-a-user-must-authorize-again)

A connected account can leave the `ACTIVE` state on its own, with no action from you or the user. When that happens, the next tool call fails until the user authorizes again. To catch it early, [listen for the `connected_account.status_updated` webhook](/agentkit/account-events/) instead of waiting for a failed call.

### Common causes

[Section titled “Common causes”](#common-causes)

OAuth connected accounts most often move to `EXPIRED` because:

* **The provider revoked the refresh token.** A password change, an admin-initiated token revocation, or a provider security policy invalidates the refresh token, so Scalekit can no longer obtain new access tokens.
* **The refresh token expired.** Providers cap refresh-token lifetimes (for example, 30 or 180 days), and the expiry is rarely surfaced in advance.
* **No refresh token was issued.** When the connection’s scopes don’t request offline access, the provider returns only a short-lived access token and no refresh token to renew it.
* **The provider hit a per-user token limit.** Some providers keep only a fixed number of refresh tokens per user and app, and silently drop the oldest ones when a user reconnects repeatedly.

In every case, the user authorizes again. When no refresh token was issued, first add the provider’s offline-access scope to the connection, or the account expires again soon after. Offline scopes don’t raise a provider’s per-user token limit: to stay under it, send a new authorization link only when the account isn’t `ACTIVE`.

### Subscribe to status changes

[Section titled “Subscribe to status changes”](#subscribe-to-status-changes)

Scalekit sends a `connected_account.status_updated` webhook when an account’s status changes, with the old and new status. When `status` becomes `EXPIRED` or `DISCONNECTED`, [create a new authorization link](#handle-inactive-accounts) and ask the user to reconnect. [Listen for account events](/agentkit/account-events/) shows how to add an endpoint, verify the signature, and handle each event.

## List connected accounts

[Section titled “List connected accounts”](#list-connected-accounts)

* Python

  ```python
  list_response = actions.list_connected_accounts(
      connection_name="github-connect",
  )
  for account in list_response.connected_accounts:
      print(account.identifier, account.status)
  ```

* Node.js

  ```typescript
  import { ConnectorStatus } from '@scalekit-sdk/node';


  const listResponse = await actions.listConnectedAccounts({
    connectionName: 'github-connect',
  });
  for (const account of listResponse.connectedAccounts) {
    console.log(account.id, account.identifier, ConnectorStatus[account.status]);
  }
  ```

## Delete a connected account

[Section titled “Delete a connected account”](#delete-a-connected-account)

Deleting a connected account removes the user’s credentials and authorization state. The user must authorize again to reconnect.

* Python

  ```python
  actions.delete_connected_account(
      connection_name="github-connect",
      identifier="user_123",
  )
  ```

* Node.js

  ```typescript
  await actions.deleteConnectedAccount({
    connectionName: 'github-connect',
    identifier: 'user_123',
  });
  ```

## Update OAuth scopes

[Section titled “Update OAuth scopes”](#update-oauth-scopes)

Scopes apply to OAuth connectors only. For non-OAuth connectors (API key, basic auth, and similar), generate a new authorization link and the Hosted Connect Widget collects the new credentials.

To request additional OAuth scopes from an existing connected account:

1. Update the connection’s scopes in **AgentKit** > **Connections** > **Edit**.
2. Generate a new authorization link for the user.
3. The user completes the OAuth consent screen, approving the updated scopes.
4. Scalekit updates the connected account with the new token set.

## Next

[Section titled “Next”](#next)

[Listen for account events](/agentkit/account-events/)Get a webhook when an account expires or is disconnected.

[Use built-in tools](/agentkit/tools/scalekit-optimized-tools/)Call a tool as the user with execute_tool.

---
# DOCUMENT BOUNDARY
---

# Set up a connection

> Set up a connection in the Scalekit dashboard or with the API, so your agent can use a third-party connector on behalf of your users.

A **connection** is a configuration you create once, in the Scalekit dashboard or [with the API](#set-up-a-connection-with-the-api). It holds everything Scalekit needs to interact with a connector’s API: OAuth app credentials, scopes, redirect URIs, and so on. One connection serves all your users.

Users don’t configure connections. Each user gets a **connected account**, the per-user record that links them to a connection and holds their tokens. Your code creates it with `get_or_create_connected_account`, or when it requests an authorization link for the user, and the user activates it by authorizing.

## What the connection form asks for

[Section titled “What the connection form asks for”](#what-the-connection-form-asks-for)

The connection form adapts to what the connector requires. Two things determine how much you need to configure:

* **OAuth-based connectors** require the most setup. You register an OAuth app with the provider, then enter those credentials into Scalekit.
* **Non-OAuth connectors** (API key, basic auth, key pairs, and similar) require minimal developer setup (usually just a name). The user provides their own credentials when they create their connected account.

The sections below walk through both patterns.

## Set up an OAuth connection

[Section titled “Set up an OAuth connection”](#set-up-an-oauth-connection)

OAuth connections need an OAuth app registered with the provider. Scalekit provides the Redirect URI; you bring the Client ID and Client Secret. Many connectors also offer Scalekit’s own credentials, so you can skip registration entirely (see the tip at the end of this section).

Already see pre-filled credentials? DCR handled registration for you.

For some connectors, Scalekit automatically completes **Dynamic Client Registration (DCR)** with the provider. If the **Client ID**, **Client Secret**, **OAuth Authorization URL**, and **Token Endpoint** fields are already filled in when you open the connection form, DCR was successful — skip steps 2–4 below and go directly to [configuring scopes](#configure-scopes).

If the fields are empty, the provider does not support DCR. Follow the manual registration steps below.

1. ### Open the connection form

   [Section titled “Open the connection form”](#open-the-connection-form)

   In the Scalekit dashboard, go to **AgentKit** > **Connections** and click **Add connection**. Select the connector you want to configure.

   The form shows the fields that connector requires.

2. ### Copy the redirect URI

   [Section titled “Copy the redirect URI”](#copy-the-redirect-uri)

   Scalekit generates a **Redirect URI** for this connection. Copy it; you’ll need it in the next step.

   This URI is where the provider sends the user after they complete the OAuth consent screen. Scalekit handles the callback automatically.

3. ### Register your OAuth app with the provider

   [Section titled “Register your OAuth app with the provider”](#register-your-oauth-app-with-the-provider)

   In the provider’s developer console (GitHub, Salesforce, Google, etc.), create an OAuth app and add Scalekit’s Redirect URI to the list of authorized redirect URIs.

   The provider will give you a **Client ID** and **Client Secret** after registration.

   Redirect URI must match exactly

   The URI in the provider’s console must match what Scalekit shows character-for-character, including trailing slashes. A mismatch causes the OAuth flow to fail with a `redirect_uri_mismatch` error.

4. ### Enter your credentials

   [Section titled “Enter your credentials”](#enter-your-credentials)

   Back in the Scalekit dashboard, enter the **Client ID** and **Client Secret** from the provider.

5. ### Configure scopes

   [Section titled “Configure scopes”](#configure-scopes)

   Select the scopes your agent needs. Scopes define what your agent can do on the user’s behalf: for example, `user:email` or `repo` for GitHub.

   Scopes apply to all connected accounts

   The scopes you set here apply to every connected account that uses this connection. If you need different scopes for different user groups, create separate connections for each group.

   Add the offline-access scope

   If the provider has an offline-access scope, such as `offline_access`, select it. Providers issue a refresh token only when the connection asks for offline access. Without one, each connected account becomes [`EXPIRED`](/agentkit/troubleshooting/#expired) when its first access token does.

6. ### Save the connection

   [Section titled “Save the connection”](#save-the-connection)

   Click **Save**. The connection is now active and ready for connected accounts to be created against it.

Use Scalekit credentials to get started faster

For many OAuth connectors, the connection form offers a **Use Scalekit credentials** option. This lets you skip OAuth app registration, in development and in production. Switch to your own credentials when you want your app’s name on the consent screen. See [Bring your own credentials](/agentkit/advanced/bring-your-own-oauth/).

## Set up a non-OAuth connection

[Section titled “Set up a non-OAuth connection”](#set-up-a-non-oauth-connection)

For connectors that use API keys, basic auth, key pairs, or similar, the connection form asks for very little. In many cases, you only need to give the connection a name.

The user provides their own credentials (their API key, account details, or private key) when they create a connected account. Scalekit collects those credentials through the connected account form and stores them securely.

1. Go to **AgentKit** > **Connections** and click **Add connection**
2. Select the connector
3. Enter a **Connection name**: this identifies the connection in the dashboard and in your code
4. Click **Save**

When a connected account is created for this connection, Scalekit presents the user with a form that collects the credentials their specific account requires.

## Set up a connection with the API

[Section titled “Set up a connection with the API”](#set-up-a-connection-with-the-api)

To create connections from code, for example one per environment or from your own setup script, use the [Connections API](/agentkit/reference/connections/). This follows the same steps as the dashboard for an OAuth connector: create the connection, register its redirect URI with the provider, then add your OAuth app’s credentials and scopes.

The examples create a Gmail connection named `gmail-work`. They read your Scalekit API credentials and the Client ID and Client Secret of the OAuth app you registered with Google from a `.env` file:

.env

```bash
SCALEKIT_ENVIRONMENT_URL=
SCALEKIT_CLIENT_ID=
SCALEKIT_CLIENT_SECRET=
GMAIL_CLIENT_ID=
GMAIL_CLIENT_SECRET=
```

Install the SDK with `pip install scalekit-sdk-python python-dotenv` or `npm install @scalekit-sdk/node dotenv`. Each step below continues the same script. Save the Node.js code in a `.mts` file, such as `connection.mts`, and run it with `npx tsx connection.mts`. The cURL tabs use `jq`.

1. ### Create the connection

   [Section titled “Create the connection”](#create-the-connection)

   Pass the connector’s identifier, such as `GMAIL`, and a connection name. The response includes the connection’s redirect URI.

   * Python

     ```python
     import os
     from dotenv import load_dotenv
     from scalekit import ScalekitClient
     from scalekit.actions.connections import OAuthSettings


     load_dotenv()
     scalekit_client = ScalekitClient(
         env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
         client_id=os.environ["SCALEKIT_CLIENT_ID"],
         client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
     )
     actions = scalekit_client.actions


     connection = actions.create_connection(provider="GMAIL", connection_name="gmail-work")
     print("Redirect URI:", connection.oauth.redirect_uri)
     ```

   * Node.js

     ```typescript
     1
     import 'dotenv/config';
     2
     import { ScalekitClient } from '@scalekit-sdk/node';
     3


     4
     const scalekit = new ScalekitClient(
     5
       process.env.SCALEKIT_ENVIRONMENT_URL!,
     6
       process.env.SCALEKIT_CLIENT_ID!,
     7
       process.env.SCALEKIT_CLIENT_SECRET!,
     8
     );
     9
     const actions = scalekit.actions;
     10


     11
     const connection = await actions.createConnection({
     12
       provider: 'GMAIL',
     13
       connectionName: 'gmail-work',
     14
     });
     15
     if (connection.settings.case === 'oauthConfig') {
     16
       console.log('Redirect URI:', connection.settings.value.redirectUri);
     17
     }
     ```

   * cURL

     ```bash
     set -a; source .env; set +a


     ACCESS_TOKEN=$(curl -s "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
       -d grant_type=client_credentials \
       -d client_id="$SCALEKIT_CLIENT_ID" \
       -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token)


     curl -s -X POST "$SCALEKIT_ENVIRONMENT_URL/api/v1/agentkit/connection" \
       -H "Authorization: Bearer $ACCESS_TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"provider": "GMAIL", "connection_name": "gmail-work"}' \
       | jq -r .connection.oauth_config.redirect_uri
     ```

   To find a connector’s identifier, call [List connectors](/agentkit/bring-your-own-connector/create-connector/#list-connectors) and read `identifier`.

2. ### Register your OAuth app with the provider

   [Section titled “Register your OAuth app with the provider”](#register-your-oauth-app-with-the-provider-1)

   In the provider’s developer console, add the redirect URI from step 1 to your OAuth app’s authorized redirect URIs, exactly as returned. Put the app’s Client ID and Client Secret in `.env`.

3. ### Add the credentials and scopes

   [Section titled “Add the credentials and scopes”](#add-the-credentials-and-scopes)

   Update the connection with your OAuth app’s credentials and the scopes your agent needs. Only the fields you send change.

   * Python

     ```python
     actions.update_connection(
         "gmail-work",
         oauth=OAuthSettings(
             client_id=os.environ["GMAIL_CLIENT_ID"],
             client_secret=os.environ["GMAIL_CLIENT_SECRET"],
             scopes=["https://www.googleapis.com/auth/gmail.readonly"],
         ),
     )
     ```

   * Node.js

     ```typescript
     await actions.updateConnection({
       connectionName: 'gmail-work',
       oauth: {
         clientId: process.env.GMAIL_CLIENT_ID!,
         clientSecret: process.env.GMAIL_CLIENT_SECRET!,
         scopes: ['https://www.googleapis.com/auth/gmail.readonly'],
       },
     });
     ```

   * cURL

     ```bash
     CONNECTION_ID=$(curl -s "$SCALEKIT_ENVIRONMENT_URL/api/v1/agentkit/connection/gmail-work" \
       -H "Authorization: Bearer $ACCESS_TOKEN" | jq -r .connection.id)


     curl -s -X PATCH "$SCALEKIT_ENVIRONMENT_URL/api/v1/agentkit/connection/$CONNECTION_ID" \
       -H "Authorization: Bearer $ACCESS_TOKEN" \
       -H "Content-Type: application/json" \
       -d "{\"oauth_config\": {\"client_id\": \"$GMAIL_CLIENT_ID\", \"client_secret\": \"$GMAIL_CLIENT_SECRET\", \"scopes\": [\"https://www.googleapis.com/auth/gmail.readonly\"]}}" \
       | jq -r .connection.connection_name
     ```

4. ### Check it worked

   [Section titled “Check it worked”](#check-it-worked)

   Get the connection and confirm it has your Client ID and scopes. The client secret comes back masked.

   * Python

     ```python
     connection = actions.get_connection("gmail-work")
     print(connection.connection_name, connection.oauth.client_id, connection.oauth.scopes)
     ```

   * Node.js

     ```typescript
     const saved = await actions.getConnection('gmail-work');
     if (saved.settings.case === 'oauthConfig') {
       console.log(saved.connectionName, saved.settings.value.clientId, saved.settings.value.scopes);
     }
     ```

   * cURL

     ```bash
     curl -s "$SCALEKIT_ENVIRONMENT_URL/api/v1/agentkit/connection/gmail-work" \
       -H "Authorization: Bearer $ACCESS_TOKEN" \
       | jq '{connection_name: .connection.connection_name, client_id: .connection.oauth_config.client_id, scopes: .connection.oauth_config.scopes}'
     ```

   Expected output: `gmail-work`, your Client ID and `https://www.googleapis.com/auth/gmail.readonly`.

To rename, list or delete connections, see the [Connections API reference](/agentkit/reference/connections/).

## Create multiple connections for the same connector

[Section titled “Create multiple connections for the same connector”](#create-multiple-connections-for-the-same-connector)

You can create more than one connection for the same connector. This is useful when:

* Different groups of users need different scopes
* You want to maintain separate OAuth apps for staging and production
* You’re integrating with multiple instances of the same service (for example, two different Salesforce orgs)

Each connection has its own name, which you use to identify it in API calls and in the dashboard.

## Use the connection in code

[Section titled “Use the connection in code”](#use-the-connection-in-code)

Your code refers to a connection by its **connection name**, the name you gave it in the form. New environments include a GitHub connection named `github-connect`. Pass it, together with the user’s [identifier](/agentkit/concepts/#choose-an-identifier), when you create a connected account, request an authorization link or execute a tool:

| Call                                                | Python                             | Node.js                            | REST API    |
| --------------------------------------------------- | ---------------------------------- | ---------------------------------- | ----------- |
| Create a connected account or an authorization link | `connection_name="github-connect"` | `connectionName: 'github-connect'` | `connector` |
| Execute a tool                                      | `connection_name="github-connect"` | `connector: 'github-connect'`      | `connector` |

Use the name, not the connection’s `conn_...` ID. The REST field is called `connector`, but it also takes the connection name. To see these calls in full, follow [Authorize a user](/agentkit/tools/authorize/), or open [Create a connected account](/agentkit/reference/connected-accounts/create-a-connected-account/) and [Get an authorization link](/agentkit/reference/authorization/get-an-authorization-link/) in the API reference.

## Common scenarios

[Section titled “Common scenarios”](#common-scenarios)

If a user can’t finish connecting, find the error on [Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/):

* [`failed_to_exchange_token`](/agentkit/troubleshooting/#authorization-failed-failed_to_exchange_token): the page says **Authorization failed** after the user approved access.
* [`session_not_found`](/agentkit/troubleshooting/#session-expired-or-not-available-session_not_found): the page says **Session expired or not available**.
* [`redirect_uri_mismatch`](/agentkit/troubleshooting/#redirect_uri_mismatch): the provider rejects the redirect URI.
* [It works for your account but fails for your customers](/agentkit/troubleshooting/#it-works-for-your-account-but-fails-for-your-customers): the provider’s OAuth app is still in development or testing.

## Next

[Section titled “Next”](#next)

[Authorize a user](/agentkit/tools/authorize/)Create a connected account and send the user an authorization link.

[Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/)Find the error your user saw and how to fix it.

---
# DOCUMENT BOUNDARY
---

# Manage encryption keys

> Protect AgentKit data at rest with Scalekit-managed encryption keys, or bring your own key from Google Cloud Key Management Service (KMS) and rotate it.

Scalekit always encrypts data at rest, with a key specific to each environment. By default Scalekit manages that key for you; you can also bring your own from a cloud Key Management Service (KMS) provider if your compliance policy requires you to own and control the root key. This guide shows you how to view and manage keys in the dashboard and set up Bring Your Own Key (BYOK) with GCP Cloud KMS.

## Key types

| Type                                           | Description                                                                                        |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| **Scalekit managed Data Encryption Key (DEK)** | Scalekit generates and manages the key. No setup required.                                         |
| **Bring your own key (BYOK)**                  | You provide a key from your own KMS. You own the key lifecycle, including rotation and revocation. |

## Key states

| State      | Description                                                                                     |
| ---------- | ----------------------------------------------------------------------------------------------- |
| Staged     | Created, not yet in use. No data is encrypted with it.                                          |
| Primary    | The active key. All new encryption operations use it.                                           |
| Deprecated | Replaced by a newer key. Existing records may still reference it until re-encryption completes. |

## Manage keys

Go to **Settings** and open the **Encryption keys** tab.

1. **Create a key**

   Click **Create New Key** and select a provider:

   * **Scalekit Managed DEK**: Scalekit generates and manages the key. Click **Create**.
   * **BYOK - GCP Cloud KMS**: Complete [Set up BYOK with GCP KMS](#set-up-byok-with-gcp-kms) first to create the GCP key and grant access, then enter the key resource name here and click **Create**.

   The key is created in **Staged** state.

2. **Activate the key**

   Click **Activate** on a Staged key. The key becomes Primary and Scalekit uses it for all new encryption operations.

   One primary key at a time

   Creating a new key does not replace the current Primary key. The new key stays Staged until you explicitly activate it.

3. **Re-encrypt existing data**

   To apply the new key to existing records, click **Re-encrypt Data**. Scalekit decrypts each existing record with the previous key and re-encrypts it with the new key.

## Set up BYOK with GCP KMS

Use BYOK to register an encryption key from Google Cloud KMS that your team owns and controls. Scalekit uses the KMS API for all encrypt and decrypt operations and never stores the key material. Whether you can use BYOK depends on your plan; see [pricing](https://www.scalekit.com/pricing). Encryption at rest with a Scalekit-managed key doesn’t.

You own the key lifecycle

BYOK gives you direct control over your encryption key. Scalekit cannot rotate or manage the GCP key on your behalf. If the key is disabled, destroyed, or Identity and Access Management (IAM) access is revoked, Scalekit cannot encrypt new data or decrypt existing records until you restore access.

### Prerequisites

* A **Google Cloud project** with the [Cloud KMS API enabled](https://cloud.google.com/kms/docs/create-encryption-keys)
* **IAM permissions** to create key rings, keys, and set key-level IAM policies
* The **Scalekit service account email**, shown in the Scalekit dashboard when you select BYOK
* `gcloud` CLI installed and authenticated, or access to the [GCP Console](https://console.cloud.google.com/security/kms)

1. **Create a key ring**

   A [key ring](https://cloud.google.com/kms/docs/resource-hierarchy#key_rings) groups encryption keys by location. Key rings cannot be deleted or renamed once created. Choose the name and location carefully.

   * gcloud CLI

     ```bash
     gcloud kms keyrings create "scalekit-kms-keyring" \
       --location "global" \
       --project "YOUR-GCP-PROJECT"
     ```

     Replace `YOUR-GCP-PROJECT` with your Google Cloud project ID.

   * GCP Console

     1. Open [**Security > Key management**](https://console.cloud.google.com/security/kms), click **Key rings**.
     2. Click **Create key ring**, enter `scalekit-kms-keyring` as the name.
     3. Set **Location** to **Global** and click **Create**.

   Choose the right location

   Use `global` if your Scalekit environment has no regional data-residency requirement. For region-specific compliance, choose a location such as `us-east1` or `europe-west1`. See [Cloud KMS locations](https://cloud.google.com/kms/docs/locations).

2. **Create an encryption key**

   Create a symmetric AES-256-GCM key inside the key ring.

   * gcloud CLI

     ```bash
     gcloud kms keys create "scalekit-kms-key" \
       --location "global" \
       --keyring "scalekit-kms-keyring" \
       --purpose "encryption" \
       --protection-level "hsm" \
       --project "YOUR-GCP-PROJECT"
     ```

   * GCP Console

     1. Click the key ring you created, then click **Create key**.
     2. Enter `scalekit-kms-key` as the name.
     3. Set **Protection level** to **HSM** (recommended) and click **Continue**.
     4. Set **Key material** to **HSM-generated** and click **Continue**.
     5. Set **Purpose** to **Symmetric encrypt/decrypt** and click **Continue**.
     6. Set **Rotation period** per your policy and click **Create**.

3. **Grant Scalekit access to the key**

   Grant both roles at the key level to limit Scalekit’s IAM access to this specific key.

   | Role                                         | Purpose                                         |
   | -------------------------------------------- | ----------------------------------------------- |
   | `roles/cloudkms.cryptoKeyEncrypterDecrypter` | Encrypt and decrypt the DEK                     |
   | `roles/cloudkms.viewer`                      | Read key metadata for health checks and listing |

   * gcloud CLI

     ```bash
     gcloud kms keys add-iam-policy-binding "scalekit-kms-key" \
       --keyring "scalekit-kms-keyring" \
       --location "global" \
       --project "YOUR-GCP-PROJECT" \
       --member "serviceAccount:SCALEKIT-SERVICE-ACCOUNT" \
       --role "roles/cloudkms.cryptoKeyEncrypterDecrypter"


     gcloud kms keys add-iam-policy-binding "scalekit-kms-key" \
       --keyring "scalekit-kms-keyring" \
       --location "global" \
       --project "YOUR-GCP-PROJECT" \
       --member "serviceAccount:SCALEKIT-SERVICE-ACCOUNT" \
       --role "roles/cloudkms.viewer"
     ```

   * GCP Console

     1. In the GCP Console, open **Security > Key management**, click **Key rings**, then click the key ring name.
     2. Click the key name, then open the **Permissions** tab.
     3. Click **Grant access**. A side panel opens.
     4. In the **New principals** field, enter the Scalekit service account email. Copy it from **Settings > Encryption keys > Create New Key > BYOK - GCP Cloud KMS** in the Scalekit dashboard.
     5. In the **Assign Roles** section, select **Cloud KMS CryptoKey Encrypter/Decrypter** from the first **Role** dropdown.
     6. Click **+ Add another role** and select **Cloud KMS Viewer**.
     7. Click **Save**. The policy update takes effect within a few minutes.

     Key-level vs project-level IAM

     Grant these roles at the **key level** to limit Scalekit’s access to only this specific key.

4. **Register the key in Scalekit**

   * In the Scalekit dashboard, go to **Settings** and open the **Encryption keys** tab.
   * Click **Create New Key** and select **BYOK - GCP Cloud KMS**.
   * Copy the Scalekit service account email shown in the modal. You need it for step 3 (grant Scalekit access to the key) if you have not granted IAM access yet.
   * Enter the fully-qualified GCP key resource name:

   ```plaintext
   projects/YOUR-GCP-PROJECT/locations/global/keyRings/scalekit-kms-keyring/cryptoKeys/scalekit-kms-key
   ```

   To retrieve the exact name from the CLI:

   ```bash
   gcloud kms keys describe "scalekit-kms-key" \
     --keyring "scalekit-kms-keyring" \
     --location "global" \
     --project "YOUR-GCP-PROJECT" \
     --format="value(name)"
   ```

   * Click **Create**. The key is created in **Staged** state.

   Key resource name format

   The key reference must follow this format:

   ```plaintext
   projects/{PROJECT_ID}/locations/{LOCATION}/keyRings/{KEYRING_NAME}/cryptoKeys/{KEY_NAME}
   ```

   To retrieve the key ring path:

   ```bash
   gcloud kms keyrings describe "scalekit-kms-keyring" \
     --location "global" \
     --project "YOUR-GCP-PROJECT" \
     --format="value(name)"
   ```

   Append `/cryptoKeys/scalekit-kms-key` to get the full key reference.

5. **Activate the key**

   Click **Activate** on the staged key. The key becomes Primary and Scalekit uses it for all new encryption operations.

   Activation is permanent

   Once activated, you cannot deactivate the key without creating and activating a new key. Confirm your IAM grants are in place before activating.

6. **Re-encrypt existing data**

   Activation covers new writes automatically. Click **Re-encrypt Data** to migrate existing records. Scalekit decrypts each record with the previous key and re-encrypts it with the new key.

## Monitor with audit logs

Cloud KMS logs every cryptographic operation to [Cloud Audit Logs](https://cloud.google.com/kms/docs/audit-logging). Use this filter in **Cloud Logging** to see all encrypt, decrypt, and key events for your key:

```plaintext
resource.type="cloudkms_cryptokey"
```

## Fix common errors

Permission denied when activating or using the key

Check that both IAM bindings are applied to the correct key:

```bash
gcloud kms keys get-iam-policy "scalekit-kms-key" \
  --keyring "scalekit-kms-keyring" \
  --location "global" \
  --project "YOUR-GCP-PROJECT"
```

The output should list the Scalekit service account with both `roles/cloudkms.cryptoKeyEncrypterDecrypter` and `roles/cloudkms.viewer`. If the bindings are missing, repeat step 3.

Re-encryption reports unrecoverable rows

Unrecoverable rows are records that could not be re-encrypted, typically because the previous key version was disabled or destroyed before re-encryption completed.

Contact  if you need help recovering affected records.

Key resource name is not accepted

Verify the format is exactly:

```plaintext
projects/PROJECT_ID/locations/LOCATION/keyRings/KEYRING_NAME/cryptoKeys/KEY_NAME
```

Run `gcloud kms keys describe` with `--format="value(name)"` to retrieve the exact string. Do not construct it manually.

## Next

[Section titled “Next”](#next)

[Security and compliance](/agentkit/security/)What Scalekit protects, and what your app is responsible for.

[Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/)Find the error your user saw and how to fix it.

---
# DOCUMENT BOUNDARY
---

# Enterprise features

> Workspace controls for security and compliance reviews: SSO and SCIM for the Scalekit dashboard, custom roles, log streaming to your SIEM and your own encryption key.

These features help your security and IT teams control who manages AgentKit, see what your agents do, and hold the keys to your data. Which ones you can use depends on your plan; see [pricing](https://www.scalekit.com/pricing), or [contact us](/support/contact-us/) to turn one on.

Encryption doesn’t depend on your plan: Scalekit always encrypts your data at rest with a key specific to each environment. Bring your own key when you want to hold that key yourself.

| Feature                                                                       | What it gives you                                                                                                                                          |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **SSO for the Scalekit dashboard**                                            | Your team signs in to the Scalekit dashboard through your identity provider. [Contact us](/support/contact-us/) to set it up                               |
| **SCIM for the Scalekit dashboard**                                           | Your identity provider adds and removes dashboard members as people join or leave. [Contact us](/support/contact-us/) to set it up                         |
| **[Custom roles](/agentkit/team-members/#create-a-custom-role)**              | Give each teammate only the dashboard permissions they need                                                                                                |
| **[Log streaming](/agentkit/log-streaming/)**                                 | Send tool calls and connected account changes to Datadog, CrowdStrike or Google SecOps                                                                     |
| **[Bring your own key](/agentkit/encryption-keys/#set-up-byok-with-gcp-kms)** | Encrypt your environment’s data with a key you hold in your own Google Cloud KMS, in place of the Scalekit-managed key, and revoke Scalekit’s access to it |

## Next

[Section titled “Next”](#next)

[Stream logs to your SIEM](/agentkit/log-streaming/)Send tool calls and account changes to Datadog, CrowdStrike or Google SecOps.

[Security and compliance](/agentkit/security/)What Scalekit protects, and what your app is responsible for.

---
# DOCUMENT BOUNDARY
---

# Environments and regions

> Use Development and Production environments in AgentKit: create Production, choose the US or EU region, find each environment URL and switch between them.

An environment is an isolated copy of AgentKit: its own connections, connected accounts, API credentials, settings and logs. You build in a Development environment, then create a Production environment for real users. This page covers both, how regions work, and how to move between environments.

## What you get at signup

[Section titled “What you get at signup”](#what-you-get-at-signup)

Signing up creates a workspace with one **Development** environment. Development is free and meant for building and testing.

Every new environment, Development or Production, starts with:

* A GitHub connection named `github-connect`, which uses Scalekit’s OAuth app, so you can make a tool call before you configure anything. The [Quickstart](/agentkit/quickstart/) uses it.
* A client ID. Generate a client secret yourself, as shown in [API credentials](/agentkit/api-credentials/).

Environments don’t share anything. A connection, a connected account or a client secret in Development doesn’t exist in Production, so you set up Production’s connections again and each user connects again there.

## Development and Production

[Section titled “Development and Production”](#development-and-production)

|                             | Development                                              | Production                                                                      |
| --------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Environment URL             | `https://.scalekit.dev`                            | `https://.scalekit.com`                                                   |
| Billing                     | Always free                                              | Billed on its own plan. See [Billing](/agentkit/billing/)                       |
| Client IDs                  | Start with `skc_`                                        | Start with `prd_`                                                               |
| User verification           | All modes, including **Scalekit users only** for testing | **Custom user verifier** or **None**                                            |
| Custom domain               | Not available                                            | Available on paid plans. See [Custom domain](/agentkit/advanced/custom-domain/) |
| Quickstart in the dashboard | Shown                                                    | Hidden                                                                          |

In the EU region, environment URLs end in `.eu.scalekit.dev` and `.eu.scalekit.com`.

## Choose a region

[Section titled “Choose a region”](#choose-a-region)

Scalekit runs separate US and EU regions with no shared data. Choose the region on the signup page, with **Change Region**, before you create your account. Your workspace and all its environments stay in that region, and you can’t move a workspace between regions later. To use both, create a workspace in each.

EU data residency is an add-on to your plan. When you create an environment in an EU workspace, the dashboard asks you to confirm it. See [Where data is hosted](/agentkit/security/#where-data-is-hosted).

## Create a Production environment

[Section titled “Create a Production environment”](#create-a-production-environment)

1. ### Enable Production

   [Section titled “Enable Production”](#enable-production)

   Open the environment switcher at the top left of the dashboard, next to your workspace name, and select **Enable Production**. Enter a card in the **Enable Production** form. When it’s saved, the dashboard shows **Production enabled** and opens **Manage Environments**.

   Only Admins can add a payment method. See [Team members and roles](/agentkit/team-members/).

2. ### Create the environment

   [Section titled “Create the environment”](#create-the-environment)

   Select **Create Environment**, enter a **Name**, choose **Production** as the environment type, and select **Create**.

   You can’t change an environment’s type after you create it.

3. ### Set it up for real users

   [Section titled “Set it up for real users”](#set-it-up-for-real-users)

   In the new environment:

   * Generate a client secret and load the new environment URL, client ID and secret into your production configuration. See [API credentials](/agentkit/api-credentials/).
   * Create your connections. For your own branding on consent screens, use [your own OAuth app](/agentkit/advanced/bring-your-own-oauth/).
   * Set **AgentKit** > **Settings** > **User Verification** to **Custom user verifier**. See [Verify users](/agentkit/user-verification/).

A new Production environment starts on the Free plan. Change it in **Billing**. See [Billing](/agentkit/billing/).

## Switch environments

[Section titled “Switch environments”](#switch-environments)

The environment switcher at the top left shows the current environment. Select it to pick another environment. Everything in the dashboard, including the **AgentKit** pages, then shows that environment.

To see every environment or rename one, select **Manage Environments** in the switcher, or open **Workspace** > **Environments**. To delete an environment, contact [support](mailto:support@scalekit.com).

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

Your code talks to the environment whose URL and credentials it uses. Call the API with the Production values, for example with the [Quickstart](/agentkit/quickstart/), and check that the connected account appears in **AgentKit** > **Connected Accounts** with Production selected in the switcher.

## Common problems

[Section titled “Common problems”](#common-problems)

### Production is locked with “Requires a payment method”

[Section titled “Production is locked with “Requires a payment method””](#production-is-locked-with-requires-a-payment-method)

The workspace has no payment method yet. Select **Enable Production** in the environment switcher and add a card, or ask an Admin to.

### A connected account exists in Development but not in Production

[Section titled “A connected account exists in Development but not in Production”](#a-connected-account-exists-in-development-but-not-in-production)

Environments share nothing. Create the connection in Production and have the user authorize again there.

### My code reaches the wrong environment

[Section titled “My code reaches the wrong environment”](#my-code-reaches-the-wrong-environment)

The environment URL, client ID and client secret must all come from the same environment. A secret from Development fails against a Production URL.

## Next

[Section titled “Next”](#next)

[API credentials](/agentkit/api-credentials/)Get the environment URL, client ID and a client secret for your code.

[Launch checklist](/agentkit/advanced/launch-checklist/)Everything to check before real users connect.

---
# DOCUMENT BOUNDARY
---

# Connect Hermes to AgentKit

> Install the Scalekit plugin from the Hermes catalog, paste three API credentials, and call GitHub, Gmail, Slack and other apps by connection name.

[Hermes](https://hermes-agent.nousresearch.com/docs/) is the self-improving AI agent from Nous Research. Hermes runs where you put it: a laptop, a small VPS, or a GPU cluster. You reach Hermes from the command line, or from a messaging app such as Slack or Telegram, and Hermes also runs work on a cron schedule.

Hermes ships with 60+ built-in tools, and the built-in tools stop at the edge of your machine. A Hermes agent that reads your Gmail, posts to your Slack, or opens a GitHub pull request needs an access token for each app. Every access token belongs to one user.

Scalekit holds the connected account for that user, stores the access token, and refreshes it. Install the **Scalekit plugin** from the Hermes plugin catalog, paste three Scalekit values once, and Hermes calls your connections with four tools. Access tokens stay in Scalekit: the plugin’s tools don’t return them to the chat.

To manage several users on one Hermes host yourself, use the [`hermes-delegated-auth` skill](#install-the-authstack-skill) instead. It’s described later on this page.

![Hermes agent calling third-party services such as Gmail through Scalekit AgentKit's OAuth handler and token vault](/d2/docs/agentkit/hermes-0.svg)

## Prerequisites

[Section titled “Prerequisites”](#prerequisites)

* [Hermes Agent](https://hermes-agent.nousresearch.com/docs/getting-started/installation) 0.20.0 or later, installed with the Hermes installer. Run `hermes update` to upgrade an older install.
* A Scalekit account with AgentKit enabled: [sign up at app.scalekit.com](https://app.scalekit.com)
* At least one connection in **AgentKit** > **Connections**. Create the connection before you ask Hermes to use the app. See [Set up a connection](/agentkit/connections/).

## Install the Scalekit plugin

[Section titled “Install the Scalekit plugin”](#install-the-scalekit-plugin)

The [Scalekit plugin](https://github.com/scalekit-inc/hermes-tools) is listed in the Hermes plugin catalog. It adds four tools to Hermes and installs no extra Python packages. Quit any open `hermes` chat before you start.

1. ### Check the Hermes version

   [Section titled “Check the Hermes version”](#check-the-hermes-version)

   ```bash
   hermes --version
   ```

   The version must be 0.20.0 or later. If it’s older, run `hermes update`.

2. ### Install the plugin from the catalog

   [Section titled “Install the plugin from the catalog”](#install-the-plugin-from-the-catalog)

   ```bash
   hermes plugins install scalekit
   ```

   Hermes installs the version of the plugin that the catalog lists. To see that entry first, run `hermes plugins info scalekit`.

3. ### Paste three API credentials

   [Section titled “Paste three API credentials”](#paste-three-api-credentials)

   Hermes asks for three values and saves them in `~/.hermes/.env`. Copy them from **Developers** > **Settings** > **API Credentials** in the Scalekit dashboard. If Hermes doesn’t ask, the values are already in that file.

   | Prompt                     | What you paste                                                                         |
   | -------------------------- | -------------------------------------------------------------------------------------- |
   | `SCALEKIT_CLIENT_ID`       | Your client ID, such as `skc_1234` Required                                            |
   | `SCALEKIT_CLIENT_SECRET`   | Your client secret. This is your Scalekit secret, not a Gmail or Slack token. Required |
   | `SCALEKIT_ENVIRONMENT_URL` | Your environment URL, such as `https://your-env.scalekit.dev` Required                 |

   Keep the client secret private

   Anyone with `SCALEKIT_CLIENT_SECRET` can call your Scalekit environment. Keep it only in `~/.hermes/.env`, and never paste it into a chat.

4. ### Enable the plugin

   [Section titled “Enable the plugin”](#enable-the-plugin)

   When Hermes asks `Enable 'scalekit' now?`, type `Y`. If you skipped that prompt, run `hermes plugins enable scalekit`.

   Start a new `hermes` chat so the tools load. If a Hermes gateway is already running, restart it with `hermes gateway restart`.

5. ### Copy a connection name

   [Section titled “Copy a connection name”](#copy-a-connection-name)

   In **AgentKit** > **Connections**, copy the name of a connection, such as `github-connect`. You give Hermes that name. Don’t use the `conn_` ID.

## Try it

[Section titled “Try it”](#try-it)

Start a new `hermes` chat and send:

```txt
List my Scalekit connections.
```

```txt
Who am I on GitHub? Use the connection github-connect.
```

Replace `github-connect` with your own connection name.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

* **The list shows each connection** with its name, type and status, for example `github-connect`, `OAUTH`, `COMPLETED`.
* **The first request for an app returns an authorization link.** Open the link and finish sign-in on the hosted page, then send the same request again. A scheduled job can’t finish sign-in, so authorize each app in a normal chat first.
* **The next request returns your GitHub login and profile URL.** The reply doesn’t include the access token.

The plugin creates an [identifier](/agentkit/concepts/) (`usr_…`) on first use and stores it in `~/.hermes/plugin-data/scalekit/identifier`. You don’t type it. Updating the plugin keeps the same identifier, so the apps you authorized stay `ACTIVE`.

## Use the four tools

[Section titled “Use the four tools”](#use-the-four-tools)

| Tool                        | What it does                                                                                              |
| --------------------------- | --------------------------------------------------------------------------------------------------------- |
| `scalekit_list_connections` | Lists the connections in your Scalekit environment.                                                       |
| `scalekit_authorize`        | Returns an authorization link, or `ACTIVE` if the user already authorized the connection.                 |
| `scalekit_execute_tool`     | Runs one named tool once the connected account is `ACTIVE`. Until then, it returns an authorization link. |
| `scalekit_proxy`            | Calls one path of the app’s API through Scalekit, for connectors with no named tool for the task.         |

## Update the plugin

[Section titled “Update the plugin”](#update-the-plugin)

```bash
hermes plugins update scalekit
```

Hermes moves the plugin to the version the catalog lists now.

## How the authstack skill works

[Section titled “How the authstack skill works”](#how-the-authstack-skill-works)

This section covers the `hermes-delegated-auth` skill. For the plugin, see [Install the Scalekit plugin](#install-the-scalekit-plugin).

The skill runs the following loop when you name an app:

1. The skill looks up the Scalekit connection you already created for [GitHub](/agentkit/connectors/github/), [Gmail](/agentkit/connectors/gmail/), or another app.
2. The skill checks the user’s connected account. `ACTIVE` means auth is complete and Scalekit holds a token.
3. The skill returns an authorization link when the connected account is not `ACTIVE`.
4. The skill fetches the tool schema, calls the tool, and returns the result.
5. The skill calls the Scalekit HTTP proxy when the connector has no named tool.

An authorization link is a one-time URL. It opens the [Hosted Connect Widget](/agentkit/hosted-connect-widget/), which shows OAuth consent or an API-key form.

## Install the authstack skill

[Section titled “Install the authstack skill”](#install-the-authstack-skill)

Use this skill instead of the plugin when you want to set the default identifier yourself in `~/.hermes/.env`. You also need [`uv`](https://docs.astral.sh/uv/) on your `PATH`.

Using a coding agent?Copy Hermes prompt

1. ### Install the skill

   [Section titled “Install the skill”](#install-the-skill)

   Install `hermes-delegated-auth` from authstack:

   ```bash
   hermes skills install scalekit-inc/authstack/kits/agentkit/host/hermes-delegated-auth
   ```

   Confirm the skill is enabled:

   ```bash
   hermes skills list
   ```

   ```txt
   │ hermes-delegated-auth │          │ local  │ local │ enabled │
   ```

   Install the Python dependencies:

   ```bash
   cd "${HERMES_HOME:-$HOME/.hermes}/skills/hermes-delegated-auth"
   uv sync
   ```

   New chats load `/hermes-delegated-auth`. Run `/reset` in a chat that is already open.

   `HERMES_HOME` changes the skill path. The commands on this page use `~/.hermes`.

   Install from the authstack path

   A raw `SKILL.md` URL leaves out `scripts/`, and the skill then fails at run time. The `integrate-agentkit-host` skill is for coding agents, not for Hermes.

2. ### Configure credentials

   [Section titled “Configure credentials”](#configure-credentials)

   Put only Scalekit client credentials in `~/.hermes/.env`. Scalekit stores and refreshes the provider tokens for GitHub, Gmail, and Slack. Provider tokens never belong in a Hermes file.

   \~/.hermes/.env

   ```bash
   SCALEKIT_CLIENT_ID=skc_your_client_id
   # Keep this secret: anyone who has it can call tools as any ACTIVE identifier.
   SCALEKIT_CLIENT_SECRET=your_client_secret
   SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.dev
   SCALEKIT_IDENTIFIER=usr_8f3a2c
   ```

   | Parameter                  | Description                            |
   | -------------------------- | -------------------------------------- |
   | `SCALEKIT_CLIENT_ID`       | Your Scalekit client ID Required       |
   | `SCALEKIT_CLIENT_SECRET`   | Your Scalekit client secret Required   |
   | `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL Required |
   | `SCALEKIT_IDENTIFIER`      | Default user the host acts as Required |

   Copy the client ID, client secret, and environment URL from **Dashboard > Developers > Settings > API Credentials**.

   `SCALEKIT_IDENTIFIER` is not a dashboard credential. Pick an opaque id that only your system knows, for example `usr_8f3a2c`. Every `tool_exec.py` command needs `SCALEKIT_IDENTIFIER`, including `--list-connections`.

   Keep the identifier unguessable

   An email address or a first name is guessable. `SCALEKIT_CLIENT_SECRET` lets anyone call tools as any `ACTIVE` identifier, so a guessable identifier widens the damage. Add [user verification](/agentkit/user-verification/) in a multi-user product so the wrong person cannot activate the slot.

3. ### Ask Hermes to act

   [Section titled “Ask Hermes to act”](#ask-hermes-to-act)

   Start a chat and ask for a real action. Hermes acts as `SCALEKIT_IDENTIFIER` by default, and Hermes passes `--identifier` to the skill when you name another user in the prompt.

   * GitHub

     ```txt
     You: Who am I on GitHub?
     ```

     Hermes loads the skill and runs these steps:

     1. Hermes looks up the GitHub connection.
     2. Hermes checks that the connected account is `ACTIVE`, and returns an authorization link when the account is not.
     3. Hermes fetches the tool schema.
     4. Hermes calls the tool and returns your GitHub login.

   * Gmail

     ```txt
     You: Show me my latest unread emails
     ```

     Hermes runs these steps:

     1. Hermes looks up the Gmail connection.
     2. Hermes returns an authorization link when you have not authorized Gmail yet.
     3. Hermes fetches the tool schema.
     4. Hermes returns the mail.

   * Notion

     ```txt
     You: Read my Notion page https://notion.so/My-Page-abc123
     ```

     Hermes runs these steps:

     1. Hermes looks up the Notion connection.
     2. Hermes returns an authorization link when you have not authorized Notion yet.
     3. Hermes fetches the page tool schema.
     4. Hermes returns the page content.

   * Slack (as a user)

     ```txt
     You: As identifier usr_8f3a2c, list my unread Slack DMs
     ```

     Hermes runs these steps:

     1. Hermes looks up the Slack connection.
     2. Hermes returns an authorization link when the Slack connected account is not `ACTIVE`.
     3. Hermes fetches the Slack tool schema.
     4. Hermes returns the direct messages (DMs) as `usr_8f3a2c`.

     Create the Slack connection with **User scope** for the prompt above. A Slack connection with **Bot scope** acts as your Slack app instead of acting as the person. See the [Slack connector](/agentkit/connectors/slack/).

   * Google Calendar

     ```txt
     You: Create an out-of-office event tomorrow on the calendar for usr_8f3a2c
     ```

     Hermes runs these steps:

     1. Hermes looks up the Google Calendar connection for `usr_8f3a2c`.
     2. Hermes returns an authorization link when the connected account is not `ACTIVE`.
     3. Hermes calls the create-event tool.
     4. Scalekit writes the event with the `usr_8f3a2c` token.

   Name the skill in the prompt to force the same path:

   ```txt
   /hermes-delegated-auth who am I on GitHub?
   ```

## Verify the authstack skill

[Section titled “Verify the authstack skill”](#verify-the-authstack-skill)

Confirm the following for `hermes-delegated-auth`. For the plugin, see [Check it worked](#check-it-worked).

* The skill is listed (`hermes skills list` or `/skills`) as `hermes-delegated-auth`
* The connected account is `ACTIVE` after you finish the authorization link
* Hermes returns data from the provider (the GitHub login, or unread mail)

Open the authorization link again and retry the same prompt if the account stays inactive.

## Use Slack as a bot or as a user

[Section titled “Use Slack as a bot or as a user”](#use-slack-as-a-bot-or-as-a-user)

Slack issues two kinds of token, and the token decides whose name appears on a message. A bot token posts as your Slack app. A user token posts as the person who authorized the app. Authorizing the Hermes gateway grants a bot token, so the gateway alone cannot post as a person.

| Job                                          | Use                                                                                                                                                                                |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Chat with the agent in a channel or DM       | Hermes Slack **gateway**. Bot tokens (`xoxb-` + `xapp-`) in `~/.hermes/.env`. See the [Hermes Slack setup](https://hermes-agent.nousresearch.com/docs/user-guide/messaging/slack). |
| Read private history, post, or act as a user | Scalekit Slack connection created with **User scope**. See the [Slack connector](/agentkit/connectors/slack/).                                                                     |

The gateway bot is a channel into the agent. The gateway bot is not `usr_8f3a2c`.

Always-on hosts use both identities. The gateway hears the channel. Scalekit acts as the user.

## Run jobs on a schedule

[Section titled “Run jobs on a schedule”](#run-jobs-on-a-schedule)

These commands attach the `hermes-delegated-auth` skill. With the Scalekit plugin, a scheduled job that reaches an app the user hasn’t authorized returns the authorization link and stops, so authorize each app in a normal chat first.

Attach the skill to a Hermes cron job. Each fire is a fresh session. The skill uses Scalekit client credentials. You do not mint a session token.

```bash
hermes cron create "0 9 * * *" "List unread emails for usr_8f3a2c and post a 5-line summary" --skill hermes-delegated-auth
```

Or in chat:

```txt
/cron add "every weekday at 9am" "List unread emails for usr_8f3a2c and post a 5-line summary" --skill hermes-delegated-auth
```

The connected account must already be `ACTIVE`. Cron cannot click an authorization link.

Leave the gateway running on a headless host. A new authorization link can then land in Slack or Telegram. Do not put Gmail or Calendar refresh tokens in `~/.hermes/.env`.

## Choose how Hermes reaches Scalekit

[Section titled “Choose how Hermes reaches Scalekit”](#choose-how-hermes-reaches-scalekit)

Pick the path that matches the host.

**Scalekit plugin.** Install it from the Hermes catalog with `hermes plugins install scalekit`. Hermes saves three Scalekit values, the plugin creates the identifier, and you name a connection in the chat.

**Authstack skill.** Install `hermes-delegated-auth` from authstack. You set `SCALEKIT_IDENTIFIER` yourself and can name another user per turn.

**Virtual MCP in your app.** Your application mints a session token before each agent run and passes that token to the agent framework. Claude Managed Agents, Mastra, and CrewAI use this path.

| Question                | Scalekit plugin                                     | Authstack skill                  | Virtual MCP in your app      |
| ----------------------- | --------------------------------------------------- | -------------------------------- | ---------------------------- |
| Who runs the agent      | Hermes                                              | Hermes                           | your app                     |
| How many end users      | one stored identifier; a tool call can name another | many; switch with `--identifier` | many                         |
| Credential on the host  | Scalekit client ID and secret                       | Scalekit client ID and secret    | none                         |
| Anything expires?       | no                                                  | no                               | yes, per run                 |
| Who mints a replacement | nobody; there is no token                           | nobody; there is no token        | your code, before each run   |
| Tool scoping            | the full connector catalog                          | the full connector catalog       | only the tools in the server |
| Best for                | getting started on your own Hermes                  | multi-user hosts and cron        | short agent runs             |

For Virtual MCP in your app, see [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/).

## Avoid these mistakes

[Section titled “Avoid these mistakes”](#avoid-these-mistakes)

* Do not put provider tokens in `~/.hermes/.env`. Put only Scalekit credentials there.
* With the Scalekit plugin, let the plugin manage the identifier. Don’t type a `usr_…` value. The `hermes-delegated-auth` skill still needs `SCALEKIT_IDENTIFIER`.
* Do not set `SCALEKIT_IDENTIFIER` to an email or another guessable value.
* Do not use the bundled Google Workspace skill (`~/.hermes/google_token.json`) if AgentKit owns the Google user. `~/.hermes/google_token.json` is one laptop login, not a per-user identifier.
* Do not run `hermes mcp login` against Scalekit Virtual MCP (a Scalekit MCP URL plus a session token). Virtual MCP uses a static bearer session token, and a static bearer token is not MCP OAuth.
* Do not treat the Slack bot token as “send as usr_8f3a2c”.
* Do not rely on Hermes to sign in your end users. One gateway belongs to one operator, who holds the Scalekit credentials. That operator’s host acts for many end users, each with their own identifier, passed with `--identifier`. Confirm who each user is in your own app with [user verification](/agentkit/user-verification/), and mint per-end-user Virtual MCP session tokens in your application, not inside Hermes.

## Common scenarios

[Section titled “Common scenarios”](#common-scenarios)

How do I authorize a new connection?

The authstack skill returns an authorization link if the account is not `ACTIVE`. The plugin returns one the first time a user asks for an app. Open the link, finish the flow on the hosted page, then return to Hermes and retry.

The hosted page adapts to the connection. An OAuth connector asks for your consent. An API key connector, such as Snowflake, asks for the credential. Hermes never collects the credential in chat.

How do I switch users?

This answer is for the `hermes-delegated-auth` skill. The Scalekit plugin creates one identifier and keeps it across updates.

Set `SCALEKIT_IDENTIFIER` in `~/.hermes/.env` as the default.

Name another user in the prompt to override the default for one turn, for example `as identifier usr_8f3a2c`. Hermes then passes `--identifier usr_8f3a2c` to the skill, and Scalekit scopes the tools to that user’s connected account.

Why do I see “connection not found”?

1. Confirm the connection exists in **AgentKit** > **Connections**
2. Confirm the connection is complete, not a draft
3. Confirm `SCALEKIT_ENVIRONMENT_URL` matches the environment that holds the connection

Use the connection name, for example `github-connect`. Don’t use the `conn_…` id.

The connected account is not ACTIVE

Check the state in **AgentKit** > **Connected accounts**:

| State                  | What you do                                                                                  |
| ---------------------- | -------------------------------------------------------------------------------------------- |
| `PENDING_AUTH`         | Open the authorization link and finish OAuth                                                 |
| `PENDING_VERIFICATION` | Complete [user verification](/agentkit/user-verification/)                                   |
| `EXPIRED`              | Open a new authorization link. The token expired, or the user revoked access at the provider |
| `DISCONNECTED`         | The account was disconnected in Scalekit. Open a new authorization link, then retry          |

See [Manage connected accounts](/agentkit/connected-accounts/).

Can I use Virtual MCP with Hermes?

Use the Scalekit plugin or the `hermes-delegated-auth` skill on the Hermes host. The skill calls `execute_tool` with Scalekit client credentials and an identifier. No session token sits on the host for that skill, so nothing expires, and one host can serve many users and run cron jobs.

When you want each run limited to a chosen set of tools, mint Virtual MCP session tokens in your own application and pass them to the agent, as in [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/). Mint a token per end user in your application, not inside Hermes.

Do not set `auth: oauth` on a Scalekit Virtual MCP server. The server takes a bearer token, not MCP OAuth.

Why does `hermes mcp login` fail against Scalekit?

`hermes mcp login` performs MCP OAuth against a vendor’s MCP server. A Scalekit Virtual MCP server does not use MCP OAuth. It takes a static URL plus a session token that **your application** mints with `create_session_token`.

Set the bearer header in `config.yaml` instead. See [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/).

## Next

[Section titled “Next”](#next)

[Browse connectors](/agentkit/connectors/)Every app AgentKit connects to, with its tools

[Authorize a user](/agentkit/tools/authorize/)Create a connected account and send the user an authorization link

[Example repo](https://github.com/scalekit-developers/hermes-agentkit-example)Runnable companion on scalekit-developers

---
# DOCUMENT BOUNDARY
---

# Hosted Connect Widget

> What your users see when they open an AgentKit authorization link: OAuth consent or a credential form, scope choices, account states and errors.

The Hosted Connect Widget is the page your users land on when they open an [authorization link](/agentkit/tools/authorize/). Scalekit hosts it, so the user’s OAuth consent, password or API key goes to Scalekit, not through your app. It adapts to the connection: OAuth connectors send the user to the app’s consent screen, and API key, basic auth and similar connectors show a form for the credentials. This page explains what users see and do in the widget, and what you control.

The widget opens as a full page in the user’s browser, not as a component you embed in your app. To make it look like your product, see [Brand the Hosted Connect Widget](/agentkit/advanced/branding/).

## What the user sees

[Section titled “What the user sees”](#what-the-user-sees)

The widget shows your logo and a headline that names your application and the app, such as “Acme wants to connect to your Gmail”. Under it are a short overview of the app, then either a **Connect** button or a form, and links to your terms of service and privacy policy.

![Hosted Connect Widget for an OAuth connector: Acme Inc. wants to connect to your Notion, with a short description of Notion and a Connect button](/.netlify/images?url=_astro%2Fhosted-connect-widget-oauth.Bzxw0X6o.webp\&w=747\&h=669\&dpl=6ae471fc143c9a4c27550463)

What happens next depends on the connector’s auth type:

| Auth type                                           | What the user does                                                                        | When the account becomes `ACTIVE`                                                                                             |
| --------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| OAuth                                               | Selects **Connect**, grants access on the app’s consent screen, and returns to the widget | When the app returns a token                                                                                                  |
| API key, basic auth, bearer token and similar       | Fills in the form and selects **Connect**                                                 | As soon as the form is submitted. Scalekit doesn’t test the credential, so a wrong value shows up later as a failed tool call |
| Client credentials or Google domain-wide delegation | Fills in the form and selects **Connect**                                                 | After Scalekit gets a token from the app with the submitted values                                                            |

The form fields come from the connector: their labels, hints and which ones are required. Secret fields, such as an API key or password, are masked as the user types. A required field can’t be left empty.

![Hosted Connect Widget for an API key connector: Acme Inc. wants to connect to your Clari Copilot, with required API Key and API Password fields, each with a hint about where to find it](/.netlify/images?url=_astro%2Fhosted-connect-widget-credentials.DGN353cq.webp\&w=1272\&h=765\&dpl=6ae471fc143c9a4c27550463)

When the user is done, the widget shows **Account connected successfully** and the headline confirms the connection. The user stays on the widget page. Your app learns the result by [checking the account’s status](/agentkit/connected-accounts/) or from the [`connected_account.status_updated` event](/agentkit/account-events/).

## Let users choose scopes

[Section titled “Let users choose scopes”](#let-users-choose-scopes)

For OAuth connectors, you can let users decide which of the connection’s scopes to grant. Turn on **Allow users to choose scopes** on the connection’s configuration, and mark the scopes your agent can’t work without as **Required**.

The widget then lists the scopes under “Choose what Acme can access in your Gmail”. Every scope starts selected. Users can clear the optional ones; required scopes stay selected. If a user clears everything, the widget warns that the app may reject the connection or apply its own defaults.

![Hosted Connect Widget scope list: Choose what Acme Inc. can access in your Gmail, with two required scopes shown checked and disabled, and three optional scopes checked](/.netlify/images?url=_astro%2Fhosted-connect-widget-scopes.CVZf0g5i.webp\&w=1497\&h=888\&dpl=6ae471fc143c9a4c27550463)

Scalekit grants only the scopes the user kept, plus the required ones. Your agent’s tool calls that need a scope the user cleared fail with the app’s permission error, so mark a scope required when a tool you rely on needs it.

## Account states

[Section titled “Account states”](#account-states)

The widget shows the connected account’s current state each time the user opens a link:

| Account status         | What the widget shows                                                                                                    |
| ---------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Not yet connected      | The **Connect** button or the credential form                                                                            |
| `ACTIVE`               | That the account is connected, with a **Disconnect** button. The user doesn’t need to connect again                      |
| `EXPIRED`              | **Re-Authentication Required** and a **Reconnect** button                                                                |
| `PENDING_VERIFICATION` | **Verification pending**: the user authorized, and your app still has to [verify the user](/agentkit/user-verification/) |

## How credentials are stored

[Section titled “How credentials are stored”](#how-credentials-are-stored)

Everything a user submits in the widget, whether OAuth tokens, an API key or a username and password, is stored the same way: encrypted with your environment’s key before it’s written. See [How credentials are stored](/agentkit/security/#how-credentials-are-stored).

By default, your app doesn’t receive these values back. Your agent uses them through [tool calls](/agentkit/tools/scalekit-optimized-tools/) and the [API proxy](/agentkit/tools/custom-tools/), and Scalekit adds the credential to each request. If your app needs the raw values, see [Get a connected account’s credentials](/agentkit/reference/connected-accounts/get-connected-account-credentials/).

## Customize the widget

[Section titled “Customize the widget”](#customize-the-widget)

| What you change                                           | Where                                                                                             |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Your name, logo, colors, font and legal links             | **Customize** > **Branding**. See [Brand the Hosted Connect Widget](/agentkit/advanced/branding/) |
| The app name, overview and description for one connection | **Connections** > your connection > **Widgets**                                                   |
| The domain the widget is served from                      | [Custom domain](/agentkit/advanced/custom-domain/)                                                |
| Which scopes users can choose                             | **Allow users to choose scopes** on the connection, as above                                      |

## Common problems

[Section titled “Common problems”](#common-problems)

### This link has expired

[Section titled “This link has expired”](#this-link-has-expired)

The user opened the authorization link after it expired. Links last five minutes by default. [Create a new link](/agentkit/tools/authorize/) and send it when the user is ready to connect.

### This connected account no longer exists

[Section titled “This connected account no longer exists”](#this-connected-account-no-longer-exists)

The connected account was deleted after the link was created. Create the connected account again, then create a new link.

### Authorization failed

[Section titled “Authorization failed”](#authorization-failed)

The app didn’t issue a token after the user approved access. The usual causes are on the user’s side at the app: an administrator restricted the app with an access policy, the app isn’t approved in the user’s organization, or the user’s account lacks permission to authorize it. The user asks their administrator to allow the app, then opens a new link.

Expand the banner to see the error, its description and the auth request ID. Include them when you contact support.

### Session expired or not available

[Section titled “Session expired or not available”](#session-expired-or-not-available)

The user opened the link with **Scalekit users only** user verification, and isn’t signed in to Scalekit in that browser. In production, use **Custom user verifier** instead. See [Verify users](/agentkit/user-verification/).

### The permissions available for this connection have changed

[Section titled “The permissions available for this connection have changed”](#the-permissions-available-for-this-connection-have-changed)

The connection’s scopes changed while the user had the widget open. The user reloads the page and chooses again.

## Next

[Section titled “Next”](#next)

[Authorize a user](/agentkit/tools/authorize/)Create the authorization link that opens the widget.

[Brand the Hosted Connect Widget](/agentkit/advanced/branding/)Use your name, logo, colors and domain.

---
# DOCUMENT BOUNDARY
---

# Stream logs to your SIEM

> Stream AgentKit tool calls, connected account changes and other events to Datadog, CrowdStrike or Google SecOps, and check what was delivered.

Send a record of your agents’ tool calls, and of changes to your users’ connected accounts, to your security team’s SIEM. Scalekit pushes each event to the destination as it happens, so you can alert on it and keep it with the rest of your audit logs.

## Before you start

[Section titled “Before you start”](#before-you-start)

* Log streaming depends on your plan. If it isn’t on yours, the **Log Streaming** page says so. [Contact us](/support/contact-us/) to turn it on.
* An Admin role in the Scalekit dashboard, or a [custom role](/agentkit/team-members/#create-a-custom-role) that can manage log streaming.
* An account with one of these destinations:

| Destination   | What you need                                                                                                                                          |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Datadog       | Your Datadog **Site**, such as `datadoghq.com`, and an **API key**                                                                                     |
| CrowdStrike   | An **Ingest URL** and an **Ingest token** from a CrowdStrike data connector                                                                            |
| Google SecOps | Set up with Scalekit: you grant a Scalekit service account access to your instance instead of sharing a credential. [Contact us](/support/contact-us/) |

Using a different log destination? [Contact us](/support/contact-us/) and tell us which one.

Each environment streams separately. Set up a destination in Production to capture your users’ activity.

## Create a destination

[Section titled “Create a destination”](#create-a-destination)

1. In the Scalekit dashboard, switch the product to **Auth for MCP & SaaS** and go to **Developers** > **Log Streaming**. Log streaming covers your AgentKit events too.

2. Select **Create destination** and choose the **Provider**.

3. Enter a **Display name**, such as `Production SIEM`, and the destination’s details from the table above.

4. Under **Events to stream**, keep every category checked, or uncheck the ones you don’t want. For AgentKit, keep **Agent Tool Calls** and **Connected Accounts**.

5. Make sure **Stream events to this destination** is on, then select **Create destination**.

To stop streaming without losing the setup, edit the destination and switch **Stream events to this destination** off.

## What AgentKit streams

[Section titled “What AgentKit streams”](#what-agentkit-streams)

| Category                  | Events                                                                                                                                                                                                                                                                                                                                                                               |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Agent Tool Calls**      | `connected_account.tool_executed`, one for each tool call made on a user’s connected account. A call that fails before Scalekit finds the connected account sends no event                                                                                                                                                                                                           |
| **Connected Accounts**    | `connected_account.created`, `connected_account.updated`, `connected_account.deleted`, `connected_account.status_updated`, `connected_account.magic_link_generated`, `connected_account.oauth_succeeded`, `connected_account.oauth_tokens_fetched`, `connected_account.token_refresh_succeeded`, `connected_account.token_refresh_failed`, `connected_account.tool_discovery_failed` |
| **Admin & Configuration** | Changes to the environment’s configuration                                                                                                                                                                                                                                                                                                                                           |

Each event has the same envelope as an [account event webhook](/agentkit/account-events/): `spec_version`, `id`, `type`, `occurred_at`, `environment_id`, `object` and `data`.

A tool call event’s `data` says who called which tool, and how it went:

* **The tool:** `tool_name`, `tool_provider`, `tool_execution_id` and `tool_execution_duration_ms`.
* **The user:** `connected_account_id`, `connected_account_identifier`, `connection_id` and `connection_name`.
* **The outcome:** `status` and `is_error`, plus `error_code` and `error_message` when the call failed.
* **The caller,** when known: `source`, `agent_id` and `agent_run_id`.

The tool’s input and output are not included.

Note

An event can be delivered more than once. Use the event’s `id` to drop duplicates in your SIEM.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

1. In the destination’s menu, select **Test connection**. Scalekit sends a test event, and the dashboard shows **Connection succeeded.** when the destination accepts it.
2. Search your SIEM for `type:scalekit.logstream.connectivity_test` to confirm it arrived.
3. Make a tool call, then open **View deliveries** on the destination. The `connected_account.tool_executed` event shows as **Delivered**.

If a delivery fails, Scalekit retries it. **View deliveries** shows each attempt with its HTTP status and when the next retry is due.

## Next

[Section titled “Next”](#next)

[Enterprise features](/agentkit/enterprise-features/)SSO, SCIM, custom roles and your own encryption key.

[Security and compliance](/agentkit/security/)What Scalekit protects, and what your app is responsible for.

---
# DOCUMENT BOUNDARY
---

# Create a Virtual MCP server

> Create an AgentKit Virtual MCP server with only the tools an agent needs, then list, update or delete it, with examples in Python, Node.js and cURL.

Create one Virtual MCP server for each agent role. It lists the connections and tools the agent can use, and gives you a fixed URL that every user of that agent shares. You do this once, not once per user.

## Before you start

[Section titled “Before you start”](#before-you-start)

* A connection for each app the agent needs, in **AgentKit** > **Connections**. [Set up a connection](/agentkit/connections/) if you haven’t. These examples use `github-connect`, the GitHub connection new environments include.

* A client set up as in the [Quickstart](/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`:

  ```bash
  TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
    -d grant_type=client_credentials \
    -d client_id="$SCALEKIT_CLIENT_ID" \
    -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token)
  ```

1. ## Choose the tools

   [Section titled “Choose the tools”](#choose-the-tools)

   Pick the smallest set of tools the agent needs. Get exact names from [tool search or the scoped tool list](/agentkit/tools/scalekit-optimized-tools/#find-the-right-tool), or from each connector’s page in the [connector catalog](/agentkit/connectors/).

   If you leave out `tools` for a connection, the server gets every tool that connection has when you create it. Tools added to the connector later aren’t included.

2. ## Create the server

   [Section titled “Create the server”](#create-the-server)

   Give the server a name that’s unique in the environment, and list the tools for each connection.

   * Python

     ```python
     from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping


     response = actions.mcp.create_config(
         name="repo-assistant",
         description="Reads the user's GitHub repositories and issues",
         connection_tool_mappings=[
             McpConfigConnectionToolMapping(
                 connection_name="github-connect",
                 tools=["github_user_repos_list", "github_issues_list"],
             ),
         ],
     )
     config_id = response.config.id
     mcp_server_url = response.config.mcp_server_url
     print(config_id, mcp_server_url)
     ```

   * Node.js

     ```ts
     const { config } = await scalekit.actions.mcp.createConfig({
       name: 'repo-assistant',
       description: "Reads the user's GitHub repositories and issues",
       connectionToolMappings: [
         { connectionName: 'github-connect', tools: ['github_user_repos_list', 'github_issues_list'] },
       ],
     });
     const configId = config!.id;
     const mcpServerUrl = config!.mcpServerUrl;
     console.log(configId, mcpServerUrl);
     ```

   * cURL

     ```bash
     curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs" \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{
         "name": "repo-assistant",
         "description": "Reads the user'\''s GitHub repositories and issues",
         "connection_tool_mappings": [
           {"connection_name": "github-connect", "tools": ["github_user_repos_list", "github_issues_list"]}
         ]
       }' |
       jq '.config | {id, mcp_server_url}'
     ```

3. ## Save the ID and URL

   [Section titled “Save the ID and URL”](#save-the-id-and-url)

   Store the server’s `id` and `mcp_server_url` in your configuration. You pass the ID when you mint session tokens, and the URL to your agent. For cURL, keep them in `CONFIG_ID` and `MCP_SERVER_URL`.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

List servers by name to see the new server and its URL:

* Python

  ```python
  response = actions.mcp.list_configs(filter_name="repo-assistant")
  for config in response.configs:
      print(config.id, config.name, config.mcp_server_url)
  ```

* Node.js

  ```ts
  const { configs } = await scalekit.actions.mcp.listConfigs({ search: 'repo-assistant' });
  for (const c of configs) console.log(c.id, c.name, c.mcpServerUrl);
  ```

* cURL

  ```bash
  curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs" \
    -H "Authorization: Bearer $TOKEN" \
    --data-urlencode "filter.name=repo-assistant" |
    jq '.configs[] | {id, name, mcp_server_url}'
  ```

Then [mint a session token](/agentkit/mcp/session-tokens/) and list the server’s tools from an MCP client.

## Choose how tools are exposed

[Section titled “Choose how tools are exposed”](#choose-how-tools-are-exposed)

A server exposes its tools in one of two modes, set with `tool_exposure`:

* `DIRECT`: the agent sees every tool you map to the server. A server can map up to 25 connections, and two connections can’t map tools with the same name.
* `META`: the agent sees five tools, `search_available_tools`, `list_available_tools`, `get_tool_details`, `execute_tool` and `connect_account`, and uses them to find and run the tools you mapped. A server can map more than 25 connections, and tool names can repeat across connections.

Choose `META` when an agent needs more connections or tools than it can list at once. If you leave `tool_exposure` out, the server uses `DIRECT`. The response’s `tool_exposure` shows the server’s mode.

* Python

  ```python
  response = actions.mcp.create_config(
      name="support-agent",
      connection_tool_mappings=[
          McpConfigConnectionToolMapping(
              connection_name="github-connect",
              tools=["github_user_repos_list", "github_issues_list"],
          ),
      ],
      tool_exposure="META",
  )
  ```

* Node.js

  ```ts
  const { config } = await scalekit.actions.mcp.createConfig({
    name: 'support-agent',
    connectionToolMappings: [
      {
        connectionName: 'github-connect',
        tools: ['github_user_repos_list', 'github_issues_list'],
      },
    ],
    toolExposure: 'META',
  });
  ```

* cURL

  ```bash
  curl -sS -X POST "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "name": "support-agent",
      "connection_tool_mappings": [
        {"connection_name": "github-connect", "tools": ["github_user_repos_list", "github_issues_list"]}
      ],
      "tool_exposure": "META"
    }' |
    jq '.config | {id, name, tool_exposure}'
  ```

To switch an existing server, pass `tool_exposure` to the update call. Leave it out to keep the current mode. After a switch, MCP clients must list the server’s tools again; session tokens you already minted keep working. A switch to `DIRECT` fails while the server maps more than 25 connections or two of its connections map tools with the same name: remove mappings first.

### How an agent uses a META server

[Section titled “How an agent uses a META server”](#how-an-agent-uses-a-meta-server)

The agent finds a tool, reads its input schema, and runs it as the user:

1. `list_available_tools` with no arguments returns each connection on the server with its provider, `tool_count` and `readiness`. With a `connection_name`, it returns that connection’s tools a page at a time.
2. `search_available_tools` finds tools by keywords, such as `create issue`. Each match names its `connection_name`.
3. `get_tool_details` returns one tool’s `input_schema`. The agent calls it before `execute_tool`.
4. `execute_tool` runs the tool with `tool_name`, `connection_name` and `input`, using the user’s own connected account.

`readiness` tells the agent whether the user can run a connection’s tools now:

| `readiness`        | Meaning                                                                                                            |
| ------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `ready`            | The user’s connected account is `ACTIVE`.                                                                          |
| `needs_reauth`     | The user’s connected account is `EXPIRED`, so the user must authorize the app again.                               |
| `needs_connection` | The user has no `ACTIVE` or `EXPIRED` account for this connection, for example they never finished authorizing it. |

If `execute_tool` returns `not_connected` or `reauth_needed`, the error names the `connection_name`. The agent calls `connect_account` with that name, shows the user the authorization link, and retries after the user finishes. Other errors are `invalid_argument`, `not_found`, `forbidden` (a [read-only token](/agentkit/mcp/read-only-tokens/) calling a tool that writes) and `internal_error`.

Because the agent checks readiness itself, you can [mint a session token](/agentkit/mcp/session-tokens/) for a META server before the user has connected every app.

## Change or remove a server

[Section titled “Change or remove a server”](#change-or-remove-a-server)

To change the tools, send the full list for each connection. It replaces the old list.

* Python

  ```python
  actions.mcp.update_config(
      config_id=config_id,
      connection_tool_mappings=[
          McpConfigConnectionToolMapping(
              connection_name="github-connect",
              tools=["github_user_repos_list", "github_issues_list", "github_issue_get"],
          ),
      ],
  )
  ```

* Node.js

  ```ts
  await scalekit.actions.mcp.updateConfig({
    configId,
    connectionToolMappings: [
      {
        connectionName: 'github-connect',
        tools: ['github_user_repos_list', 'github_issues_list', 'github_issue_get'],
      },
    ],
  });
  ```

* cURL

  ```bash
  curl -sS -X PUT "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID" \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
      "connection_tool_mappings": [
        {"connection_name": "github-connect", "tools": ["github_user_repos_list", "github_issues_list", "github_issue_get"]}
      ]
    }' |
    jq '.config.connection_tool_mappings'
  ```

Change a server when no agent run is using it, because a run can lose a tool mid-conversation. For a larger change, create a new server and point your agent at its URL.

Deleting a server stops its URL from working immediately, including for session tokens already minted.

* Python

  ```python
  actions.mcp.delete_config(config_id=config_id)
  ```

* Node.js

  ```ts
  await scalekit.actions.mcp.deleteConfig(configId);
  ```

* cURL

  ```bash
  curl -sS -X DELETE "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID" \
    -H "Authorization: Bearer $TOKEN"
  ```

## Common problems

[Section titled “Common problems”](#common-problems)

### `invalid tool ''`

[Section titled “invalid tool '\'”](#invalid-tool-tool_name)

The tool name doesn’t exist on that connection. Copy names from tool search or the scoped tool list.

### `connection '' not found`

[Section titled “connection '\' not found”](#connection-connection_name-not-found)

The connection name doesn’t match **AgentKit** > **Connections** exactly.

### `config with name '' already exists`

[Section titled “config with name '\' already exists”](#config-with-name-name-already-exists)

Server names are unique in an environment. Choose another name, or update the existing server.

### `a DIRECT virtual MCP server supports at most 25 connections; use META for more`

[Section titled “a DIRECT virtual MCP server supports at most 25 connections; use META for more”](#a-direct-virtual-mcp-server-supports-at-most-25-connections-use-meta-for-more)

The server maps more than 25 connections. Set `tool_exposure` to `META`, or map fewer connections.

### `validation failed due to tool overlap for config ''`

[Section titled “validation failed due to tool overlap for config '\'”](#validation-failed-due-to-tool-overlap-for-config-name)

Two connections on a `DIRECT` server map tools with the same name. Remove one of the mappings, or set `tool_exposure` to `META`. A switch from `META` to `DIRECT` fails with `cannot switch config '' to DIRECT: its connections define overlapping tools` for the same reason.

## Next

[Section titled “Next”](#next)

[Mint session tokens](/agentkit/mcp/session-tokens/)Authorize a user's run and connect an MCP client.

[Choose a framework](/agentkit/examples/)Connect the server to CrewAI, Mastra, Claude Managed Agents and more.

---
# DOCUMENT BOUNDARY
---

# Connect any MCP client

> The HTTP contract of an AgentKit Virtual MCP server for clients that don't use the Scalekit SDK: URL, bearer token, transport, requests, responses, token lifetime and errors.

A Virtual MCP server, or a [connection’s MCP server](/agentkit/mcp/connection-mcp-servers/), is a standard MCP endpoint, so any client that speaks streamable HTTP can call it: an agent runtime, a hosted platform, your own HTTP code or `curl`. Your backend needs the Scalekit API only to mint session tokens; the client never needs the SDK. This page is the contract that client works against.

## The contract

[Section titled “The contract”](#the-contract)

|                 |                                                                                                                                                                                                                                                                                    |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Endpoint        | The server’s `mcp_server_url`, from [Create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/), or a connection’s MCP server URL, `/mcp/v3/connections/`. Send every request as `POST`.                                                  |
| Authentication  | `Authorization: Bearer `. A token is for one user on one server. [Mint session tokens](/agentkit/mcp/session-tokens/) shows how.                                                                                                                                    |
| Request headers | `Content-Type: application/json` and `Accept: application/json, text/event-stream`                                                                                                                                                                                                 |
| Transport       | MCP streamable HTTP. Each response is a server-sent event stream with one `message` event, whose `data` line is the JSON-RPC response.                                                                                                                                             |
| Session         | None to manage. `tools/list` and `tools/call` work without an `initialize` call first, and the server sends no `Mcp-Session-Id`. Clients that do send `initialize` and `notifications/initialized` get the normal responses.                                                       |
| Methods         | `initialize`, `tools/list` and `tools/call`. `GET` on the URL returns `405`: the server never opens a stream of its own.                                                                                                                                                           |
| Tools           | The tools you chose for the server, or every tool of the connection. A [read-only token](/agentkit/mcp/read-only-tokens/) gets only the read-only ones. Also `connect_account`: when the user still needs to connect an app, it returns an authorization link for that connection. |
| Token lifetime  | 1 hour by default, from 60 seconds to 24 hours. A token can’t be refreshed: mint a new one. Tokens are independent, so minting one doesn’t revoke the others.                                                                                                                      |

## Call the server

[Section titled “Call the server”](#call-the-server)

1. ## Mint a session token

   [Section titled “Mint a session token”](#mint-a-session-token)

   Your backend mints the token for the user, as in [Mint session tokens](/agentkit/mcp/session-tokens/#mint-the-token), and hands the client the server URL and the token. Keep them in `MCP_SERVER_URL` and `SESSION_TOKEN`.

2. ## List the tools

   [Section titled “List the tools”](#list-the-tools)

   ```bash
   curl -sS "$MCP_SERVER_URL" \
     -H "Authorization: Bearer $SESSION_TOKEN" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json, text/event-stream" \
     -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
   ```

   The response is one event. Its `data` line holds each tool’s `name`, `description` and `inputSchema`:

   ```text
   event: message
   data: {"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"connect_account","description":"Connect (or RE-connect) the user's account…","inputSchema":{…}},{"name":"github_user_repos_list",…}]}}
   ```

3. ## Call a tool

   [Section titled “Call a tool”](#call-a-tool)

   Send the tool’s `name` and `arguments` that match its `inputSchema`:

   ```bash
   curl -sS "$MCP_SERVER_URL" \
     -H "Authorization: Bearer $SESSION_TOKEN" \
     -H "Content-Type: application/json" \
     -H "Accept: application/json, text/event-stream" \
     -d '{
       "jsonrpc": "2.0",
       "id": 2,
       "method": "tools/call",
       "params": {"name": "github_user_repos_list", "arguments": {"per_page": 5}}
     }'
   ```

   The result is MCP text content. Its `text` is a JSON string, and the app’s response is under `data`:

   ```text
   event: message
   data: {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\n  \"data\": [ … ]\n}"}]}}
   ```

   To read it in a shell, keep the `data:` line and parse the text twice:

   ```bash
   … | sed -n 's/^data: //p' | jq -r '.result.content[0].text' | jq '.data'
   ```

## Connect a user’s account from the agent

[Section titled “Connect a user’s account from the agent”](#connect-a-users-account-from-the-agent)

If a tool call fails because the user hasn’t authorized that connection, or tools you expect are missing from `tools/list`, call `connect_account`:

1. Call it with no arguments. The response lists `connections_requiring_authorization` and `connections_already_authorized`, each with a status and a reason. Nothing is created, so this is always safe.
2. Call it again with `connection_name` set to one of the connections that needs authorization. The response has a time-limited `magic_link_url`.
3. Show the link to the user. The agent shouldn’t open it itself. Once the user signs in, retry the tool call with the same token.

A connection that uses an organization-wide credential has no user step: the response says `auth_mode` is `ORG_WIDE` and has no link.

## Renew the token

[Section titled “Renew the token”](#renew-the-token)

The client can’t renew a token. Before it expires, your backend mints a new one and the client switches to it. A host with an expired token fails at its next request, not at startup. [Choose when to mint](/agentkit/mcp/session-tokens/#choose-when-to-mint) covers single runs, chat sessions, scheduled jobs, concurrent runs and long-running hosts.

## Errors

[Section titled “Errors”](#errors)

A rejected token gets HTTP `401` with `{"error":"unauthorized","error_description":"invalid or expired bearer token"}`. Errors inside a request that got through come back as JSON-RPC errors in the `message` event.

| What you see                                           | Cause                                                                                                                   | Fix                                                                                                                                                                         |
| ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`, `invalid or expired bearer token`               | No `Authorization` header, an expired token, a token minted for another server, or a token that was copied incompletely | Mint a new token for this server and user.                                                                                                                                  |
| `405`                                                  | A `GET` request                                                                                                         | Send JSON-RPC requests as `POST`.                                                                                                                                           |
| JSON-RPC error `-32602` with `unknown tool`            | The tool isn’t on this server, the name is misspelled, or the token is read-only and the tool isn’t                     | Use a name from `tools/list`. To add a tool, [update the server](/agentkit/mcp/configure-mcp-server/#change-or-remove-a-server). For a tool that writes, mint a full token. |
| A tool call fails, or a connection’s tools are missing | The user hasn’t authorized that connection                                                                              | Call `connect_account`, as above.                                                                                                                                           |

## Next

[Section titled “Next”](#next)

[Mint session tokens](/agentkit/mcp/session-tokens/)Mint the token on your backend, and decide when to mint again.

[Choose a framework](/agentkit/examples/)Set up CrewAI, Mastra or Claude Managed Agents against the server.

---
# DOCUMENT BOUNDARY
---

# Use a connection's MCP server

> Every AgentKit connection has its own MCP server with all of the connection's tools. Find its URL, mint a session token for a user and connect any MCP client.

Every AgentKit [connection](/agentkit/concepts/) has its own MCP server. It serves all of that connection’s tools, and there is nothing to create: once the connection exists, so does its server. Mint a session token for a user, and any MCP client can call the connection’s tools as that user.

## Choose a connection MCP server or a Virtual MCP server

[Section titled “Choose a connection MCP server or a Virtual MCP server”](#choose-a-connection-mcp-server-or-a-virtual-mcp-server)

|               | Connection MCP server                                    | [Virtual MCP server](/agentkit/mcp/overview/)      |
| ------------- | -------------------------------------------------------- | -------------------------------------------------- |
| Tools         | Every tool of one connection                             | The tools you choose, from one or more connections |
| Setup         | None                                                     | Create it once per agent role                      |
| URL           | `/mcp/v3/connections/` | The `mcp_server_url` returned when you create it   |
| Session token | Minted for the connection                                | Minted for the server                              |

Use a connection MCP server when an agent works with one app and should see what that app offers. Use a Virtual MCP server when an agent needs a few tools from several apps, or you want to keep the tool list short. Both accept [read-only tokens](/agentkit/mcp/read-only-tokens/).

## Before you start

[Section titled “Before you start”](#before-you-start)

* An AgentKit [connection](/agentkit/connections/), and its name, such as `gmail`. The URL uses the name exactly as stored, including case.

* The user’s `identifier`: your app’s ID for that user, the same value you passed when they authorized the connection. [Choose an identifier](/agentkit/concepts/#choose-an-identifier) explains why it shouldn’t be an email address.

* A client set up as in the [Quickstart](/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`:

  ```bash
  TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
    -d grant_type=client_credentials \
    -d client_id="$SCALEKIT_CLIENT_ID" \
    -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token)
  ```

1. ## Get the server URL

   [Section titled “Get the server URL”](#get-the-server-url)

   In the dashboard, go to **AgentKit** > **Connections**, open the connection, and copy **MCP Server URL** from the **Configuration** tab. It has this form:

   ```text
   https:///mcp/v3/connections/gmail
   ```

   You can also build it from your environment URL and the connection name. The path is case-sensitive, so use the name exactly as it appears in the dashboard.

2. ## Mint a session token

   [Section titled “Mint a session token”](#mint-a-session-token)

   Mint a token for the user before each run. Set `expiry` a little above how long the run takes: between 60 seconds and 24 hours, one hour by default.

   * Python

     ```python
     from datetime import timedelta


     session = actions.mcp.create_session_token(
         connection_name="gmail",
         identifier="user_123",
         expiry=timedelta(minutes=15),
     )
     session_token = session.token
     ```

   * Node.js

     ```ts
     const session = await scalekit.actions.mcp.createSessionToken({
       connectionName: 'gmail',
       identifier: 'user_123',
       expirySeconds: 900,
     });
     const sessionToken = session.token;
     ```

   * cURL

     ```bash
     SESSION_TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/connections/gmail/tokens" \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"identifier": "user_123", "expiry": "900s"}' |
       jq -r .token)
     ```

   The token works only on this connection’s server. To limit the agent to tools that don’t change anything, add `access_level="READ_ONLY"` (Python) or `accessLevel: 'READ_ONLY'` (Node.js): see [Limit an agent to read-only tools](/agentkit/mcp/read-only-tokens/).

3. ## Connect the MCP client

   [Section titled “Connect the MCP client”](#connect-the-mcp-client)

   Give your MCP client the server URL and the header `Authorization: Bearer `. Most frameworks take them as the server’s URL and headers. [Mint session tokens](/agentkit/mcp/session-tokens/#connect-the-mcp-client) shows the MCP SDK clients in Python and Node.js, and [Connect any MCP client](/agentkit/mcp/connect-any-client/) has the HTTP contract for everything else.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

List the tools with the token:

```bash
curl -sS "$SCALEKIT_ENVIRONMENT_URL/mcp/v3/connections/gmail" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' |
  sed -n 's/^data: //p' | jq -r '.result.tools[].name'
```

You see the connection’s tools, such as `gmail_fetch_mails`, and `connect_account`. If the user hasn’t authorized the connection yet, the agent calls `connect_account` to get an authorization link for them, as in [Connect a user’s account from the agent](/agentkit/mcp/connect-any-client/#connect-a-users-account-from-the-agent).

## Common problems

[Section titled “Common problems”](#common-problems)

### `404` when minting the token

[Section titled “404 when minting the token”](#404-when-minting-the-token)

No active connection has that name in this environment. Check the name in **AgentKit** > **Connections**, and that the connection is enabled.

### `400` when minting the token

[Section titled “400 when minting the token”](#400-when-minting-the-token)

The identifier or `expiry` is invalid, the connection isn’t an AgentKit connection, or the user has no active account on the connection. In the last case, [send the user an authorization link](/agentkit/tools/authorize/) and mint again once the account is `ACTIVE`.

### `401` with `invalid or expired bearer token`

[Section titled “401 with invalid or expired bearer token”](#401-with-invalid-or-expired-bearer-token)

The token expired, was minted for a different server, or the URL’s connection name differs from the stored name, for example in case. Copy the URL from the dashboard and mint a new token for this connection.

## Next

[Section titled “Next”](#next)

[Limit an agent to read-only tools](/agentkit/mcp/read-only-tokens/)Mint a token that can only read, on any MCP server.

[Mint session tokens](/agentkit/mcp/session-tokens/)Decide when to mint for scheduled, concurrent and long-running agents.

---
# DOCUMENT BOUNDARY
---

# Limit an agent to read-only tools

> Mint a read-only session token so an agent sees and calls only the tools that don't change anything, on a Virtual MCP server or a connection's MCP server.

A read-only session token gives an agent only the tools that read data, such as listing, searching and fetching. Tools that send, create, update or delete are left out of the agent’s tool list and refused if it calls them. You set this when you mint the token, for a [Virtual MCP server](/agentkit/mcp/overview/) or a [connection’s MCP server](/agentkit/mcp/connection-mcp-servers/), with no change to the server.

## Why use one

[Section titled “Why use one”](#why-use-one)

* **Least privilege per run.** An agent that summarizes a user’s inbox or answers questions about their CRM never needs to send an email or edit a record. A read-only token gives it only what the task needs.
* **Prompt injection can’t trigger a write.** Agents read content written by other people: emails, tickets, documents. If that content tells the agent to forward a thread or delete a file, the agent has no tool to do it with. Write tools aren’t in its tool list, and a call to one is refused.
* **One field, not a separate server.** You don’t build and maintain a second, read-only server for each agent. The same server serves full and read-only runs, and the token decides.
* **Different access per run.** Mint a read-only token for a scheduled digest and a full token for a run where the user has approved an action. Tokens are independent, so one run’s access never changes another’s.
* **Same user isolation.** A read-only token is still bound to one user and one server, so the agent reads only that user’s data, through their own connected accounts.

## How it works

[Section titled “How it works”](#how-it-works)

Tools are marked by what they do. On a connector’s page, such as [Gmail](/agentkit/connectors/gmail/), each tool is labeled **Read-only**, **Write** or **Destructive**. A tool without a read-only marking counts as a write. With a read-only token:

| The agent                  | What happens                                                                                                             |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Lists tools (`tools/list`) | Only tools marked read-only are returned, plus `connect_account`, so the agent can still ask the user to connect an app. |
| Calls a read-only tool     | The call runs as the user, as with a full token.                                                                         |
| Calls any other tool       | The call is refused with JSON-RPC error `-32602`, `unknown tool ""`. Nothing reaches the app.                      |

On a Virtual MCP server, the token gets the read-only tools among the tools you chose for that server. On a connection’s MCP server, it gets the read-only tools of that connection.

Tools from an [MCP connector you bring](/agentkit/bring-your-own-connector/overview/) are read-only when the upstream MCP server marks them that way, with `readOnlyHint`. A tool without that marking is left out of a read-only token.

## Before you start

[Section titled “Before you start”](#before-you-start)

* A [Virtual MCP server](/agentkit/mcp/configure-mcp-server/) and its ID, or a [connection’s MCP server](/agentkit/mcp/connection-mcp-servers/) and the connection name.
* The user’s `identifier` and a client set up as in [Mint session tokens](/agentkit/mcp/session-tokens/#before-you-start).

1. ## Mint a read-only token

   [Section titled “Mint a read-only token”](#mint-a-read-only-token)

   Add the access level when you mint the token. Use `READ_ONLY` in capitals: the value is case-sensitive.

   For a Virtual MCP server:

   * Python

     ```python
     from datetime import timedelta


     session = actions.mcp.create_session_token(
         mcp_config_id=config_id,
         identifier="user_123",
         expiry=timedelta(minutes=15),
         access_level="READ_ONLY",
     )
     session_token = session.token
     ```

   * Node.js

     ```ts
     const session = await scalekit.actions.mcp.createSessionToken({
       mcpConfigId: configId,
       identifier: 'user_123',
       expirySeconds: 900,
       accessLevel: 'READ_ONLY',
     });
     const sessionToken = session.token;
     ```

   * cURL

     ```bash
     SESSION_TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID/tokens" \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"identifier": "user_123", "expiry": "900s", "access_level": "READ_ONLY"}' |
       jq -r .token)
     ```

   For a connection’s MCP server:

   * Python

     ```python
     from datetime import timedelta


     session = actions.mcp.create_session_token(
         connection_name="gmail",
         identifier="user_123",
         expiry=timedelta(minutes=15),
         access_level="READ_ONLY",
     )
     session_token = session.token
     ```

   * Node.js

     ```ts
     const session = await scalekit.actions.mcp.createSessionToken({
       connectionName: 'gmail',
       identifier: 'user_123',
       expirySeconds: 900,
       accessLevel: 'READ_ONLY',
     });
     const sessionToken = session.token;
     ```

   * cURL

     ```bash
     SESSION_TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/connections/gmail/tokens" \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"identifier": "user_123", "expiry": "900s", "access_level": "READ_ONLY"}' |
       jq -r .token)
     ```

   Leave the access level out, or set it to `FULL`, for a token with every tool the server serves.

2. ## Connect the agent

   [Section titled “Connect the agent”](#connect-the-agent)

   Pass the token to the MCP client as `Authorization: Bearer `, exactly as with a full token. [Mint session tokens](/agentkit/mcp/session-tokens/#connect-the-mcp-client) shows the MCP SDK clients.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

List the tools with the read-only token, then with a full one, and compare:

```bash
curl -sS "$MCP_SERVER_URL" \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}' |
  sed -n 's/^data: //p' | jq -r '.result.tools[].name'
```

The read-only list has only tools that read, such as `gmail_fetch_mails`, plus `connect_account`. Tools such as `gmail_send_message` appear only with the full token.

To see which tools a read-only token gets before you mint it, read each tool’s `read_only_hint` from the tool list:

* Python

  ```python
  from google.protobuf.json_format import MessageToDict


  page, _ = actions.tools.list_scoped_tools(
      identifier="user_123",
      filter={"connection_names": ["gmail"]},
      page_size=100,
  )
  definitions = [MessageToDict(t.tool)["definition"] for t in page.tools]
  print([d["name"] for d in definitions if d.get("annotations", {}).get("read_only_hint") is True])
  ```

* Node.js

  ```ts
  const page = await scalekit.tools.listScopedTools('user_123', {
    filter: { connectionNames: ['gmail'] },
    pageSize: 100,
  });
  const readOnly = page.tools
    .map((t) => t.tool?.definition as { name?: string; annotations?: { read_only_hint?: boolean } } | undefined)
    .filter((d) => d?.annotations?.read_only_hint === true)
    .map((d) => d?.name);
  console.log(readOnly);
  ```

## Common problems

[Section titled “Common problems”](#common-problems)

### `unknown tool ""` when the agent calls a tool

[Section titled “unknown tool "\" when the agent calls a tool”](#unknown-tool-name-when-the-agent-calls-a-tool)

The tool isn’t read-only, so a read-only token doesn’t include it. If the agent needs it for this run, mint a full token. Otherwise, tell the agent in its instructions that it can only read.

### `400` with `access_level` when minting

[Section titled “400 with access_level when minting”](#400-with-access_level-when-minting)

The value must be exactly `FULL` or `READ_ONLY`, in capitals. `read_only` and other spellings are rejected.

### `tools/list` returns only `connect_account`

[Section titled “tools/list returns only connect_account”](#toolslist-returns-only-connect_account)

None of the tools on this server are read-only. On a Virtual MCP server, [add read-only tools](/agentkit/mcp/configure-mcp-server/#change-or-remove-a-server), such as list and search tools, or mint a full token.

## Next

[Section titled “Next”](#next)

[Mint session tokens](/agentkit/mcp/session-tokens/)Decide when to mint for scheduled, concurrent and long-running agents.

[Security and compliance](/agentkit/security/)What Scalekit protects, and what your app is responsible for.

---
# DOCUMENT BOUNDARY
---

# Mint session tokens

> Mint a session token so an agent can call a Virtual MCP server's tools as one user, and decide when to mint again for scheduled and long-running agents.

A session token lets an MCP client call a Virtual MCP server’s tools as one user. Mint one before each run: check that the user’s connections are active, mint the token, and pass it to the agent as a bearer token.

This page uses a Virtual MCP server. For a [connection’s own MCP server](/agentkit/mcp/connection-mcp-servers/), you mint the token for the connection instead; everything else is the same.

## Before you start

[Section titled “Before you start”](#before-you-start)

* A [Virtual MCP server](/agentkit/mcp/configure-mcp-server/) and its ID. For cURL, keep the ID in `CONFIG_ID` and the URL in `MCP_SERVER_URL`.

* The user’s `identifier`: your app’s ID for that user, the same value you passed when they authorized their connections. Use an ID that’s stable and hard to guess, not an email address. [Choose an identifier](/agentkit/concepts/#choose-an-identifier) explains why.

* A client set up as in the [Quickstart](/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`:

  ```bash
  TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
    -d grant_type=client_credentials \
    -d client_id="$SCALEKIT_CLIENT_ID" \
    -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token)
  ```

1. ## Check the user’s connections

   [Section titled “Check the user’s connections”](#check-the-users-connections)

   List the user’s connected account for each connection on the server. For any that isn’t `ACTIVE`, send the user its authorization link before the run.

   On a server whose `tool_exposure` is `META`, this check is optional: the agent sees each connection’s readiness and can call `connect_account` to get the link. See [How an agent uses a META server](/agentkit/mcp/configure-mcp-server/#how-an-agent-uses-a-meta-server).

   * Python

     ```python
     response = actions.mcp.list_mcp_connected_accounts(
         config_id=config_id,
         identifier="user_123",
         include_auth_link=True,
     )
     not_ready = [a for a in response.connected_accounts if a.connected_account_status != "ACTIVE"]
     for account in not_ready:
         print(f"{account.connection_name} is {account.connected_account_status}: {account.authentication_link}")
     ```

   * Node.js

     ```ts
     const { connectedAccounts } = await scalekit.actions.mcp.listConnectedAccounts({
       configId,
       identifier: 'user_123',
       includeAuthLink: true,
     });
     const notReady = connectedAccounts.filter((a) => a.connectedAccountStatus !== 'ACTIVE');
     for (const a of notReady) {
       console.log(`${a.connectionName} is ${a.connectedAccountStatus}: ${a.authenticationLink}`);
     }
     ```

   * cURL

     ```bash
     curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID/connected_accounts" \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"identifier": "user_123", "include_auth_link": true}' |
       jq '.connected_accounts[] | select(.connected_account_status != "ACTIVE") | {connection_name, connected_account_status, authentication_link}'
     ```

   If the user has no account yet for a connection, this call creates one in `PENDING_AUTH` and returns its link.

2. ## Mint the token

   [Section titled “Mint the token”](#mint-the-token)

   Set `expiry` a little above how long the run takes. It must be between 60 seconds and 24 hours, and defaults to one hour. If the run only needs to read, add `access_level="READ_ONLY"` (Python) or `accessLevel: 'READ_ONLY'` (Node.js): see [Limit an agent to read-only tools](/agentkit/mcp/read-only-tokens/).

   * Python

     ```python
     from datetime import timedelta


     session = actions.mcp.create_session_token(
         mcp_config_id=config_id,
         identifier="user_123",
         expiry=timedelta(minutes=30),
     )
     session_token = session.token
     print(session.expires_at)
     ```

   * Node.js

     ```ts
     const session = await scalekit.actions.mcp.createSessionToken({
       mcpConfigId: configId,
       identifier: 'user_123',
       expirySeconds: 30 * 60,
     });
     const sessionToken = session.token;
     console.log(new Date(Number(session.expiresAt?.seconds) * 1000));
     ```

   * cURL

     ```bash
     SESSION_TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/mcp/configs/$CONFIG_ID/tokens" \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"identifier": "user_123", "expiry": "1800s"}' |
       jq -r .token)
     ```

3. ## Connect the MCP client

   [Section titled “Connect the MCP client”](#connect-the-mcp-client)

   Pass the server URL and `Authorization: Bearer ` to your MCP client. Most frameworks take them as the server’s URL and headers. With the MCP SDKs, or directly over HTTP:

   * Python

     ```python
     1
     import asyncio
     2


     3
     import httpx2
     4
     from mcp import Client
     5
     from mcp.client.streamable_http import streamable_http_client
     6


     7


     8
     async def list_tools():
     9
         headers = {"Authorization": f"Bearer {session_token}"}
     10
         async with httpx2.AsyncClient(headers=headers, timeout=30) as http_client:
     11
             async with Client(streamable_http_client(mcp_server_url, http_client=http_client)) as mcp_client:
     12
                 result = await mcp_client.list_tools()
     13
                 print([tool.name for tool in result.tools])
     14


     15


     16
     asyncio.run(list_tools())
     ```

   * Node.js

     ```ts
     import { Client } from '@modelcontextprotocol/sdk/client/index.js';
     import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';


     const transport = new StreamableHTTPClientTransport(new URL(mcpServerUrl), {
       requestInit: { headers: { Authorization: `Bearer ${sessionToken}` } },
     });
     const mcpClient = new Client({ name: 'repo-assistant', version: '1.0.0' });
     await mcpClient.connect(transport);
     const { tools } = await mcpClient.listTools();
     console.log(tools.map((tool) => tool.name));
     await mcpClient.close();
     ```

   * cURL

     ```bash
     curl -sS "$MCP_SERVER_URL" \
       -H "Authorization: Bearer $SESSION_TOKEN" \
       -H "Content-Type: application/json" \
       -H "Accept: application/json, text/event-stream" \
       -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
     ```

   The Python example uses `mcp` 2.x (`pip install "mcp>=2"`), and the Node.js example uses `@modelcontextprotocol/sdk`. For clients without an MCP SDK, [Connect any MCP client](/agentkit/mcp/connect-any-client/) has the full HTTP contract, including `tools/call` and the response format. [Choose a framework](/agentkit/examples/) shows the setup for CrewAI, Mastra and Claude Managed Agents.

## Choose when to mint

[Section titled “Choose when to mint”](#choose-when-to-mint)

A token can’t be refreshed, so decide when your code mints a new one:

| Your agent                              | When to mint                                                                                                                                               |
| --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Runs once, for minutes                  | Once, right before the run. Set `expiry` above the longest run you expect.                                                                                 |
| Chat session with a user                | When the session starts. Mint again if the session outlives the token, or when a call returns `401`.                                                       |
| Scheduled job, such as a daily briefing | At the start of each run. Don’t store a token between runs.                                                                                                |
| Many runs at once                       | One token per run. Tokens are independent, and minting a new one doesn’t revoke the others.                                                                |
| Long-running host or gateway            | On a schedule, before `expires_at`. Give the host the new token, then reload it. A host with an expired token fails at its next tool call, not at startup. |

A token lasts at most 24 hours, so a client that can’t update its headers, such as a config file you edit by hand, stops working within a day.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

`tools/list` returns the tools you chose for the server, and a tool call returns the app’s response under `data`.

## Common problems

[Section titled “Common problems”](#common-problems)

### `401` with `invalid or expired bearer token`

[Section titled “401 with invalid or expired bearer token”](#401-with-invalid-or-expired-bearer-token)

The token expired, is for a different server, or was copied incompletely. Mint a new token for this server.

### `expiry must be between 60s and 24h`

[Section titled “expiry must be between 60s and 24h”](#expiry-must-be-between-60s-and-24h)

Set `expiry` between 60 seconds and 24 hours. In REST, send it in seconds, such as `"1800s"`.

### `unknown tool ""` with a read-only token

[Section titled “unknown tool "\" with a read-only token”](#unknown-tool-name-with-a-read-only-token)

A [read-only token](/agentkit/mcp/read-only-tokens/) includes only the server’s read-only tools. Mint a full token if the run needs that tool.

### A tool call fails because the account isn’t connected

[Section titled “A tool call fails because the account isn’t connected”](#a-tool-call-fails-because-the-account-isnt-connected)

The user’s account for that connection isn’t `ACTIVE`. Run the check in step 1 and send the authorization link. The agent can also call `connect_account` to get the link itself.

## Next

[Section titled “Next”](#next)

[Limit an agent to read-only tools](/agentkit/mcp/read-only-tokens/)Mint a token that sees and calls only read-only tools.

[Connect any MCP client](/agentkit/mcp/connect-any-client/)The HTTP contract for clients that don't use an MCP SDK.

[Choose a framework](/agentkit/examples/)Run an agent against the server in your framework.

[Verify users](/agentkit/user-verification/)Confirm who authorized each connection before production.

---
# DOCUMENT BOUNDARY
---

# Security and compliance

> How AgentKit protects user credentials: per-environment encryption keys in Google Cloud KMS, what Scalekit stores and for how long, regions and certifications.

AgentKit holds your users’ OAuth tokens and API keys, and makes API calls with them on your agent’s behalf. This page explains how Scalekit protects those credentials and the data that passes through a tool call, and what your app is responsible for. Use it for a security review, or as a checklist before you onboard real users.

## Certifications

[Section titled “Certifications”](#certifications)

* **SOC 2 Type II** and **ISO 27001** certified.
* **GDPR** and **CCPA** compliant. The [Data Processing Agreement](https://www.scalekit.com/legal/data-processing-agreement) includes Standard Contractual Clauses and lists sub-processors.
* **[HIPAA](https://www.scalekit.com/legal/hipaa) eligible**, with a Business Associate Agreement on Enterprise plans.

Audit reports and penetration test summaries are available under NDA from the [Trust Center](https://www.scalekit.com/trust-center).

## How credentials are stored

[Section titled “How credentials are stored”](#how-credentials-are-stored)

Every credential a user connects is encrypted before it’s written to the database, whether it’s an OAuth token or a value entered in the [Hosted Connect Widget](/agentkit/hosted-connect-widget/), such as an API key or a username and password:

* **A key per environment.** Scalekit encrypts credentials with AES-256-GCM under a data encryption key that belongs to one environment. No two environments share a key.
* **Keys wrapped by Google Cloud KMS.** Each environment’s key is itself encrypted by a master key in Google Cloud KMS, which is held apart from the database and the application. A copy of the database alone yields nothing usable.
* **Scoped by environment.** Every read and write of stored credentials is filtered by environment, so one environment’s context can’t reach another’s credentials.
* **Rotation without downtime.** When a key rotates, new values are encrypted with the new key and Scalekit re-encrypts existing values in the background, so existing credentials keep working. To re-encrypt existing data right away, use **Re-encrypt Data** on [Encryption keys](/agentkit/encryption-keys/).
* **Your own key, optionally.** Hold the master key in your own Google Cloud KMS. Revoking Scalekit’s access to it makes the stored credentials unusable to Scalekit. See [Encryption keys](/agentkit/encryption-keys/).

By default, Scalekit doesn’t return a user’s raw tokens in API responses. Your agent calls tools with `execute_tool` or the [API proxy](/agentkit/tools/custom-tools/), and Scalekit adds the token to the outgoing request. If your app needs to call an app with the tokens itself, Scalekit support can turn on credential access for an environment. See [Get a connected account’s credentials](/agentkit/reference/connected-accounts/get-connected-account-credentials/).

## What Scalekit stores

[Section titled “What Scalekit stores”](#what-scalekit-stores)

* **Users’ OAuth tokens and API keys.** Stored: Yes, encrypted as above. Kept for: Until the connected account is deleted.
* **Connection settings, including your OAuth app’s client secret.** Stored: Yes. Kept for: Until the connection is deleted.
* **Tool call responses.** Stored: No. Returned to your agent and not kept. Kept for: Not stored.
* **Tool call inputs.** Stored: No. Sent to the app and not kept. Kept for: Not stored.
* **Tool call logs.** Stored: Tool name, connection, connected account, identifier, status, error code, duration and time. Also the app’s error response, when **Store connector error details** is on. Kept for: 90 days.
* **Your API client secrets.** Stored: A one-way hash. The secret is shown once, when you create it. Kept for: Until you delete it.

Tool call logs show in **AgentKit** > **Logs**. They never contain a tool’s successful response. When a call fails, they keep Scalekit’s own error message, and keep the error the app returned only when **Store connector error details** is on in **AgentKit** > **Logs**. That error can include data from the app, so leave the setting off unless you’re debugging a connector.

Tool call logs also contain the user’s identifier, so use an internal user ID rather than an email address. See [Choose an identifier](/agentkit/concepts/#choose-an-identifier).

## Where data is hosted

[Section titled “Where data is hosted”](#where-data-is-hosted)

Scalekit runs two independent regions on Google Cloud, with no shared application state:

| Region | Location                   |
| ------ | -------------------------- |
| US     | `us-west2` (Los Angeles)   |
| EU     | `europe-west3` (Frankfurt) |

You choose the region when you sign up, and your workspace’s data stays in it. See [Environments and regions](/agentkit/environments/).

To keep credentials, tool calls and logs inside your own infrastructure, [self-host AgentKit](/agentkit/self-hosted/) with an enterprise license.

## In transit

[Section titled “In transit”](#in-transit)

All Scalekit endpoints require HTTPS with TLS 1.3. If an app only accepts traffic from known addresses, allow Scalekit’s [outbound IP addresses](/reference/outbound-ip-addresses/) for your region.

## What your app is responsible for

[Section titled “What your app is responsible for”](#what-your-app-is-responsible-for)

Scalekit protects credentials once a user connects. Your app decides who connects and who can act as whom:

* **Verify users in production.** Set **AgentKit** > **Settings** > **User Verification** to **Custom user verifier**, so the person who approves access is the user your app meant. With **None**, anyone who opens an authorization link activates the account. See [Verify users](/agentkit/user-verification/).
* **Use a stable, internal identifier.** Pass your app’s user ID, never an email address or anything a user can choose. Take it from your server-side session, not from the request body, so one user can’t act as another.
* **Keep one user’s credentials with that user.** When you hand a credential to an agent runtime, such as a [Virtual MCP session token](/agentkit/mcp/session-tokens/), store it per user and never share it across users.
* **Keep API credentials on the server.** Load your client ID and secret from environment variables or a secret manager, never from browser or mobile code. Rotate a secret by creating a new one before you delete the old one. See [API credentials](/agentkit/api-credentials/).
* **Request only the scopes you need.** A narrower scope limits what a leaked token can do. See [Configure scopes](/agentkit/connections/#configure-scopes).
* **Give agents only the tools they need.** Start with read-only tools, and ask the user before a destructive call. Tool definitions carry `read_only_hint` and `destructive_hint` for this. See [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/).
* **Verify webhook signatures.** Scalekit signs every webhook. Check the signature before you trust a payload. See [Verify each request](/agentkit/account-events/#verify-each-request-and-handle-the-event).
* **Limit who can use the dashboard.** Give teammates the least access their work needs. See [Team members and roles](/agentkit/team-members/).

## Next

[Section titled “Next”](#next)

[Launch checklist](/agentkit/advanced/launch-checklist/)Everything to check before real users connect.

[Verify users](/agentkit/user-verification/)Confirm the person who connects is the user your app intended.

---
# DOCUMENT BOUNDARY
---

# Self-host AgentKit

> Run AgentKit on Kubernetes or Docker in your own infrastructure, with no connection to Scalekit. What changes for connectors, OAuth apps, network and SDKs.

AgentKit runs in your own infrastructure with an enterprise license, on Kubernetes or Docker. Connections, connected accounts, the token vault, tool execution and Virtual MCP servers all run there. Once it’s installed, your instance has no connection to Scalekit’s infrastructure: credentials, tool calls and logs stay in your network.

Use it when credentials and tool calls must stay in your network for data residency, compliance or network isolation. To get access, [talk to an engineer](https://scalekit.com/demo). Installation, configuration and upgrade guides come with your license.

## What you provide

[Section titled “What you provide”](#what-you-provide)

| Dependency                               | Requirement                                                     |
| ---------------------------------------- | --------------------------------------------------------------- |
| Kubernetes (for a Kubernetes deployment) | 1.27 or later, managed or self-managed, with Helm 3.12 or later |
| Ingress (for a Kubernetes deployment)    | Kubernetes Gateway API or the nginx ingress controller          |
| PostgreSQL                               | 15 or later (CockroachDB is also supported)                     |
| Redis                                    | 6.2 or later                                                    |
| SMTP                                     | Any provider, for team invitations and sign-in email            |
| Domain                                   | A domain and TLS certificate for your instance                  |

## Network access

[Section titled “Network access”](#network-access)

No traffic goes to Scalekit, but tools still call the apps they connect to. Your instance calls each connector’s API and OAuth token endpoint directly, so allow outbound HTTPS from your instance to the providers you use, such as Google, Slack or Salesforce. Your users’ browsers must reach the provider’s consent screen when they authorize a connected account.

A connector works only if your instance can reach its provider. Connectors for apps that run inside your network, including [your own connectors](/agentkit/bring-your-own-connector/overview/), need no internet access.

## OAuth apps

[Section titled “OAuth apps”](#oauth-apps)

Scalekit’s own OAuth credentials, offered as **Use Scalekit credentials** when you create a connection, belong to Scalekit’s cloud and aren’t available on a self-hosted instance. Create an OAuth app with each provider and use it for the connection, as in [Use your own OAuth app](/agentkit/advanced/bring-your-own-oauth/). Register the redirect URI your instance shows in the connection form, which is on your own domain.

## Point your app at your instance

[Section titled “Point your app at your instance”](#point-your-app-at-your-instance)

The SDKs and the REST API work the same way. Set `SCALEKIT_ENVIRONMENT_URL` to your instance’s environment URL, and use a client ID and secret from **API credentials** in your instance’s dashboard at `https://app.`. See [API credentials](/agentkit/api-credentials/).

## Next

[Section titled “Next”](#next)

[Security and compliance](/agentkit/security/)How credentials are stored, and what your app is responsible for.

[Launch checklist](/agentkit/advanced/launch-checklist/)What to check before real users connect accounts.

---
# DOCUMENT BOUNDARY
---

# Team members and roles

> Invite teammates to your Scalekit workspace, give them the Admin, Developer, Member or No Access role, override a role per environment, or create custom roles.

Invite your team to the Scalekit dashboard and give each person a role that matches their work. A role applies to every environment in the workspace unless you override it for one environment.

## Before you start

[Section titled “Before you start”](#before-you-start)

* The **Admin** role. Other roles can see the team but can’t change it.

## Roles

[Section titled “Roles”](#roles)

| Role          | For                                                                        |
| ------------- | -------------------------------------------------------------------------- |
| **Admin**     | Full access to everything, including members, roles and billing.           |
| **Developer** | Build and configure AgentKit. Can’t manage members, roles or billing.      |
| **Member**    | Read-only access to settings and logs.                                     |
| **No Access** | Can’t open the dashboard. Use it to suspend someone without removing them. |

What each role can do in AgentKit:

| Area                                               | Admin | Developer | Member |
| -------------------------------------------------- | ----- | --------- | ------ |
| Connections: view                                  | Yes   | Yes       | Yes    |
| Connections: create and edit                       | Yes   | Yes       | No     |
| Run tools in the **Playground**                    | Yes   | No        | No     |
| Connected accounts: view and create                | Yes   | Yes       | No     |
| Connected accounts: view credentials and revoke    | Yes   | No        | No     |
| API credentials: view, generate and delete secrets | Yes   | Yes       | No     |
| Tool call logs                                     | Yes   | Yes       | Yes    |
| Webhooks: view                                     | Yes   | Yes       | Yes    |
| Webhooks: create and edit                          | Yes   | Yes       | No     |
| Environments: create                               | Yes   | No        | No     |
| Environments: rename                               | Yes   | Yes       | No     |
| Billing                                            | Yes   | No        | No     |
| Team, roles and environment access: view           | Yes   | Yes       | Yes    |
| Team, roles and environment access: change         | Yes   | No        | No     |

**No Access** can do none of these.

## Invite a teammate

[Section titled “Invite a teammate”](#invite-a-teammate)

1. Open **Workspace** > **Team Members** and select **Invite**.

2. Enter their **Email**, choose a **Role**, and select **Invite**.

The person appears in **Team Members** with the role you chose.

## Change a role

[Section titled “Change a role”](#change-a-role)

In **Team Members**, open the member’s menu and select **Edit role**. Choose a new **Workspace role** and select **Save**.

### Give a different role in one environment

[Section titled “Give a different role in one environment”](#give-a-different-role-in-one-environment)

Under **Environment access** in the same drawer, select **Set override** next to an environment and choose the role for that environment only. For example, make someone an Admin in Development and a Member in Production. **Reset to Default** removes the override.

## Remove a teammate

[Section titled “Remove a teammate”](#remove-a-teammate)

In **Team Members**, open the member’s menu, select **Remove Member**, and confirm with **Delete**. To block access but keep the person’s record, give them **No Access** instead.

## Create a custom role

[Section titled “Create a custom role”](#create-a-custom-role)

When the four roles don’t fit, open **Workspace** > **Roles** and select **Create role**. Give it a **Role name** and **Description**, then choose its **Permissions**, or start from a preset such as **Support** or **Billing Owner**. You can only grant permissions your own role has.

Custom roles appear in the **Role** list when you invite or edit a member.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

Ask the teammate to sign in. They see only the pages their role allows: for example, a Member doesn’t see **API Credentials**, and a Developer doesn’t see **Billing**.

## Common problems

[Section titled “Common problems”](#common-problems)

### I don’t see **Invite** or **Edit role**

[Section titled “I don’t see Invite or Edit role”](#i-dont-see-invite-or-edit-role)

Your role can’t change the team. Ask an Admin.

### A Developer can’t run a tool in the Playground

[Section titled “A Developer can’t run a tool in the Playground”](#a-developer-cant-run-a-tool-in-the-playground)

Running tools uses a user’s live credentials, so only Admins can. Give the person the Admin role in a Development environment with an environment override, or create a custom role that includes it.

## Next

[Section titled “Next”](#next)

[Environments and regions](/agentkit/environments/)Development and Production, and how to create Production.

[Security and compliance](/agentkit/security/)What Scalekit protects, and what your app is responsible for.

---
# DOCUMENT BOUNDARY
---

# Authorize a user

> Generate an authorization link, send it to your user, and confirm their connected account is active before your agent executes tools.

Once a connection is configured, your users need to grant your agent access to their account. This happens once per user per connection. Scalekit stores their tokens and keeps them fresh automatically.

The flow is:

1. Create a connected account for the user
2. Generate an authorization link and send it to the user
3. The user completes the OAuth consent screen
4. The connected account becomes `ACTIVE`. Your agent can now execute tools.

## Create a connected account and generate a link

[Section titled “Create a connected account and generate a link”](#create-a-connected-account-and-generate-a-link)

These samples use `actions`, set up as in the [Quickstart](/agentkit/quickstart/). The first block shows the setup; later blocks reuse it.

* Python

  ```python
  1
  import os
  2


  3
  from dotenv import load_dotenv
  4
  from scalekit import ScalekitClient
  5
  from scalekit.common.exceptions import ScalekitServerException
  6


  7
  load_dotenv()
  8


  9
  scalekit_client = ScalekitClient(
  10
      env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
  11
      client_id=os.environ["SCALEKIT_CLIENT_ID"],
  12
      client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
  13
  )
  14
  actions = scalekit_client.actions
  15


  16
  try:
  17
      # Create or retrieve the connected account for this user
  18
      response = actions.get_or_create_connected_account(
  19
          connection_name="github-connect",
  20
          identifier="user_123"  # your app's unique user ID
  21
      )
  22
      connected_account = response.connected_account
  23


  24
      # Generate the authorization link if the account is not yet active
  25
      if connected_account.status != "ACTIVE":
  26
          link_response = actions.get_authorization_link(
  27
              connection_name="github-connect",
  28
              identifier="user_123"
  29
          )
  30
          auth_url = link_response.link
  31
          # Redirect or send auth_url to the user
  32
  except ScalekitServerException as e:
  33
      # For example, the connection name doesn't match the dashboard
  34
      print("Scalekit error:", e.error_code, e.message)
  ```

* Node.js

  ```typescript
  1
  import 'dotenv/config';
  2
  import { ConnectorStatus, ScalekitClient, ScalekitServerException } from '@scalekit-sdk/node';
  3


  4
  const scalekit = new ScalekitClient(
  5
    process.env.SCALEKIT_ENVIRONMENT_URL!,
  6
    process.env.SCALEKIT_CLIENT_ID!,
  7
    process.env.SCALEKIT_CLIENT_SECRET!,
  8
  );
  9
  const actions = scalekit.actions;
  10


  11
  try {
  12
    // Create or retrieve the connected account for this user
  13
    const response = await actions.getOrCreateConnectedAccount({
  14
      connectionName: 'github-connect',
  15
      identifier: 'user_123',  // your app's unique user ID
  16
    });
  17


  18
    const connectedAccount = response.connectedAccount;
  19


  20
    // Generate the authorization link if the account is not yet active
  21
    if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
  22
      const linkResponse = await actions.getAuthorizationLink({
  23
        connectionName: 'github-connect',
  24
        identifier: 'user_123',
  25
      });
  26
      const authUrl = linkResponse.link;
  27
      // Redirect or send authUrl to the user
  28
    }
  29
  } catch (err) {
  30
    if (!(err instanceof ScalekitServerException)) throw err;
  31
    // For example, the connection name doesn't match the dashboard
  32
    console.error('Scalekit error:', err.errorCode, err.message);
  33
  }
  ```

Custom user verifier needs user_verify_url

If the environment uses the **Custom user verifier** mode, also pass `user_verify_url` and `state` when you request the link. Without `user_verify_url`, the request fails. See [Verify users](/agentkit/user-verification/#generate-the-authorization-link).

By default, the link expires five minutes after you create it. The response’s `expiry` field gives the exact time, so create the link when you’re ready to send it, and create a new one if it expires.

## Send the link to the user

[Section titled “Send the link to the user”](#send-the-link-to-the-user)

How you deliver the link depends on your application:

* **Web app:** redirect the user to `auth_url` directly if they’re in an active browser session
* **Email or notification:** send the link when the user isn’t actively in your app, or when connecting at their own pace is acceptable
* **In-app prompt:** show a button (“Connect GitHub”) when you want to prompt connection at a specific moment in the user’s workflow

Once the user opens the link and approves the OAuth consent screen, Scalekit exchanges the authorization code for tokens and marks the connected account `ACTIVE`. You do not need to handle the OAuth callback yourself.

To learn when the user has finished, [listen for account events](/agentkit/account-events/): a `connected_account.status_updated` event with status `ACTIVE` arrives when the account activates. Or check the status again with `get_connected_account_details` in Python or `getConnectedAccount` in Node.js.

Production: add user verification

New environments use the **None** verification mode, so any user who completes the OAuth flow activates the connected account. Before production, switch to **Custom user verifier** so your app confirms the authorizing user is the one it intended to connect. See [Verify user identity](/agentkit/user-verification/).

## Check status and re-authorize

[Section titled “Check status and re-authorize”](#check-status-and-re-authorize)

Check the connected account status before executing tools. Tokens can expire or be revoked, so generate a new authorization link using the same flow when that happens.

* Python

  ```python
  1
  response = actions.get_or_create_connected_account(
  2
      connection_name="github-connect",
  3
      identifier="user_123"
  4
  )
  5
  connected_account = response.connected_account
  6
  # ACTIVE: ready for tool calls
  7
  # PENDING_AUTH: the user hasn't completed authorization yet
  8
  # PENDING_VERIFICATION: authorized, but user verification is still pending
  9
  # EXPIRED: tokens expired or were revoked; the user must authorize again
  10
  # DISCONNECTED: the account was disconnected
  11


  12
  if connected_account.status != "ACTIVE":
  13
      link_response = actions.get_authorization_link(
  14
          connection_name="github-connect",
  15
          identifier="user_123"
  16
      )
  17
      # Redirect or send link_response.link to the user
  ```

* Node.js

  ```typescript
  1
  import { ConnectorStatus } from '@scalekit-sdk/node';
  2


  3
  const response = await actions.getOrCreateConnectedAccount({
  4
    connectionName: 'github-connect',
  5
    identifier: 'user_123',
  6
  });
  7


  8
  const connectedAccount = response.connectedAccount;
  9
  // ACTIVE: ready for tool calls
  10
  // PENDING_AUTH: the user hasn't completed authorization yet
  11
  // PENDING_VERIFICATION: authorized, but user verification is still pending
  12
  // EXPIRED: tokens expired or were revoked; the user must authorize again
  13
  // DISCONNECTED: the account was disconnected
  14


  15
  if (connectedAccount?.status !== ConnectorStatus.ACTIVE) {
  16
    const linkResponse = await actions.getAuthorizationLink({
  17
      connectionName: 'github-connect',
  18
      identifier: 'user_123',
  19
    });
  20
    // Redirect or send linkResponse.link to the user
  21
  }
  ```

## Common problems

[Section titled “Common problems”](#common-problems)

### `user_verify_url is required when user_verify_mode is b2b`

[Section titled “user_verify_url is required when user_verify_mode is b2b”](#user_verify_url-is-required-when-user_verify_mode-is-b2b)

The environment uses the **Custom user verifier** mode, and the link request didn’t include `user_verify_url`. The request fails with a `400`. Pass `user_verify_url` and `state` as shown in [Verify users](/agentkit/user-verification/#generate-the-authorization-link), or switch to **Scalekit users only** for internal testing.

### The account stays `PENDING_AUTH`

[Section titled “The account stays PENDING_AUTH”](#the-account-stays-pending_auth)

The user didn’t finish authorizing, or the link expired before they opened it. Create a new link and send it again.

### Still stuck?

[Section titled “Still stuck?”](#still-stuck)

Find the error message or the account status on [Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/). It covers each connected-account status, the errors on Scalekit’s connection page, OAuth app misconfiguration and provider-specific errors.

## Next

[Section titled “Next”](#next)

[Verify users](/agentkit/user-verification/)Confirm that the person who authorized is the user you meant.

[Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect.

---
# DOCUMENT BOUNDARY
---

# Call any API

> Call any endpoint of a connector's API as the user through the Scalekit API proxy, then wrap it as a tool for your agent. Python, Node.js and cURL.

When no [built-in tool](/agentkit/tools/scalekit-optimized-tools/) does what you need, call the app’s API directly through the Scalekit proxy. You send the path, method and body; Scalekit adds the user’s credentials, calls the app at its base URL and returns the raw response. Then you can wrap the call as your own tool. The proxy is on for most connectors.

Search the built-in tools first: they come with schemas and structured output, and cover most of each app’s API.

## Before you start

[Section titled “Before you start”](#before-you-start)

* An `ACTIVE` connected account for the user. [Authorize a user](/agentkit/tools/authorize/) shows how to get one.

* The connection name from **AgentKit** > **Connections**. These examples use a Slack connection named `slack`.

* A client set up as in the [Quickstart](/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`:

  ```bash
  TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
    -d grant_type=client_credentials \
    -d client_id="$SCALEKIT_CLIENT_ID" \
    -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token)
  ```

* The app’s API reference for the endpoint you want:

  | Connector  | API reference                                                                                    |
  | ---------- | ------------------------------------------------------------------------------------------------ |
  | Gmail      | [Gmail API](https://developers.google.com/gmail/api/reference/rest)                              |
  | Slack      | [Slack API methods](https://api.slack.com/methods)                                               |
  | GitHub     | [GitHub REST API](https://docs.github.com/en/rest)                                               |
  | Salesforce | [Salesforce REST API](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/) |
  | HubSpot    | [HubSpot API](https://developers.hubspot.com/docs/api/overview)                                  |

1. ## Check the connected account is active

   [Section titled “Check the connected account is active”](#check-the-connected-account-is-active)

   The proxy only works for an `ACTIVE` connected account. Check before you call, and send a new authorization link if it isn’t.

   * Python

     ```python
     account = actions.get_connected_account_details(
         connection_name="slack",
         identifier="user_123",
     ).connected_account
     if account.status != "ACTIVE":
         raise SystemExit(f"Slack is {account.status}. Send the user a new authorization link.")
     ```

   * Node.js

     ```ts
     import { ConnectorStatus } from '@scalekit-sdk/node';


     const { connectedAccount: account } = await scalekit.actions.getConnectedAccount({
       connectionName: 'slack',
       identifier: 'user_123',
     });
     if (account?.status !== ConnectorStatus.ACTIVE) {
       throw new Error('Slack is not ACTIVE. Send the user a new authorization link.');
     }
     ```

   * cURL

     ```bash
     curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/connected_accounts/details" \
       -H "Authorization: Bearer $TOKEN" \
       --data-urlencode "connector=slack" \
       --data-urlencode "identifier=user_123" |
       jq -r '.connected_account.status'
     ```

2. ## Call the endpoint

   [Section titled “Call the endpoint”](#call-the-endpoint)

   Pass the path from the app’s API reference, without the base URL. Query parameters go in `query_params`, and a JSON body goes in `body`. This reads the custom profile fields of the user’s Slack workspace, which no built-in Slack tool covers:

   * Python

     ```python
     from scalekit.common.exceptions import ScalekitServerException


     try:
         response = actions.request(
             connection_name="slack",
             identifier="user_123",
             method="GET",
             path="/api/team.profile.get",
             query_params={"visibility": "visible"},
         )
         print(response.status_code, response.json())
     except ScalekitServerException as e:
         print("Request failed:", e.error_code, e.message)
     ```

   * Node.js

     ```ts
     import { ScalekitServerException } from '@scalekit-sdk/node';


     try {
       const response = await scalekit.actions.request({
         connectionName: 'slack',
         identifier: 'user_123',
         method: 'GET',
         path: '/api/team.profile.get',
         queryParams: { visibility: 'visible' },
       });
       console.log(response.status, response.data);
     } catch (err) {
       if (!(err instanceof ScalekitServerException)) throw err;
       console.error('Request failed:', err.errorCode, err.message);
     }
     ```

   * cURL

     ```bash
     curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/proxy/api/team.profile.get" \
       -H "Authorization: Bearer $TOKEN" \
       -H "connection_name: slack" \
       -H "identifier: user_123" \
       --data-urlencode "visibility=visible"
     ```

   A write works the same way with a body. This sets a field on the user’s Slack profile:

   * Python

     ```python
     response = actions.request(
         connection_name="slack",
         identifier="user_123",
         method="POST",
         path="/api/users.profile.set",
         body={"profile": {"title": "Support engineer"}},
     )
     print(response.status_code, response.json())
     ```

   * Node.js

     ```ts
     const response = await scalekit.actions.request({
       connectionName: 'slack',
       identifier: 'user_123',
       method: 'POST',
       path: '/api/users.profile.set',
       body: { profile: { title: 'Support engineer' } },
     });
     console.log(response.status, response.data);
     ```

   * cURL

     ```bash
     curl -sS -X POST "$SCALEKIT_ENVIRONMENT_URL/proxy/api/users.profile.set" \
       -H "Authorization: Bearer $TOKEN" \
       -H "connection_name: slack" \
       -H "identifier: user_123" \
       -H "Content-Type: application/json" \
       -d '{"profile": {"title": "Support engineer"}}'
     ```

   The proxy returns the app’s response as is: its status code, headers and body. In Python, `request` returns a `requests` response (`status_code`, `json()`) for every status, so check `status_code`. In Node.js, it returns an Axios response (`status`, `data`) for a `2xx` status, and throws a `ScalekitServerException` for any other status, so wrap the call in `try`/`catch`.

3. ## Wrap it as a tool

   [Section titled “Wrap it as a tool”](#wrap-it-as-a-tool)

   Give your model a tool that matches what the agent needs to do, not the shape of the app’s endpoint. Keep the input schema small, validate model input before you send it, and return only the fields the model needs.

   * Python

     ```python
     1
     list_profile_fields_tool = {
     2
         "name": "slack_list_profile_fields",
     3
         "description": "List the custom profile fields defined in the user's Slack workspace, "
     4
         "such as Title or Pronouns. Returns each field's ID, label and type.",
     5
         "input_schema": {"type": "object", "properties": {}},
     6
     }
     7


     8
     def slack_list_profile_fields(identifier: str) -> dict:
     9
         response = actions.request(
     10
             connection_name="slack",
     11
             identifier=identifier,
     12
             method="GET",
     13
             path="/api/team.profile.get",
     14
         )
     15
         data = response.json()
     16
         if not data.get("ok"):
     17
             return {"error": data.get("error")}
     18
         fields = data.get("profile", {}).get("fields", [])
     19
         return {"fields": [{"id": f["id"], "label": f["label"], "type": f["type"]} for f in fields]}
     20


     21
     print(slack_list_profile_fields("user_123"))
     ```

   * Node.js

     ```ts
     1
     const listProfileFieldsTool = {
     2
       name: 'slack_list_profile_fields',
     3
       description:
     4
         "List the custom profile fields defined in the user's Slack workspace, " +
     5
         "such as Title or Pronouns. Returns each field's ID, label and type.",
     6
       input_schema: { type: 'object', properties: {} },
     7
     };
     8


     9
     type ProfileField = { id: string; label: string; type: string };
     10


     11
     async function slackListProfileFields(identifier: string) {
     12
       const response = await scalekit.actions.request({
     13
         connectionName: 'slack',
     14
         identifier,
     15
         method: 'GET',
     16
         path: '/api/team.profile.get',
     17
       });
     18
       const data = response.data as { ok: boolean; error?: string; profile?: { fields?: ProfileField[] } };
     19
       if (!data.ok) return { error: data.error };
     20
       const fields = data.profile?.fields ?? [];
     21
       return { fields: fields.map(({ id, label, type }) => ({ id, label, type })) };
     22
     }
     23


     24
     console.log(listProfileFieldsTool.name, await slackListProfileFields('user_123'));
     ```

   Pass the tool definition to your model with the built-in tools, and run the function when the model calls it.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

The call returns the app’s own response. For Slack, a `200` with `"ok": true` and the fields you asked for. Slack reports errors in the body with `"ok": false`, while most other APIs use a `4xx` status.

## Common problems

[Section titled “Common problems”](#common-problems)

### `proxy not enabled for provider`

[Section titled “proxy not enabled for provider”](#proxy-not-enabled-for-provider)

The proxy is off for that connector. Contact  to turn it on.

### `authorization details are missing for connected account identifier`

[Section titled “authorization details are missing for connected account identifier”](#authorization-details-are-missing-for-connected-account-identifier)

No active connected account matches this identifier and connection. Check both values, and [authorize the user](/agentkit/tools/authorize/) if the account isn’t `ACTIVE`.

### Node.js: `request()` throws a `401` on the first call

[Section titled “Node.js: request() throws a 401 on the first call”](#nodejs-request-throws-a-401-on-the-first-call)

Make another SDK call before your first `request()`, such as the account check in step 1. The Node.js client fetches its access token on its first regular SDK call, and `request()` then sends that token. In a long-running process, if `request()` later throws a `401` because the token expired, make another SDK call and retry.

### The app returns `401` or `403`

[Section titled “The app returns 401 or 403”](#the-app-returns-401-or-403)

The user’s token is missing a scope the endpoint needs. Add the scope to the connection, then have the user authorize again.

### The app only accepts traffic from known IP addresses

[Section titled “The app only accepts traffic from known IP addresses”](#the-app-only-accepts-traffic-from-known-ip-addresses)

Add Scalekit’s [outbound IP addresses](/reference/outbound-ip-addresses/) for your region to the app’s allowlist.

## Next

[Section titled “Next”](#next)

[Choose a framework](/agentkit/examples/)Add your tools to an agent in LangChain, OpenAI, Anthropic and more.

[Create a connector](/agentkit/bring-your-own-connector/overview/)Add an app that isn't in the catalog.

---
# DOCUMENT BOUNDARY
---

# Use built-in tools

> Find the tools a user can call, run one with execute_tool, and pass the tool list to your model. Python, Node.js and cURL.

Every connector ships prebuilt tools, such as `github_user_repos_list` or `slack_send_message`. Each has a JSON Schema for its input and returns structured output. Your code names the tool and the user; Scalekit adds that user’s credentials, calls the app’s API and returns the result. This page finds a tool, runs it, and hands the tool list to your model.

## Before you start

[Section titled “Before you start”](#before-you-start)

* An `ACTIVE` connected account for the user. [Authorize a user](/agentkit/tools/authorize/) shows how to get one.

* The connection name from **AgentKit** > **Connections**. These examples use `github-connect`, the GitHub connection new environments include.

* A client set up as in the [Quickstart](/agentkit/quickstart/): `actions` in Python, `scalekit` in Node.js. For cURL, get an access token and keep it in `TOKEN`:

  ```bash
  TOKEN=$(curl -sS "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
    -d grant_type=client_credentials \
    -d client_id="$SCALEKIT_CLIENT_ID" \
    -d client_secret="$SCALEKIT_CLIENT_SECRET" | jq -r .access_token)
  ```

1. ## Find the right tool

   [Section titled “Find the right tool”](#find-the-right-tool)

   Search by the job you want done. Pass the user’s identifier to see, for each tool, whether that user can call it now.

   * Python

     ```python
     from scalekit.v1.tools.tools_pb2 import ToolReadinessState


     results, _ = actions.tools.search_tools(
         query="list my GitHub repositories",
         identifier="user_123",
         top_k=5,
     )
     for tool in results.tools:
         ready = [
             c.connected_account_id
             for c in tool.connections
             if c.readiness_state == ToolReadinessState.TOOL_READINESS_STATE_READY
         ]
         print(tool.name, round(tool.score, 2), "ready" if ready else "not connected")
     ```

   * Node.js

     ```ts
     import { ToolReadinessState } from '@scalekit-sdk/node';


     const { tools } = await scalekit.tools.searchTools('list my GitHub repositories', {
       identifier: 'user_123',
       topK: 5,
     });
     for (const tool of tools) {
       const ready = tool.connections.filter((c) => c.readinessState === ToolReadinessState.READY);
       console.log(tool.name, tool.score.toFixed(2), ready.length ? 'ready' : 'not connected');
     }
     ```

   * cURL

     ```bash
     curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools:search" \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{"query": "list my GitHub repositories", "identifier": "user_123", "top_k": 5}' |
       jq '.tools[] | {name, score, connections}'
     ```

   Each result has a `score` (higher is closer) and a `connections` list with a `readiness_state` per connection:

   * `READY`: the user has an `ACTIVE` account on that connection. Call the tool now.
   * `NEEDS_CONNECTION`: the account exists but isn’t active. [Send a new authorization link](/agentkit/tools/authorize/).
   * `NEEDS_REAUTH`: the token expired. Send a new authorization link.

   A connector the user has never connected isn’t in `connections` at all.

   To get the tools of specific connections instead, list the scoped tools. Pass the connections your agent works with, such as the ones the user picked in your app, and get the tools of those connections in one list:

   * The user needs a connected account on each connection you pass. Otherwise the call fails.
   * Pass one connection per connector. Two connections of the same connector, such as two GitHub connections, fail with `INVALID_ARGUMENT`.
   * Pages hold up to 100 tools. Pass `next_page_token` back until it’s empty.

   The samples list the tools of `github-connect` and of a Gmail connection named `gmail`. Change `gmail` to a connection in your environment, or remove it.

   * Python

     ```python
     1
     from google.protobuf.json_format import MessageToDict
     2


     3
     definitions = []
     4
     page_token = None
     5
     while True:
     6
         page, _ = actions.tools.list_scoped_tools(
     7
             identifier="user_123",
     8
             filter={"connection_names": ["github-connect", "gmail"]},
     9
             page_size=100,
     10
             page_token=page_token,
     11
         )
     12
         definitions += [MessageToDict(t.tool)["definition"] for t in page.tools]
     13
         page_token = page.next_page_token
     14
         if not page_token:
     15
             break
     16


     17
     print(len(definitions), [d["name"] for d in definitions[:5]])
     ```

   * Node.js

     ```ts
     const definitions = [];
     let pageToken: string | undefined;
     do {
       const page = await scalekit.tools.listScopedTools('user_123', {
         filter: { connectionNames: ['github-connect', 'gmail'] },
         pageSize: 100,
         pageToken,
       });
       for (const t of page.tools) {
         if (t.tool?.definition) definitions.push(t.tool.definition);
       }
       pageToken = page.nextPageToken || undefined;
     } while (pageToken);


     console.log(definitions.length, definitions.slice(0, 5).map((d) => d.name));
     ```

   * cURL

     ```bash
     curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools/scoped" \
       -H "Authorization: Bearer $TOKEN" \
       --data-urlencode "identifier=user_123" \
       --data-urlencode "filter.connection_names=github-connect" \
       --data-urlencode "filter.connection_names=gmail" \
       --data-urlencode "page_size=100" |
       jq '{names: [.tools[].tool.definition.name], next_page_token}'
     ```

2. ## Call the tool

   [Section titled “Call the tool”](#call-the-tool)

   Pass the tool name, its input, and the user. Identify the user with `identifier` plus the connection name, or with `connected_account_id` alone, which you get from a search result’s `connections` or from `get_connected_account`. When you pass `connected_account_id`, Scalekit ignores `identifier`.

   * Python

     ```python
     from scalekit.common.exceptions import ScalekitServerException


     try:
         result = actions.execute_tool(
             tool_name="github_user_repos_list",
             identifier="user_123",
             connection_name="github-connect",
             tool_input={"per_page": 5, "sort": "updated"},
         )
         print(result.execution_id)
         print(result.data)
     except ScalekitServerException as e:
         print("Tool call failed:", e.error_code, e.message)
     ```

   * Node.js

     ```ts
     import { ScalekitServerException } from '@scalekit-sdk/node';


     try {
       const result = await scalekit.actions.executeTool({
         toolName: 'github_user_repos_list',
         identifier: 'user_123',
         connector: 'github-connect',
         toolInput: { per_page: 5, sort: 'updated' },
       });
       console.log(result.executionId);
       console.log(result.data);
     } catch (err) {
       if (!(err instanceof ScalekitServerException)) throw err;
       console.error('Tool call failed:', err.errorCode, err.message);
     }
     ```

   * cURL

     ```bash
     curl -sS "$SCALEKIT_ENVIRONMENT_URL/api/v1/execute_tool" \
       -H "Authorization: Bearer $TOKEN" \
       -H "Content-Type: application/json" \
       -d '{
         "tool_name": "github_user_repos_list",
         "connector": "github-connect",
         "identifier": "user_123",
         "params": {"per_page": 5, "sort": "updated"}
       }' |
       jq '{execution_id, data}'
     ```

3. ## Read the result

   [Section titled “Read the result”](#read-the-result)

   The response wraps the app’s output. Read the tool’s fields from `data`, and keep `execution_id` to find the call in your logs.

   Some apps report an error in a successful (`2xx`) response. Then the call still returns, and `data` holds the app’s error. Slack, for example, returns:

   ```json
   { "ok": false, "error": "missing_scope", "needed": "users:read" }
   ```

   Check `data` for the app’s error shape before you use it. When the app answers with an error status instead, such as `401`, `403`, `429` or another `4xx` or `5xx`, the call fails with an error whose message starts with `tool execution failed`. See [Common problems](#common-problems).

4. ## Give the tools to your model

   [Section titled “Give the tools to your model”](#give-the-tools-to-your-model)

   Tool definitions carry more than a model needs. Keep `name`, `description` and `input_schema`, a JSON Schema object for the tool’s input. Anthropic’s API takes it as `input_schema` unchanged, and OpenAI’s takes it as a function’s `parameters`. This continues from the list in step 1.

   * Python

     ```python
     llm_tools = [
         {
             "name": d["name"],
             "description": d.get("description", ""),
             "input_schema": d.get("input_schema", {"type": "object", "properties": {}}),
         }
         for d in definitions
     ]
     read_only = [d["name"] for d in definitions if d.get("annotations", {}).get("read_only_hint")]
     print(len(llm_tools), "tools,", len(read_only), "read-only")
     ```

   * Node.js

     ```ts
     const llmTools = definitions.map((d) => ({
       name: String(d.name),
       description: String(d.description ?? ''),
       input_schema: d.input_schema ?? { type: 'object', properties: {} },
     }));
     const readOnly = definitions.filter(
       (d) => (d.annotations as { read_only_hint?: boolean } | undefined)?.read_only_hint,
     );
     console.log(llmTools.length, 'tools,', readOnly.length, 'read-only');
     ```

   * cURL

     ```bash
     curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/tools/scoped" \
       -H "Authorization: Bearer $TOKEN" \
       --data-urlencode "identifier=user_123" \
       --data-urlencode "filter.connection_names=github-connect" \
       --data-urlencode "page_size=100" |
       jq '[.tools[].tool.definition | {name, description, input_schema}]'
     ```

   Each definition’s `annotations` include `read_only_hint` and `destructive_hint`. Use them to start an agent with read-only tools, or to ask the user before a destructive call.

   When the model asks for a tool, call it as in step 2 and send `data` back as the tool result. LangChain and Google ADK get native tool objects from `actions.langchain.get_tools` and `actions.google.get_tools`. [Choose a framework](/agentkit/examples/) has a complete agent for each framework.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

The call returns an `execution_id` and a `data` object with the app’s response, such as a list of repositories for `github_user_repos_list`. Search results for the user show `READY` for the connection you called.

## Common problems

[Section titled “Common problems”](#common-problems)

### `failed to get tool: `

[Section titled “failed to get tool: \”](#failed-to-get-tool-tool_name)

That tool name doesn’t exist in your environment. Use a name from search or from the scoped list, exactly as returned.

### `connection not found for the given key`

[Section titled “connection not found for the given key”](#connection-not-found-for-the-given-key)

The connection name doesn’t match **AgentKit** > **Connections** exactly. Connection names can differ between environments, so read them from configuration rather than hard-coding them.

### `connected account not found`

[Section titled “connected account not found”](#connected-account-not-found)

No connected account exists for this identifier on this connection. Check that you pass the same identifier you used when the user authorized, or [authorize the user](/agentkit/tools/authorize/) first.

### `connected account is not active`

[Section titled “connected account is not active”](#connected-account-is-not-active)

The user hasn’t finished authorizing, or the account expired or was disconnected. Send a new [authorization link](/agentkit/tools/authorize/) and wait until the status is `ACTIVE`.

### `missing or invalid  parameter`

[Section titled “missing or invalid \ parameter”](#missing-or-invalid-parameter-parameter)

The input doesn’t match the tool’s `input_schema`. Check required fields and types in the tool’s definition.

### The call returns, but `data` holds an error such as `missing_scope`

[Section titled “The call returns, but data holds an error such as missing_scope”](#the-call-returns-but-data-holds-an-error-such-as-missing_scope)

The app refused the call in a `2xx` response. For a missing scope, add the scope to the connection, then have the user authorize again so their token includes it.

### `tool execution failed - unauthorized access`

[Section titled “tool execution failed - unauthorized access”](#tool-execution-failed---unauthorized-access)

The app answered `401`: it rejected the user’s token. Check the connected account’s status, and if the user revoked access, send a new [authorization link](/agentkit/tools/authorize/).

### `tool execution failed - forbidden access`

[Section titled “tool execution failed - forbidden access”](#tool-execution-failed---forbidden-access)

The app answered `403`. The user’s token is usually missing a scope the tool needs, or the user lacks permission in the app. Add the scope to the connection, then have the user authorize again.

### `tool execution failed - rate limited`

[Section titled “tool execution failed - rate limited”](#tool-execution-failed---rate-limited)

The app answered `429`. Wait before you retry, and slow down calls for this user.

### `tool execution failed - `

[Section titled “tool execution failed - \”](#tool-execution-failed---message)

The app answered with another error status. The rest of the message is the app’s own error. Check the input against the tool’s `input_schema` and the app’s API reference.

## Next

[Section titled “Next”](#next)

[Call any API](/agentkit/tools/custom-tools/)Reach an endpoint no built-in tool covers, with the user's credentials.

[Choose a framework](/agentkit/examples/)A complete agent in LangChain, OpenAI, Anthropic and more.

To serve these tools to an MCP client instead, [create a Virtual MCP server](/agentkit/mcp/configure-mcp-server/).

---
# DOCUMENT BOUNDARY
---

# Manage trigger subscriptions

> Check each trigger subscription's status and polling interval, fix accounts that need attention, remove subscriptions and change delivery endpoints in the dashboard.

After you [set up a trigger](/agentkit/triggers/set-up/), check its subscriptions in the dashboard to see whether events are flowing, which accounts need attention and how often polling runs. This page covers subscription statuses, intervals, removing a subscription and changing delivery endpoints.

Triggers are in beta

[Contact us](/support/contact-us/) to turn triggers on for your environment.

## See a connection’s subscriptions

[Section titled “See a connection’s subscriptions”](#see-a-connections-subscriptions)

Open **AgentKit** > **Connections**, select the connection, then open the **Triggers** tab. A summary line above the list shows the number of triggers and subscriptions, and how many subscriptions need attention, or **all active**.

Each trigger row shows its number of subscriptions. Select the count to open the trigger’s subscriptions. Filter them by **All**, **Needs attention** or **Active**, or search by the account’s identifier. Subscriptions that need attention are listed first.

## Subscription statuses

[Section titled “Subscription statuses”](#subscription-statuses)

| Status                        | Means                                                                                                                                          | What to do                                                                                                                                                    |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Active**                    | The subscription receives events.                                                                                                              | Nothing.                                                                                                                                                      |
| **Throttled**                 | A polling subscription receives events, but Scalekit checks less often than you asked, because the environment has many polling subscriptions. | Nothing, if the slower interval is acceptable. Otherwise, remove subscriptions you don’t use, and subscribe with longer intervals where speed doesn’t matter. |
| **Credential issue — paused** | Polling stopped because the account’s access no longer works, for example because the user revoked it.                                         | Ask the user to reconnect the account. Polling resumes on its own, and the first check after it resumes covers the time it was paused.                        |
| **Paused**                    | The subscription is stopped and receives no events.                                                                                            | Remove it and subscribe again to receive events.                                                                                                              |
| **Needs attention**           | A status this page doesn’t list.                                                                                                               | Contact support with the subscription’s trigger and account.                                                                                                  |

**Active** and **Throttled** subscriptions receive events; the others don’t. The summary and the **Needs attention** filter include every subscription that isn’t **Active**, so a **Throttled** subscription shows there too, even though its events still arrive.

## Polling intervals

[Section titled “Polling intervals”](#polling-intervals)

Each polling subscription shows **Checking every** and the interval Scalekit actually uses. That’s the interval you set when you subscribed, or the trigger’s default, unless the subscription is **Throttled**: then it’s longer.

The interval sets how long a change can take to reach you: an event arrives up to one interval after the change, plus a few seconds. Each trigger has a minimum interval, shown under **Cadence** in the trigger’s details.

## Accounts that need attention

[Section titled “Accounts that need attention”](#accounts-that-need-attention)

A subscription can be **Active** while its connected account can’t receive the trigger. The subscription then shows a warning tag, and it won’t fire until the user reconnects the account through your app’s connect flow:

| Tag                                  | Means                                                                                                                                                                        |
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Needs re-authentication**          | The account’s credential no longer works. Send the user a new authorization link.                                                                                            |
| **Disconnected**                     | The account was disconnected. Reconnect it with a new authorization link.                                                                                                    |
| **Authorized under a different app** | The user authorized with different app credentials than the connection uses now, for example before you switched to your own app. Ask the user to reconnect.                 |
| **Awaiting confirmation**            | The account works, but Scalekit hasn’t confirmed which workspace or user at the app it belongs to, so it can’t route this trigger’s events to it. Ask the user to reconnect. |

[Authorize a user](/agentkit/tools/authorize/) shows how to send a new authorization link. To get told when an account’s status changes, listen for [account events](/agentkit/account-events/).

## Remove a subscription

[Section titled “Remove a subscription”](#remove-a-subscription)

Removing a subscription stops its events right away. It can’t be undone: to receive the events again, subscribe again.

1. On the connection’s **Triggers** tab, select the trigger’s subscription count.

2. Select **Remove** next to the subscription.

3. Check the confirmation, which names the account or the whole connection and the subscription’s filters, and confirm.

## Change delivery endpoints

[Section titled “Change delivery endpoints”](#change-delivery-endpoints)

Open **Developers** > **Trigger Delivery** to see the environment’s delivery endpoints, which connections each one receives events from and whether it’s enabled.

* **Edit an endpoint** to change its URL, description or the connections it receives events from.
* **Delete an endpoint** to stop sending events to it. It stops right away, deliveries still in progress to it are lost, and it can’t be undone.
* **Reveal** and copy an endpoint’s signing secret when you set up a new server.

Under **Recent delivery attempts**, each attempt shows when it ran, the endpoint and your server’s response code, or **No response**. A failed attempt is retried, so a newer attempt for the same event can still succeed.

## Next

[Section titled “Next”](#next)

[Receive trigger events](/agentkit/triggers/receive-events/)Verify each request, handle repeats and read the event fields.

[Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and why accounts expire.

---
# DOCUMENT BOUNDARY
---

# Receive trigger events

> Verify and parse AgentKit trigger events in Python or Node.js, respond fast, skip repeats with dedupe_key, and read each field of the event.

Each time a trigger fires, Scalekit sends your delivery endpoint an HTTP `POST` with the event as JSON. Your server verifies the signature, returns a `2xx` status quickly and hands the event to your agent. This page shows a handler in Python and Node.js, how to handle repeats and retries, and what each field in the event means.

Triggers are in beta

[Contact us](/support/contact-us/) to turn triggers on for your environment.

## Before you start

[Section titled “Before you start”](#before-you-start)

* A delivery endpoint and its signing secret, from [Set up a trigger](/agentkit/triggers/set-up/#add-a-delivery-endpoint). The code below reads the secret from `SCALEKIT_TRIGGER_SECRET`.
* Your Scalekit API credentials in `SCALEKIT_ENVIRONMENT_URL`, `SCALEKIT_CLIENT_ID` and `SCALEKIT_CLIENT_SECRET`, as in the [Quickstart](/agentkit/quickstart/). The SDK client verifies the signature.

## Verify and handle each request

[Section titled “Verify and handle each request”](#verify-and-handle-each-request)

Scalekit signs every request with the endpoint’s secret. Verify the signature against the raw request body before you trust the event, so a forged request can’t make your agent act. The SDK checks the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers and rejects requests more than five minutes old. Then parse the body as JSON.

* Python

  Install the dependencies, then run the server with `uvicorn triggers:app --port 3000`:

  ```bash
  pip install scalekit-sdk-python fastapi uvicorn
  ```

  triggers.py

  ```python
  1
  import json
  2
  import os
  3


  4
  from fastapi import FastAPI, Request, Response
  5
  from scalekit import ScalekitClient
  6
  from scalekit.common.exceptions import WebhookVerificationError
  7


  8
  scalekit_client = ScalekitClient(
  9
      os.environ["SCALEKIT_ENVIRONMENT_URL"],
  10
      os.environ["SCALEKIT_CLIENT_ID"],
  11
      os.environ["SCALEKIT_CLIENT_SECRET"],
  12
  )
  13
  app = FastAPI()
  14
  handled = set()  # use your database in production
  15


  16


  17
  @app.post("/scalekit/triggers")
  18
  async def scalekit_trigger(request: Request):
  19
      body = await request.body()  # the exact bytes Scalekit signed
  20
      try:
  21
          scalekit_client.verify_webhook_payload(
  22
              os.environ["SCALEKIT_TRIGGER_SECRET"], request.headers, body
  23
          )
  24
      except WebhookVerificationError:
  25
          # A bad or missing signature, or a request older than five minutes
  26
          return Response(status_code=400)
  27
      event = json.loads(body)
  28


  29
      # Retries repeat an event. Key on dedupe_key plus the account you act as.
  30
      key = (event["dedupe_key"], event["connected_account_id"])
  31
      if key in handled:
  32
          return Response(status_code=204)
  33


  34
      if event["delivery_scope"] == "account":
  35
          print(f"{event['trigger_type']} for {event['connected_account_id']}: {event.get('resource_id')}")
  36
      else:
  37
          # A connection-wide event names no account: your app decides who acts on it
  38
          print(f"{event['trigger_type']} on {event['connection_id']}: {event.get('resource_id')}")
  39
      # Start your agent in a background job here, then respond
  40


  41
      handled.add(key)
  42
      return Response(status_code=204)
  ```

* Node.js

  Install the dependencies, then save the code as `triggers.ts` and run it with `npx tsx triggers.ts`:

  ```bash
  npm install @scalekit-sdk/node express
  npm install --save-dev tsx @types/express
  ```

  triggers.ts

  ```ts
  1
  import express from 'express';
  2
  import { ScalekitClient, WebhookVerificationError } from '@scalekit-sdk/node';
  3


  4
  const scalekit = new ScalekitClient(
  5
    process.env.SCALEKIT_ENVIRONMENT_URL!,
  6
    process.env.SCALEKIT_CLIENT_ID!,
  7
    process.env.SCALEKIT_CLIENT_SECRET!,
  8
  );
  9
  const app = express();
  10
  const handled = new Set(); // use your database in production
  11


  12
  // express.text keeps the body as the exact text Scalekit signed
  13
  app.post('/scalekit/triggers', express.text({ type: 'application/json' }), (req, res) => {
  14
    const body: string = req.body;
  15
    try {
  16
      scalekit.verifyWebhookPayload(
  17
        process.env.SCALEKIT_TRIGGER_SECRET!,
  18
        req.headers as Record,
  19
        body,
  20
      );
  21
    } catch (err) {
  22
      if (!(err instanceof WebhookVerificationError)) throw err;
  23
      // A bad or missing signature, or a request older than five minutes
  24
      return res.sendStatus(400);
  25
    }
  26
    const event = JSON.parse(body);
  27


  28
    // Retries repeat an event. Key on dedupe_key plus the account you act as.
  29
    const key = `${event.dedupe_key}:${event.connected_account_id}`;
  30
    if (handled.has(key)) return res.sendStatus(204);
  31


  32
    if (event.delivery_scope === 'account') {
  33
      console.log(`${event.trigger_type} for ${event.connected_account_id}: ${event.resource_id}`);
  34
    } else {
  35
      // A connection-wide event names no account: your app decides who acts on it
  36
      console.log(`${event.trigger_type} on ${event.connection_id}: ${event.resource_id}`);
  37
    }
  38
    // Start your agent in a background job here, then respond
  39


  40
    handled.add(key);
  41
    res.sendStatus(204);
  42
  });
  43


  44
  app.listen(3000);
  ```

* Any language

  Without a Scalekit SDK, verify the request yourself before you parse the body:

  1. Read the `webhook-id`, `webhook-timestamp` and `webhook-signature` headers. Reject the request if one is missing.
  2. Reject the request if `webhook-timestamp`, in Unix seconds, is more than five minutes from your server’s current time.
  3. Remove the `whsec_` prefix from the signing secret and base64-decode the rest. That’s the key.
  4. Compute an HMAC-SHA256 with that key over `{webhook-id}.{webhook-timestamp}.{raw body}`, and base64-encode the result.
  5. `webhook-signature` holds one or more space-separated signatures, each written `v1,`. Accept the request if any `v1` signature equals your result. Compare in constant time.

  For example, with only Python’s standard library:

  verify_signature.py

  ```python
  1
  import base64
  2
  import hashlib
  3
  import hmac
  4
  import time
  5


  6


  7
  def verify_signature(body: bytes, headers: dict, secret: str) -> bool:
  8
      """headers: the request headers, with lowercase names."""
  9
      msg_id = headers.get("webhook-id")
  10
      timestamp = headers.get("webhook-timestamp")
  11
      signatures = headers.get("webhook-signature")
  12
      if not (msg_id and timestamp and signatures and timestamp.isdigit()):
  13
          return False
  14
      if abs(time.time() - int(timestamp)) > 5 * 60:
  15
          return False
  16


  17
      key = base64.b64decode(secret.removeprefix("whsec_"))
  18
      signed = f"{msg_id}.{timestamp}.".encode() + body
  19
      expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
  20
      for candidate in signatures.split(" "):
  21
          version, _, signature = candidate.partition(",")
  22
          if version == "v1" and hmac.compare_digest(signature, expected):
  23
              return True
  24
      return False
  ```

  After the signature checks out, parse the body as JSON and read the fields in [Event fields](#event-fields).

The signature covers the body byte for byte. Verify the raw body exactly as received: parsing the JSON first and serializing it again changes the bytes and fails verification.

## Respond quickly and handle repeats

[Section titled “Respond quickly and handle repeats”](#respond-quickly-and-handle-repeats)

* **Return a `2xx` status fast.** Start the agent’s work after you respond or in a background job. A response other than `2xx`, or no response within 30 seconds, counts as a failure and is retried.
* **Verify before you queue.** Verify the signature in the handler that receives the request, then queue the verified event. A queued request can be older than five minutes by the time a worker reads it, and the SDK rejects it.
* **Expect the same event more than once.** Delivery is at least once. Scalekit sends an event again when it doesn’t get a `2xx` from your endpoint within 30 seconds, including when your server handled the event but its response was slow or didn’t arrive. Each repeat carries the same `dedupe_key`. Store the keys you’ve handled and skip a repeat. When one connection-wide event makes several of your users act, store the key together with each user’s account, so a retry after a partial failure skips only the ones that finished.
* **Don’t rely on order.** Events can arrive out of order. Use `occurred_at` when order matters.

### Retries

[Section titled “Retries”](#retries)

Scalekit makes up to 8 attempts to deliver an event, with increasing delays between them. Any status outside `2xx` is a failure, including `4xx`, and each attempt waits up to 30 seconds for a response.

| Attempt | Delay after the previous attempt |
| ------- | -------------------------------- |
| 1       | Immediately                      |
| 2       | 5 seconds                        |
| 3       | 5 minutes                        |
| 4       | 30 minutes                       |
| 5       | 2 hours                          |
| 6       | 5 hours                          |
| 7       | 10 hours                         |
| 8       | 10 hours                         |

If the eighth attempt fails, about 27 hours after the first, the delivery is marked as failed. Each attempt and your server’s response code appear under **Recent delivery attempts** on **Developers** > **Trigger Delivery**.

## Event fields

[Section titled “Event fields”](#event-fields)

Every event has the same shape, for webhook and polling triggers alike. This event came from a Salesforce **Lead created** subscription:

salesforce_lead_created

```json
1
{
2
  "version": "1",
3
  "trigger_type": "salesforce_lead_created",
4
  "subscription_id": "trgs_141020933185734981",
5
  "delivery_scope": "account",
6
  "connection_id": "conn_133400101014995480",
7
  "connected_account_id": "ca_133400349586228019",
8
  "resource_type": "salesforce_lead_created",
9
  "resource_id": "00Q5g00000AbCdEFGH",
10
  "occurred_at": "2026-10-10T09:14:03Z",
11
  "detection_mode": "poll",
12
  "payload_state": "full",
13
  "payload": {
14
    "attributes": {
15
      "type": "Lead",
16
      "url": "/services/data/v66.0/sobjects/Lead/00Q5g00000AbCdEFGH"
17
    },
18
    "Id": "00Q5g00000AbCdEFGH",
19
    "Name": "Jane Doe",
20
    "FirstName": "Jane",
21
    "LastName": "Doe",
22
    "Email": "jane@example.com",
23
    "Phone": null,
24
    "Company": "Example Corp",
25
    "Title": "VP Sales",
26
    "Status": "Open - Not Contacted",
27
    "LeadSource": "Web",
28
    "OwnerId": "0055g00000XyZabCDE",
29
    "CreatedDate": "2026-10-10T09:14:03.000+0000"
30
  },
31
  "dedupe_key": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGH",
32
  "correlation_id": "ca_5F133400349586228019.trgs_5F141020933185734981.00Q5g00000AbCdEFGH"
33
}
```

| Field                  | Meaning                                                                                                                                                                                         |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `version`              | The event format’s version, `"1"`                                                                                                                                                               |
| `trigger_type`         | The trigger that fired, such as `salesforce_lead_created`                                                                                                                                       |
| `subscription_id`      | The subscription that produced the event. Tells apart events from two subscriptions on the same account                                                                                         |
| `delivery_scope`       | `account`: the event is about one connected account. `connection`: the event is about the whole connection                                                                                      |
| `connection_id`        | The connection the event came from. Always present                                                                                                                                              |
| `connected_account_id` | The connected account, for `account` events. An empty string for `connection` events: branch on `delivery_scope`, not on this field                                                             |
| `resource_type`        | The kind of record that changed                                                                                                                                                                 |
| `resource_id`          | The app’s ID for the record, such as a Salesforce lead ID. Can be missing                                                                                                                       |
| `occurred_at`          | When the change happened in the app, in UTC (RFC 3339). Can be missing                                                                                                                          |
| `detection_mode`       | `poll` for a polling trigger, `webhook` for a webhook trigger                                                                                                                                   |
| `payload_state`        | `full`: `payload` holds the record. `reference`: the record couldn’t be included, for example because it’s too large, and `payload` is `null`. Fetch the record from the app with `resource_id` |
| `payload`              | The app’s own record or event, as its API returns it, with the app’s field names                                                                                                                |
| `dedupe_key`           | The same on every delivery of this event. Use it to skip repeats                                                                                                                                |
| `correlation_id`       | Carry it into the work the event starts, to trace that work back to the event                                                                                                                   |

These fields are the same for every trigger. Only `payload` differs: it holds the app’s own data, so its fields depend on the trigger. For a polling trigger it’s the record, as above. For a webhook trigger it’s the request body the provider sent. Each connector’s page describes what its triggers send, as in [Slack triggers](/agentkit/connectors/slack/#triggers).

The fields keep these snake_case names in every language. `occurred_at` is an RFC 3339 string in UTC.

* **Treat `dedupe_key` and `correlation_id` as opaque strings.** Compare them; don’t parse them. Their format can change.
* **Expect new fields and values.** Later versions can add fields and new values for `delivery_scope`, `detection_mode` and `payload_state`. Ignore fields you don’t know, and keep a default branch for unknown values.
* **Read `payload` with the app’s field names.** It’s the app’s own data, not a Scalekit format, so its shape differs per connector and trigger.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

Make a change the trigger watches for, as in [Set up a trigger](/agentkit/triggers/set-up/#check-it-worked). Your server prints a line such as `salesforce_lead_created for ca_133400349586228019: 00Q5g00000AbCdEFGH`, and **Recent delivery attempts** on **Developers** > **Trigger Delivery** shows the attempt with status `204`.

## Common problems

[Section titled “Common problems”](#common-problems)

### `Missing required headers`

[Section titled “Missing required headers”](#missing-required-headers)

The `webhook-id`, `webhook-timestamp` and `webhook-signature` headers didn’t reach the SDK. Check that a proxy in front of your server doesn’t strip them, and pass the request’s headers object unchanged.

### `Invalid signature`

[Section titled “Invalid signature”](#invalid-signature)

Node.js spells it `Invalid Signature`. The body you verified isn’t byte for byte what Scalekit sent, or the secret belongs to another endpoint or environment. Verify the raw body before any JSON parsing, and copy the secret again with **Reveal** on **Developers** > **Trigger Delivery**.

### `Message timestamp too old`

[Section titled “Message timestamp too old”](#message-timestamp-too-old)

The request is more than five minutes old, or your server’s clock is off. Sync the clock, and verify requests before you queue them.

### My endpoint returns `2xx` but the event is handled twice

[Section titled “My endpoint returns 2xx but the event is handled twice”](#my-endpoint-returns-2xx-but-the-event-is-handled-twice)

A retry or a second delivery repeated the event. Skip events whose `dedupe_key`, together with the account you act as, you’ve already handled.

## Next

[Section titled “Next”](#next)

[Manage subscriptions](/agentkit/triggers/manage/)Read subscription status and intervals, and remove a subscription.

[Call tools with the SDK](/agentkit/tools/scalekit-optimized-tools/)Let your agent act on the event with the user's connected account.

---
# DOCUMENT BOUNDARY
---

# Set up a trigger

> Add a delivery endpoint, then subscribe connected accounts to a polling trigger or register a webhook trigger with the app. Everything happens in the Scalekit dashboard.

Set up a trigger in the Scalekit dashboard so your server receives an event each time something changes in a user’s connected app. You add a delivery endpoint once per environment, then subscribe to triggers on each connection. Polling triggers and webhook triggers need different setup: follow the section for the kind you picked.

New to triggers? [How triggers work](/agentkit/triggers/overview/) explains the two kinds and how events are delivered.

Triggers are in beta

[Contact us](/support/contact-us/) to turn triggers on for your environment.

## Before you start

[Section titled “Before you start”](#before-you-start)

* Triggers turned on for the environment.
* A [connection](/agentkit/connections/) for the app, such as Salesforce or Slack.
* The **Admin** or **Developer** role. See [Team members and roles](/agentkit/team-members/).
* A server with a public HTTPS URL that accepts `POST` requests. While you develop, a tunnel to your local server works.
* For a polling trigger: a connected account in `ACTIVE` status on the connection. [Authorize a user](/agentkit/tools/authorize/) shows how to create one.
* For a webhook trigger: access to your own app’s settings at the provider, such as your Slack app.

## Add a delivery endpoint

[Section titled “Add a delivery endpoint”](#add-a-delivery-endpoint)

A delivery endpoint is the URL on your server where Scalekit sends trigger events. An endpoint can receive events from every connection in the environment, or only from the connections you choose, so you can send different apps’ events to different endpoints.

1. In the Scalekit dashboard, select the environment, then open **Developers** > **Trigger Delivery** and select **Add endpoint**.

2. Enter your **Endpoint URL**, for example `https://example.com/scalekit/triggers`, and an optional **Description**.

3. Under **Receives events from**, choose **All connections**, or **Only these connections** and pick the connections. You can change this later without registering the endpoint again.

4. Select **Save endpoint**.

5. In the endpoints table, select **Reveal** under **Signing secret**, then copy the secret. It starts with `whsec_`. Store it with your other server secrets, for example as `SCALEKIT_TRIGGER_SECRET`, and never expose it to a browser.

Each environment has its own endpoints and secrets. Add an endpoint in Production too when you go live.

## Open the connection’s triggers

[Section titled “Open the connection’s triggers”](#open-the-connections-triggers)

Open **AgentKit** > **Connections**, select the connection, then open the **Triggers** tab. It lists the triggers the connector offers, each tagged:

* **Webhook** or **Polled**: how Scalekit learns about changes.
* **Per user** or **Per connection**: whether you subscribe each connected account or the connection once.

Select **Show details** on a trigger to see **Requires**, the scopes the connection must grant for the trigger to work, and for a polling trigger **Cadence**, its default and minimum interval.

Above the list, a setup card shows the steps this connection still needs, such as generating a webhook URL or connecting an account. Once they’re done, it shrinks to a **Setup complete** line with a **Manage** button.

Some triggers need your own app

If the connection uses Scalekit’s shared app credentials and its triggers need your own app, the setup card’s first step asks you to use your own app. Add your app’s credentials to the connection, as in [Bring your own credentials](/agentkit/advanced/bring-your-own-oauth/), then come back to the **Triggers** tab.

## Subscribe to a polling trigger

[Section titled “Subscribe to a polling trigger”](#subscribe-to-a-polling-trigger)

Polling triggers need no setup in the app. Subscribe each connected account whose changes you want.

1. On the **Triggers** tab, select **Subscribe** next to the trigger, such as **Lead created** in Salesforce or **Activity created or updated** in Pipedrive.

2. Pick the connected account. To subscribe several at once, select **Subscribe several accounts** and pick them. An account that can’t receive the trigger, for example because it isn’t `ACTIVE`, shows why and isn’t added.

3. Optional: fill in the trigger’s filters, such as a lead source. Leave a filter empty to receive every matching record. Each distinct set of filter values is a separate subscription.

4. Optional: set **Check interval (seconds)**. It defaults to the trigger’s default interval, and an interval below the trigger’s minimum is refused.

5. Select **Subscribe**.

The first check runs shortly after you subscribe, then once per interval. Records created or changed before you subscribed aren’t sent, except ones changed shortly before, which the first check can pick up.

## Set up a webhook trigger

[Section titled “Set up a webhook trigger”](#set-up-a-webhook-trigger)

Webhook triggers need three things from your app at the provider: Scalekit’s webhook URL registered as the event destination, the provider’s signing secret saved in Scalekit, and a subscription to the provider’s events the trigger uses. The setup card on the **Triggers** tab walks you through them in order. Each provider names these settings differently: the connector’s page lists where they are, as in [Slack triggers](/agentkit/connectors/slack/#triggers).

1. **Generate the webhook URL.** In the setup card, select **Generate webhook URL**. The URL is unique to this connection.

2. **Paste the provider’s signing secret.** Copy the signing secret from your app’s settings at the provider. In the setup card, select **Set signing secret**, paste it and select **Save**.

   Save the secret before you register the URL. Some providers check the URL as soon as you paste it, and Scalekit needs the secret to answer that check.

3. **Register the URL with the provider.** Copy the URL from the setup card and paste it into your app’s event or webhook settings. If the provider checks the URL, Scalekit answers for you.

   Then subscribe your app at the provider to the events the trigger needs. The connector’s page lists them. Subscribe only to those: events that match no trigger subscription aren’t delivered to your endpoint.

4. **Confirm the registration.** Back in the setup card, select **I’ve registered it**. Scalekit can’t see the provider’s settings, so this step is marked done only when you confirm.

5. **Subscribe.** Select **Subscribe** next to the trigger. A **Per connection** trigger has no account to pick. Optional: fill in the trigger’s filters, such as a channel, to receive only matching events. Then select **Subscribe**.

Events from the provider now reach your delivery endpoint within seconds.

### Replace the provider’s signing secret

[Section titled “Replace the provider’s signing secret”](#replace-the-providers-signing-secret)

When you rotate the signing secret at the provider, update it in Scalekit without dropping events:

1. Open the connection’s **Triggers** tab, select **Manage** on the **Setup complete** line, then select **Replace signing secret**.

2. Paste the new secret and select **Save**. Scalekit now accepts events signed with either the previous or the new secret. The card shows when the previous secret stops being accepted.

3. Update the secret at the provider.

4. Select **Confirm rotation**, and confirm. From then on, Scalekit accepts only the new secret, and rejects events signed with the previous one.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

There’s no test event to send, so make a real change:

* **Polling trigger:** create or change a matching record in the app, such as a new lead in Salesforce or a new activity in Pipedrive, in the subscribed user’s account. Wait up to one interval.
* **Webhook trigger:** do what the trigger watches for in the app, such as mentioning your app in a Slack channel it’s in.

Then open **Developers** > **Trigger Delivery**. Under **Recent delivery attempts**, the event appears with your server’s response code. A `2xx` code means your server accepted it. To handle the request in your server, see [Receive trigger events](/agentkit/triggers/receive-events/).

## Common problems

[Section titled “Common problems”](#common-problems)

### An account can’t be subscribed

[Section titled “An account can’t be subscribed”](#an-account-cant-be-subscribed)

The account picker shows why next to the account. The common reasons:

* **The account’s credential has expired**: ask the user to reconnect, as in [Handle inactive accounts](/agentkit/connected-accounts/#handle-inactive-accounts), then subscribe again.
* **The account hasn’t finished connecting**: the user hasn’t completed the authorization link. Wait until the account is `ACTIVE`.
* **The account’s grant doesn’t cover a scope the trigger requires**: add the scopes listed under **Requires** to the connection, then ask the user to reconnect so they grant the new scopes.

### No events arrive

[Section titled “No events arrive”](#no-events-arrive)

* Check that an endpoint covers the connection: the setup card on the **Triggers** tab shows how many delivery endpoints cover it. An endpoint set to **Only these connections** must include this connection.
* Check **Recent delivery attempts** on **Developers** > **Trigger Delivery**. A non-`2xx` code or **No response** means the request reached your server, or tried to, and failed there.
* For a polling trigger, wait a full interval after the change, and check that the change matches the subscription’s filters.
* For a webhook trigger, check that the provider shows the URL as verified and that your app at the provider subscribes to the right events.

### The provider says the webhook URL didn’t verify

[Section titled “The provider says the webhook URL didn’t verify”](#the-provider-says-the-webhook-url-didnt-verify)

Save the provider’s signing secret in Scalekit first, then register the URL again. Scalekit answers the provider’s check only once it has the secret.

## Next

[Section titled “Next”](#next)

[Receive trigger events](/agentkit/triggers/receive-events/)Verify each request in Python or Node.js, respond fast and handle repeats.

[Manage subscriptions](/agentkit/triggers/manage/)Read subscription status, change endpoints and remove a subscription.

---
# DOCUMENT BOUNDARY
---

# Troubleshoot connection and OAuth errors

> Fix AgentKit connection errors: account statuses, failed_to_exchange_token, session_not_found, redirect_uri_mismatch, and Google, Microsoft or Slack consent.

When a user can’t connect an app, or a connected account stops working, the cause is almost always one of three things: the account’s status, an error from Scalekit’s connection error page, or a rule the provider enforces on your OAuth app. Check the status first, then find the error text below. Each error is its own heading, so you can link straight to it.

For errors from tool calls, the API proxy or a Virtual MCP server, see the **Common problems** on [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/#common-problems), [Call any API](/agentkit/tools/custom-tools/#common-problems) and [Virtual MCP servers](/agentkit/mcp/overview/).

## Start with diagnostics

[Section titled “Start with diagnostics”](#start-with-diagnostics)

The connected account’s status tells you whether the user never finished authorizing, still needs identity verification, has an expired token, or disconnected.

* Python

  ```python
  1
  import os
  2


  3
  from dotenv import load_dotenv
  4
  from scalekit import ScalekitClient
  5


  6
  load_dotenv()
  7


  8
  scalekit_client = ScalekitClient(
  9
      env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
  10
      client_id=os.environ["SCALEKIT_CLIENT_ID"],
  11
      client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
  12
  )
  13
  actions = scalekit_client.actions
  14


  15
  account = actions.get_connected_account_details(
  16
      identifier="user_123",
  17
      connection_name="github-connect",
  18
  ).connected_account
  19


  20
  print(account.status)  # ACTIVE, EXPIRED, PENDING_AUTH, PENDING_VERIFICATION or DISCONNECTED
  ```

* Node.js

  ```typescript
  1
  import 'dotenv/config';
  2
  import { ConnectorStatus, ScalekitClient } from '@scalekit-sdk/node';
  3


  4
  const scalekit = new ScalekitClient(
  5
    process.env.SCALEKIT_ENVIRONMENT_URL!,
  6
    process.env.SCALEKIT_CLIENT_ID!,
  7
    process.env.SCALEKIT_CLIENT_SECRET!,
  8
  );
  9


  10
  const { connectedAccount } = await scalekit.actions.getConnectedAccount({
  11
    identifier: 'user_123',
  12
    connectionName: 'github-connect',
  13
  });
  14


  15
  // The status name, such as ACTIVE or PENDING_AUTH
  16
  console.log(connectedAccount && ConnectorStatus[connectedAccount.status]);
  ```

* cURL

  ```bash
  curl -sS -G "$SCALEKIT_ENVIRONMENT_URL/api/v1/connected_accounts/details" \
    -H "Authorization: Bearer $TOKEN" \
    --data-urlencode "connector=github-connect" \
    --data-urlencode "identifier=user_123" |
    jq '.connected_account.status'
  ```

If the status is `ACTIVE` but a tool call fails, run a read-only tool such as `github_user_get_authenticated`. If that works, the connection is fine and the problem is in the call: see [Use built-in tools](/agentkit/tools/scalekit-optimized-tools/#common-problems).

To learn about status changes without polling, subscribe to the `connected_account.status_updated` and `connected_account.token_refresh_failed` webhooks. See [Listen for account events](/agentkit/account-events/).

## Connected account status

[Section titled “Connected account status”](#connected-account-status)

| Status                 | Meaning                                                            | Fix                                               |
| ---------------------- | ------------------------------------------------------------------ | ------------------------------------------------- |
| `ACTIVE`               | Credentials are valid                                              | Nothing to fix. Tool calls work.                  |
| `PENDING_AUTH`         | The user hasn’t finished authorizing                               | [Send an authorization link](#pending_auth)       |
| `PENDING_VERIFICATION` | Authorization succeeded, identity verification hasn’t              | [Finish user verification](#pending_verification) |
| `EXPIRED`              | The token expired or was revoked, and Scalekit couldn’t refresh it | [Send a new authorization link](#expired)         |
| `DISCONNECTED`         | The account was disconnected in Scalekit                           | [Send a new authorization link](#disconnected)    |

### `PENDING_AUTH`

[Section titled “PENDING_AUTH”](#pending_auth)

The user hasn’t finished authorizing, or you started re-authorization and they haven’t completed it. Create an authorization link and send it through your app, for example as an in-app prompt or on a settings page.

* Python

  ```python
  if account.status == "PENDING_AUTH":
      link = actions.get_authorization_link(
          connection_name="github-connect",
          identifier="user_123",
      )
      print(link.link)
  ```

* Node.js

  ```typescript
  import { ConnectorStatus } from '@scalekit-sdk/node';


  if (connectedAccount?.status === ConnectorStatus.PENDING_AUTH) {
    const { link } = await scalekit.actions.getAuthorizationLink({
      connectionName: 'github-connect',
      identifier: 'user_123',
    });
    console.log(link);
  }
  ```

The status changes to `ACTIVE` when the user finishes, or to `PENDING_VERIFICATION` if your environment verifies users.

### `PENDING_VERIFICATION`

[Section titled “PENDING_VERIFICATION”](#pending_verification)

The user authorized the app, but Scalekit is waiting for your app to confirm the user’s identity before it activates the account. Complete the verification step described in [Verify user identity](/agentkit/user-verification/). After verification succeeds, the status is `ACTIVE`.

### `EXPIRED`

[Section titled “EXPIRED”](#expired)

The access token expired and Scalekit couldn’t refresh it, for example because the user removed your app’s access at the provider, such as in their Google Account’s third-party connections. Scalekit refreshes tokens on its own, so there’s no refresh call to make: send the user a new authorization link, as for [`PENDING_AUTH`](#pending_auth).

If the account expires again soon after every re-authorization, the connection never received a refresh token. Providers issue one only when the connection asks for offline access. Add the provider’s offline-access scope, such as `offline_access`, in [Configure scopes](/agentkit/connections/#configure-scopes), then have the user connect again. [Common causes](/agentkit/connected-accounts/#common-causes) lists the other reasons accounts expire.

### `DISCONNECTED`

[Section titled “DISCONNECTED”](#disconnected)

The account was disconnected in Scalekit, from the dashboard or by a disconnect call from your app. Its credentials are cleared, so only re-authorization fixes it. Send a new authorization link, and until the user reconnects, show the state in your UI and stop scheduling tool calls for that account.

### 401 or `REAUTHENTICATION_NEEDED`: the user must authorize again

[Section titled “401 or REAUTHENTICATION_NEEDED: the user must authorize again”](#401-or-reauthentication_needed-the-user-must-authorize-again)

A tool call returns HTTP 401 with `error_code` `TOOL_ERROR` and `tool_error_code` `REAUTHENTICATION_NEEDED`, or `UNAUTHENTICATED` while the connected account is `EXPIRED`. The app rejected the user’s credentials, usually because the user revoked your app’s access or the token expired. Retrying with a new Scalekit access token doesn’t help.

Send the user a new authorization link, as for [`PENDING_AUTH`](#pending_auth), and pause tool calls for that account until its status is `ACTIVE` again. [Authorize a user](/agentkit/tools/authorize/) shows the full flow, and [Errors and rate limits](/agentkit/reference/errors/) lists every error code.

## Errors on Scalekit’s connection page

[Section titled “Errors on Scalekit’s connection page”](#errors-on-scalekits-connection-page)

When authorization fails, Scalekit sends the user to its connection error page. The page shows a title, and its **Debug info** panel shows the `error` code, the `error_description` and an **Auth Request ID**. Find the title or the code below.

Keep the Auth Request ID

The Auth Request ID lets Scalekit support find the failed attempt in the logs. **Copy error details** on the page copies it with the error and description.

### Authorization failed (`failed_to_exchange_token`)

[Section titled “Authorization failed (failed_to_exchange_token)”](#authorization-failed-failed_to_exchange_token)

The user approved access, but Scalekit couldn’t finish the connection. Read the `error_description` to find which step failed:

* **The provider rejected the token exchange.** The description holds the provider’s error, such as `invalid_client` or `invalid_grant`. Check the client ID and secret on the connection: see [Invalid client or client authentication failed](#invalid-client-or-client-authentication-failed). An authorization code is valid once and briefly, so a user who waited a long time on the consent screen should start again.

The page also suggests organization-level causes. The user’s admin may have blocked your app with an access policy, the app may lack permission in the organization’s settings, or the user’s account may not have the rights to authorize it. The [provider errors](#provider-errors) below cover how Google, Microsoft and Slack report these.

### Session expired or not available (`session_not_found`)

[Section titled “Session expired or not available (session_not_found)”](#session-expired-or-not-available-session_not_found)

Your environment’s user verification mode is **Scalekit users only**, and the person authorizing isn’t signed in to the Scalekit dashboard. Scalekit uses the dashboard session to confirm who the user is, so without one it can’t activate the account.

* If the user is on your team, have them sign in to the Scalekit dashboard in the same browser and start the flow again.
* If the user is a customer, they don’t have a Scalekit login. Switch to **Custom user verifier** in **AgentKit** > **Settings** > **User Verification**, and verify users in your own app. See [Verify user identity](/agentkit/user-verification/).

This isn’t a timeout: retrying without signing in fails the same way.

### Account mismatch (`access_denied`)

[Section titled “Account mismatch (access_denied)”](#account-mismatch-access_denied)

The flow was refused. Two things cause it:

* **The user declined consent** at the provider, or the provider denied the request. The provider sends `access_denied` back and Scalekit shows it with this title. Offer the user a way to start again.
* **Scalekit’s identity check failed** in **Scalekit users only** mode. The user is signed in to a different Scalekit workspace, or, for a flow started from the dashboard’s playground, as a different user from the one who started it. Sign in with the expected account and start again.

### Invalid request (`invalid_request`)

[Section titled “Invalid request (invalid_request)”](#invalid-request-invalid_request)

The request reached Scalekit without a required parameter, or with one it couldn’t read, such as a missing `auth_request_id` or `state`. This happens when a link is truncated, edited or reused. Create a new authorization link and use it unchanged. If the provider returned `invalid_request`, the authorization request didn’t meet its rules: check the connection’s scopes and settings.

### Something went wrong (`server_error`)

[Section titled “Something went wrong (server_error)”](#something-went-wrong-server_error)

An unexpected error, in Scalekit or at the provider. Start the flow again after a few minutes, and check the [Scalekit status page](https://status.scalekit.com) and the provider’s status page. If it persists, contact support with the Auth Request ID.

Two `error_description` values point to a specific step:

* **`user_verify_url not configured for verification redirect`.** Your environment uses **Custom user verifier**, but the authorization link was created without a `user_verify_url`. Pass one when you create the link, or switch modes. See [Verify user identity](/agentkit/user-verification/#common-scenarios).
* **`Error executing post auth hooks`.** Scalekit stored the tokens but a step after authorization failed. Start the flow again. If it fails again, contact support with the Auth Request ID.

### Other codes from the provider

[Section titled “Other codes from the provider”](#other-codes-from-the-provider)

Scalekit passes the provider’s own OAuth error through to the page, where it shows as **Unable to complete connection**. The codes are defined in [RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1):

| Code                        | Meaning                                                               | What to do                                         |
| --------------------------- | --------------------------------------------------------------------- | -------------------------------------------------- |
| `unauthorized_client`       | The provider doesn’t allow this OAuth app to use this flow            | Check the app’s settings in the provider’s console |
| `invalid_scope`             | A requested scope is unknown or not allowed for this app              | Fix the scopes on the connection, then retry       |
| `unsupported_response_type` | The provider doesn’t support the authorization code flow for this app | Check the app type in the provider’s console       |
| `temporarily_unavailable`   | The provider is overloaded or down for maintenance                    | Retry after a few minutes                          |

## OAuth app configuration errors

[Section titled “OAuth app configuration errors”](#oauth-app-configuration-errors)

These come from the OAuth app a connection uses, whether it’s Scalekit’s or [one you registered](/agentkit/advanced/bring-your-own-oauth/).

### `redirect_uri_mismatch`

[Section titled “redirect_uri_mismatch”](#redirect_uri_mismatch)

The redirect URI registered in the provider’s OAuth app doesn’t match the one Scalekit sends. Providers require an exact match.

1. In the Scalekit dashboard, open **AgentKit** > **Connections** and select the connection.
2. Copy the **Redirect URI**.
3. In the provider’s developer console, paste it into the OAuth app’s allowed redirect URIs.
4. Save, and start the connection flow again.

Most mismatches are a trailing slash (`/callback/` and `/callback`), `http` instead of `https`, or a missing port number in local development.

### Invalid client or client authentication failed

[Section titled “Invalid client or client authentication failed”](#invalid-client-or-client-authentication-failed)

The client ID or secret on the connection doesn’t match the provider’s OAuth app. Providers report it as `invalid_client`, inside a `failed_to_exchange_token` error.

1. Open **AgentKit** > **Connections** and select the connection.
2. Compare the **Client ID** and **Client Secret** with the OAuth app in the provider’s console.
3. If the secret was rotated or expired, create a new one in the provider’s console and paste it into the connection.
4. Start the connection flow again.

### `invalid_state`

[Section titled “invalid_state”](#invalid_state)

Scalekit checks the OAuth `state` parameter to protect against cross-site request forgery, and fails the flow with `invalid_state` when it’s missing or doesn’t match. The connection error page shows it as **Unable to complete connection**. Finish the flow in the same browser it started in, allow cookies, and start again from a fresh authorization link rather than a bookmarked or back-button page.

### Authorization succeeds but tools fail on a missing scope

[Section titled “Authorization succeeds but tools fail on a missing scope”](#authorization-succeeds-but-tools-fail-on-a-missing-scope)

The user’s token doesn’t include a scope a tool needs. The app returns an error such as Slack’s `missing_scope`, or a `403`. Add the scope in [Configure scopes](/agentkit/connections/#configure-scopes), then have the user authorize again: existing tokens don’t gain new scopes.

### The connection works but access tokens come back empty

[Section titled “The connection works but access tokens come back empty”](#the-connection-works-but-access-tokens-come-back-empty)

A connected account has an ID, such as `ca_...`, but its access and refresh token fields are empty. That’s expected: API responses don’t include provider tokens by default, which keeps them out of your app. Call tools with `execute_tool` or the [API proxy](/agentkit/tools/custom-tools/), which add the user’s token for you. To read the tokens themselves, contact [support](mailto:support@scalekit.com) to enable it.

## Provider errors

[Section titled “Provider errors”](#provider-errors)

Each provider sets its own rules for OAuth apps. These are the common ones, with links to the provider’s documentation.

### It works for your account but fails for your customers

[Section titled “It works for your account but fails for your customers”](#it-works-for-your-account-but-fails-for-your-customers)

A provider OAuth app often starts in a development or testing state that only its creator, or a short list of test users, can authorize. The connection works while you test it with your own account, and fails when a customer connects. Listing tools also works for you, because your own account already authorized the app. This applies to OAuth apps you registered and to connections that use Scalekit’s shared credentials.

To fix it, finish the provider’s requirements for external use, publish the app, and connect again from an account outside your organization. [Publish your own OAuth apps](/agentkit/advanced/launch-checklist/#publish-your-own-oauth-apps) in the launch checklist lists the steps. For example:

* **Google** limits apps in the **Testing** publishing status to 100 test users that you list on the OAuth consent screen. Publish the app to production, and complete verification if it requests sensitive or restricted scopes. See [Google’s publishing status guide](https://support.google.com/cloud/answer/15549945).
* **Airtable** returns “This OAuth application cannot be used outside of development” until the integration has a privacy policy URL, a terms of service URL and a support email. See [Airtable’s OAuth integration guide](https://airtable.com/developers/web/guides/oauth-integrations).
* **ZoomInfo** offers partner apps for serving customers across organizations. Partner apps go through ZoomInfo’s review before customers can install them. See [ZoomInfo partner apps](https://docs.gtm.ai/docs/partner-app).

### Google: unverified app screen or `admin_policy_enforced`

[Section titled “Google: unverified app screen or admin_policy_enforced”](#google-unverified-app-screen-or-admin_policy_enforced)

Google shows an unverified app screen when an app that hasn’t completed verification requests sensitive or restricted scopes, and caps such an app at 100 new users. Complete [Google’s app verification](https://support.google.com/cloud/answer/7454865), or request fewer sensitive scopes.

`admin_policy_enforced` means the user’s Google Workspace administrator doesn’t allow the app to access one or more requested scopes. The administrator can [mark the app as trusted](https://knowledge.workspace.google.com/admin/apps/control-which-apps-access-google-workspace-data) by its OAuth client ID in the Admin console’s API controls. See [Google’s OAuth error reference](https://developers.google.com/identity/protocols/oauth2/web-server).

Testing apps get short-lived refresh tokens

Google issues refresh tokens that expire in 7 days to apps with an external user type in **Testing** status, unless they request only name, email and profile scopes. Accounts on such an app expire weekly until you publish it. See [Google’s OAuth 2.0 guide](https://developers.google.com/identity/protocols/oauth2).

### Microsoft: `AADSTS65001` or “Need admin approval”

[Section titled “Microsoft: AADSTS65001 or “Need admin approval””](#microsoft-aadsts65001-or-need-admin-approval)

`AADSTS65001` means the user or an administrator hasn’t consented to the permissions the app requests. When the tenant doesn’t allow users to consent, the user sees **Need admin approval**, and the error can be `AADSTS90094` (administrator consent is required).

1. Open the app registration in the Microsoft Entra admin center.
2. Check that its API permissions match the connection’s scopes.
3. Have a tenant administrator select **Grant admin consent**.
4. Start the connection flow again.

See Microsoft’s [consent troubleshooting guide](https://learn.microsoft.com/en-us/troubleshoot/entra/entra-id/app-integration/troubleshoot-consent-issues) and [error code reference](https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes).

### Microsoft: `AADSTS50020`

[Section titled “Microsoft: AADSTS50020”](#microsoft-aadsts50020)

The account the user signed in with doesn’t exist in the tenant the app expects, for example a personal account or an account from another organization signing in to a single-tenant app. Have the user sign in with their work or school account in that tenant, have an administrator add them as an external user, or make the app registration multi-tenant. See Microsoft’s [error code reference](https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes).

### Slack: the workspace requires app approval

[Section titled “Slack: the workspace requires app approval”](#slack-the-workspace-requires-app-approval)

When a Slack workspace only allows pre-approved apps, a member who tries to install your app submits a request instead, and the connection can’t finish until an administrator approves it. Ask a workspace administrator to approve the app, or test in a workspace where you manage app settings. See [Slack’s app approval guide](https://slack.com/help/articles/222386767-Manage-app-approval-for-your-workspace).

### The provider rate limits your requests

[Section titled “The provider rate limits your requests”](#the-provider-rate-limits-your-requests)

The provider rejected calls because of its rate limit or quota, usually with a `429`. Retry with exponential backoff, and cache read results where you can. With [your own OAuth credentials](/agentkit/advanced/bring-your-own-oauth/), your app gets its own quota at providers that set quotas per OAuth app.

## Get help

[Section titled “Get help”](#get-help)

Open **AgentKit** > **Connected Accounts** in the Scalekit dashboard to see the account’s status and tool call logs. When you contact , include:

* The Auth Request ID from the connection error page, if there was one
* The connected account ID, or the user’s identifier and the connection name
* The full error text and when it happened
* The steps that reproduce it

## Next

[Section titled “Next”](#next)

[Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect.

[Listen for account events](/agentkit/account-events/)Get a webhook when an account expires or is disconnected.

---
# DOCUMENT BOUNDARY
---

# Verify users

> Confirm that the person who authorized an AgentKit connection is the user your app meant, with a verification callback in production or Scalekit users only.

User verification applies to OAuth-based connectors only. For API key, basic auth, and key pair connectors, the user provides credentials directly. No OAuth flow, no verification step needed.

For OAuth connectors, **user verification** confirms that the user who completed the OAuth consent is the same user your app intended to connect, before Scalekit activates the connected account. It stops an authorization link from activating the wrong account, for example when a link is forwarded or phished.

Choose a mode in **AgentKit** > **Settings** > **User Verification**:

* **Custom user verifier** (recommended): your server confirms that the authorizing user matches the user your app intended to connect. Use it in production.
* **Scalekit users only**: Scalekit checks that the authorizing user is invited to your Scalekit workspace and signed in to the dashboard. No code required. Use it for development and testing, when every user is on your team.
* **None**: no verification. The connected account activates as soon as OAuth completes, so anyone who opens an authorization link can activate it.

New environments start with **None**, because your agent may run somewhere that can’t confirm which user is signed in. Switch to **Custom user verifier** before you onboard real users.

Scalekit users only is for testing

In this mode, the user authorizing the connection must already be signed in to the Scalekit dashboard. No verify route or API calls are needed in your code. Switch to **Custom user verifier** before onboarding real users.

![User Verification settings in the Scalekit dashboard, with the Custom user verifier, Scalekit users only and None modes](/.netlify/images?url=_astro%2Fuser-verification-config.D0Zf3zDS.png\&w=1162\&h=868\&dpl=6ae471fc143c9a4c27550463)

With **Custom user verifier**, your application implements the verify step. End users never interact with Scalekit directly.

When the user finishes OAuth, Scalekit redirects to your verify URL with `auth_request_id` and `state` params. Your route reads the user from your session, calls Scalekit’s verify API with the `auth_request_id` and the original `identifier`, and if they match, the connected account activates.

Review the verification sequence

![Connected account user verification sequence: authorization link, OAuth consent, authorization code, and redirect to your app for verification](/d2/docs/agentkit/user-verification-0.svg)

## Implement verification in your app

[Section titled “Implement verification in your app”](#implement-verification-in-your-app)

If you haven’t installed the SDK yet, see the [quickstart](/agentkit/quickstart/).

### Generate the authorization link

[Section titled “Generate the authorization link”](#generate-the-authorization-link)

Pass these fields when creating the authorization link:

| Field             | Description                                                                                                                                                                                                                                                                                                                                                                                           |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `identifier`      | **Required.** Your user’s ID in your system, such as `user_123`. Use a stable internal ID, not an email address: a user can change their email, and an old address can be reassigned to someone else, so the identifier would no longer point to the same person. An email is also easy to guess, which makes misuse easier. Scalekit stores it and checks that the verify call sends the same value. |
| `user_verify_url` | **Required.** Your callback URL; Scalekit redirects the user here after OAuth completes.                                                                                                                                                                                                                                                                                                              |
| `state`           | **Recommended.** A random value to prevent CSRF.                                                                                                                                                                                                                                                                                                                                                      |

How to use state

Generate a cryptographically random value per flow, store it in a secure HTTP-only cookie, and validate it against the `state` query param on callback. Discard the request if they don’t match; this prevents an attacker from sending crafted verify URLs to your users.

* Python

  ```python
  import secrets


  # Generate a state value to prevent CSRF
  state = secrets.token_urlsafe(32)
  # Store state in a secure, HTTP-only cookie to validate on callback


  response = scalekit_client.actions.get_authorization_link(
      connection_name=connector,
      identifier=user_id,
      user_verify_url="https://app.yourapp.com/user/verify",
      state=state,
  )
  ```

* Node.js

  ```typescript
  import crypto from 'node:crypto';


  // Generate a state value to prevent CSRF
  const state = crypto.randomUUID();
  // Store state in a secure, HTTP-only cookie to validate on callback


  const { link } = await scalekit.actions.getAuthorizationLink({
    identifier: userId,
    connectionName: connector,
    userVerifyUrl: 'https://app.yourapp.com/user/verify',
    state,
  });
  ```

### Handle the verification callback

[Section titled “Handle the verification callback”](#handle-the-verification-callback)

After OAuth completes, Scalekit redirects to your `user_verify_url`:

```http
GET https://app.yourapp.com/user/verify?auth_request_id=req_xyz&state=
```

Validate `state` against your cookie, then call Scalekit’s verify endpoint server-side.

Never trust query params for identity

Read the user’s identity from your own session, not from the URL. Use `state` for session correlation only.

* Python

  ```python
  # 1. Validate state from query param matches state in cookie
  # 2. Read user identity from your session, not from the URL


  response = scalekit_client.actions.verify_connected_account_user(
      auth_request_id=auth_request_id,
      identifier=user_id,    # must match what was stored at link creation
  )
  # On success: redirect to response.post_user_verify_redirect_url
  ```

* Node.js

  ```typescript
  // 1. Validate state from query param matches state in cookie
  // 2. Read user identity from your session, not from the URL


  const { postUserVerifyRedirectUrl } =
    await scalekit.actions.verifyConnectedAccountUser({
      authRequestId: auth_request_id,
      identifier: userId,     // must match what was stored at link creation
    });
  // On success: redirect to postUserVerifyRedirectUrl
  ```

On success, the connected account is activated. Redirect the user using `post_user_verify_redirect_url`.

## Common scenarios

[Section titled “Common scenarios”](#common-scenarios)

Why does creating the authorization link fail with `user_verify_url is required when user_verify_mode is b2b`?

The environment is set to **Custom user verifier**, and the request for the authorization link didn’t include `user_verify_url`. The call fails with a `400` before any link is created. Pass `user_verify_url` and `state` when you [generate the authorization link](#generate-the-authorization-link), or, for internal testing, switch the mode to **Scalekit users only**.

Why does authorization fail when no verification redirect URL is configured?

The authorization flow fails with a `failed_to_exchange_token` error (`user_verify_url not configured for verification redirect`) when the environment is set to **Custom user verifier**, but the flow started without a `user_verify_url`. Scalekit completes the OAuth exchange but has nowhere to redirect the user for verification.

Resolve it in one of two ways:

* **In production**, pass `user_verify_url` when you generate the authorization link, and implement the [verification callback](#handle-the-verification-callback) at that URL.
* **In development or internal testing**, set the mode to **Scalekit users only** in **AgentKit** > **Settings** > **User Verification**. This mode needs no `user_verify_url` and no verify route, as long as every authorizing user is signed in to your Scalekit dashboard.

Why does the connection page say “Session expired or not available”?

The environment uses **Scalekit users only**, and the person authorizing isn’t signed in to the Scalekit dashboard. Sign in to the dashboard in the same browser and start again, or switch to **Custom user verifier** for users who aren’t on your team. See [`session_not_found`](/agentkit/troubleshooting/#session-expired-or-not-available-session_not_found).

## Next

[Section titled “Next”](#next)

[Manage connected accounts](/agentkit/connected-accounts/)Check an account's status and ask users to reconnect.

[Troubleshoot connection and OAuth errors](/agentkit/troubleshooting/)Find the error your user saw and how to fix it.

---
# DOCUMENT BOUNDARY
---

# User authentication flow

> Learn how Scalekit routes users through authentication based on login method and organization SSO policies.

The user’s authentication journey on the hosted login page can differ based on the **login method** they choose and the **organization policies** configured in Scalekit.

## Organization policies

[Section titled “Organization policies”](#organization-policies)

Organizations can enforce Enterprise SSO for their users. An organization must create an enabled [SSO connection](/authenticate/auth-methods/enterprise-sso/) and add [organization domains](/authenticate/manage-users-orgs/organization-domains/).

Scalekit uses **Home Realm Discovery (HRD)** to determine whether a user’s email domain matches a configured organization domain. When a match is found, the user is routed to that organization’s SSO identity provider.

**Examples**

* A user tries to log in as `user@samecorp.com` on the hosted login page. If `samecorp.com` is registered as an organization domain with SSO enabled, the user is redirected to that organization’s IdP to complete authentication.
* A user tries to log in with Google as `user@samecorp.com` on the hosted login page. If `samecorp.com` is registered as an organization domain with SSO enabled, the user is redirected to that organization’s IdP after returning from Google.

## Login method–specific behavior

[Section titled “Login method–specific behavior”](#login-methodspecific-behavior)

Scalekit allows users to choose different login methods on the hosted login page. The timing of organization domain checks differs slightly by method, but the rules remain consistent.

### Social login

[Section titled “Social login”](#social-login)

* User authenticates with a social IdP (e.g., Google, GitHub).
* Scalekit evaluates the user’s email after social auth completes.
* Home Realm Discovery (HRD) checks whether the email domain matches an organization domain.
* **Domain match:** User is redirected to the organization’s SSO IdP.
* **No match:** Authentication completes.

This ensures that enterprise users must complete SSO authentication even if they initially choose social login.

### Passkey login

[Section titled “Passkey login”](#passkey-login)

* User authenticates using a passkey.
* Authentication succeeds immediately.
* Scalekit performs Home Realm Discovery (HRD) to check the email domain.
* **Domain match:** User is redirected to SSO.
* **No match:** Authentication completes.

Passkeys authenticate the user, but do not override organization SSO policy.

### Email-based login

[Section titled “Email-based login”](#email-based-login)

* User enters their email address.
* Home Realm Discovery (HRD) runs **before authentication** to check the email domain.
* **Domain match:** User is redirected to SSO.
* **No match:** Scalekit performs OTP or magic link verification, then authentication completes.

### Authentication flow

[Section titled “Authentication flow”](#authentication-flow)

This diagram shows the different variations of user’s authentication journey on the hosted login page.

![Authentication flow: the user picks social, passkey or email login, and home realm discovery routes email users to SSO or passwordless](/d2/docs/authenticate/auth-methods/authentication-flow-0.svg)

***

## Enterprise SSO Trust model

[Section titled “Enterprise SSO Trust model”](#enterprise-sso-trust-model)

Most enterprise identity providers (IdPs) like Okta or Microsoft Entra do not prove that a user actually controls the email inbox they sign in with. They only assert an email address in the SAML/OIDC token. Because of this, when a user logs in via Enterprise SSO, Scalekit does not automatically treat that SSO connection as a trusted source of email ownership.

Since Scalekit cannot be sure that the SSO user truly owns the email address, the user is taken through an email ownership check (magic link or OTP) to prove control of that inbox. After the user successfully verifies their email, that SSO connection is marked as a verified channel for that specific user, and they do not need to verify email ownership again on subsequent logins via the same connection.

If you want an Enterprise SSO connection to be treated as a trusted provider for a specific domain, you can assign one or more domains to the organization. Then, for users logging in via that Enterprise SSO connection whose email address matches one of the configured domains, Scalekit skips additional email ownership verification.

| SSO trust case | Example                                                                                                                 | Result                                                         |
| -------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| Trusted SSO    | Org has added `acmecorp.com` in organization domain. User authenticates as `user@acmecorp.com` with organization SSO.   | Email ownership trusted                                        |
| Untrusted SSO  | Org has added `acmecorp.com` in organization domain and user authenticates as `user@foocorp.com` with organization SSO. | Email ownership not trusted → Additional verification required |

***

## Forcing SSO from your application

[Section titled “Forcing SSO from your application”](#forcing-sso-from-your-application)

Your app can override Home Realm Discovery (HRD) by passing ‎`organization_id` or ‎`connection_id` in the authentication request ↗ to Scalekit. When you do this:

* Scalekit skips HRD and redirects the user directly to the specified SSO IdP.
* After SSO authentication completes, Scalekit checks whether the user’s email domain matches one of the organization domains configured on that SSO connection.
* **Domain match**: authentication completes.
* **No match**: Scalekit requires additional verification (OTP or magic link) before completing authentication.

## IdP‑initiated SSO

[Section titled “IdP‑initiated SSO”](#idpinitiated-sso)

In IdP‑initiated SSO, authentication starts at the identity provider instead of your application or the hosted login page. After the IdP authenticates the user and redirects to Scalekit, Scalekit evaluates email ownership trust:

* If the user’s email domain matches one of the organization domains configured on the SSO connection, authentication completes.
* If the email domain does not match, Scalekit requires additional verification (OTP or magic link) before completing authentication.

This workflow ensures IdP‑initiated flows follow the same email ownership and trust guarantees as app‑initiated SSO

***

## Account linking

[Section titled “Account linking”](#account-linking)

### What happens

[Section titled “What happens”](#what-happens)

Scalekit maintains a single user record per email address. For example, if a user first authenticates with passwordless login (magic link/OTP) and later uses Google or Enterprise SSO, Scalekit links both identities to the same user record. These identities are stored on the user object for your app to read if needed. This avoids duplicate users when people switch authentication methods.

### Why it is safe

[Section titled “Why it is safe”](#why-it-is-safe)

Scalekit only treats an SSO IdP as a trusted source of email ownership when:

* the authenticated email domain matches one of the organization domains configured on the SSO connection, or
* the user has previously proven email ownership via magic link or OTP.

Because the organization has proven domain ownership, and/or the user has proven inbox control, emails from that SSO connection are treated as valid. This prevents attackers from linking identities unless email ownership has been verified through trusted mechanisms.

---
# DOCUMENT BOUNDARY
---

# Implement enterprise SSO

> How to implement enterprise SSO for your application

Enterprise single sign-on (SSO) enables users to authenticate using their organization’s identity provider (IdP), such as Okta, Azure AD, or Google Workspace. [After completing the quickstart](/authenticate/fsa/quickstart/), follow this guide to implement SSO for an organization, streamline admin onboarding, enforce login requirements, and validate your configuration.

1. ## Enable SSO for the organization

   [Section titled “Enable SSO for the organization”](#enable-sso-for-the-organization)

   When a user signs up for your application, Scalekit automatically creates an organization and assigns an admin role to the user. Provide an option in your user interface to enable SSO for the organization or workspace.

   Here’s how you can do that with Scalekit. Use the following SDK method to activate SSO for the organization:

   * Node.js

     Enable SSO

     ```javascript
     const settings = {
       features: [
         {
           name: 'sso',
           enabled: true,
         }
       ],
     };


     await scalekit.organization.updateOrganizationSettings(
       '', // Get this from the idToken or accessToken
       settings
     );
     ```

   * Python

     Enable SSO

     ```python
     settings = [
         {
             "name": "sso",
             "enabled": True
         }
     ]


     scalekit.organization.update_organization_settings(
         organization_id='',  # Get this from the idToken or accessToken
         settings=settings
     )
     ```

   * Java

     Enable SSO

     ```java
     OrganizationSettingsFeature featureSSO = OrganizationSettingsFeature.newBuilder()
             .setName("sso")
             .setEnabled(true)
             .build();


     updatedOrganization = scalekitClient.organizations()
             .updateOrganizationSettings(organizationId, List.of(featureSSO));
     ```

   * Go

     Enable SSO

     ```go
     settings := OrganizationSettings{
         Features: []Feature{
             {
                 Name:    "sso",
                 Enabled: true,
             },
         },
     }


     organization, err := sc.Organization().UpdateOrganizationSettings(ctx, organizationId, settings)
     if err != nil {
         // Handle error
     }
     ```

   You can also enable this from the [organization settings](/authenticate/fsa/user-management-settings/) in the Scalekit dashboard.

2. ## Enable admin portal for enterprise customer onboarding

   [Section titled “Enable admin portal for enterprise customer onboarding”](#enable-admin-portal-for-enterprise-customer-onboarding)

   After SSO is enabled for that organization, provide a method for configuring a SSO connection with the organization’s identity provider.

   Scalekit offers two primary approaches:

   * Generate a link to the admin portal from the Scalekit dashboard and share it with organization admins via your usual channels.
   * Or embed the admin portal in your application in an inline frame so administrators can configure their IdP without leaving your app.

   [See how to onboard enterprise customers](/sso/guides/onboard-enterprise-customers/)

3. ## Identify and enforce SSO for organization users

   [Section titled “Identify and enforce SSO for organization users”](#identify-and-enforce-sso-for-organization-users)

   Administrators typically register [organization-owned domains](/authenticate/manage-users-orgs/organization-domains/) through the admin portal. When a user attempts to sign in with an email address matching a registered domain, they are automatically redirected to their organization’s designated identity provider for authentication. This is also known as **Home Realm Discovery**.

   **Organization domains** automatically route users to the correct SSO connection based on their email address. When a user signs in with an email domain that matches a registered organization domain, Scalekit redirects them to that organization’s SSO provider and enforces SSO login.

   For example, if an organization registers `megacorp.org`, any user signing in with an `joe@megacorp.org` email address is redirected to Megacorp’s SSO provider.

   ![Scalekit organization overview with the Organization Domains section showing a verified domain used for Home Realm Discovery](/.netlify/images?url=_astro%2Forganization_domain.CYaGBzer.png\&w=2940\&h=1592\&dpl=6ae471fc143c9a4c27550463)

   Navigate to **Dashboard > Organizations** and select the target organization > **Overview** > **Organization Domains** section to register organization domains.

4. ## Test your SSO integration

   [Section titled “Test your SSO integration”](#test-your-sso-integration)

   Scalekit offers a “Test Organization” feature that enables SSO flow validation without requiring test accounts from your customers’ identity providers.

   To quickly test the integration, enter an email address using the domains `joe@example.com` or `jane@example.org`. This will trigger a redirect to the IdP simulator, which serves as the test organization’s identity provider for authentication.

   For a comprehensive step-by-step walkthrough, refer to the [Test SSO integration guide](/sso/guides/test-sso/).

---
# DOCUMENT BOUNDARY
---

# Add passkeys login method

> Enable passkey authentication for your users

Passkeys replace passwords with biometric authentication (fingerprint, face recognition) or device PINs. Built on FIDO® standards (WebAuthn and CTAP), passkeys offer superior security by eliminating phishing and credential stuffing vulnerabilities, while also providing a seamless one-tap login experience. Unlike traditional authentication methods, passkeys sync across devices, removing the need for multiple enrollments and providing better recovery options when devices are lost.

Your [existing Scalekit integration](/authenticate/fsa/quickstart) already supports passkeys. To implement, enable passkeys in the Scalekit dashboard and leverage Scalekit’s built-in user passkey registration functionality.

1. ## Enable passkeys in the Scalekit dashboard

   [Section titled “Enable passkeys in the Scalekit dashboard”](#enable-passkeys-in-the-scalekit-dashboard)

   Go to Scalekit Dashboard > Authentication > Auth methods > Passkeys and click “Enable”

   ![Enable passkeys button in Scalekit settings](/.netlify/images?url=_astro%2Fenable-btn.C08G8Owj.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

2. ## Manage passkey registration

   [Section titled “Manage passkey registration”](#manage-passkey-registration)

   Let users manage passkeys just by redirecting them to Scalekit from your app (usually through a button in your app that says “Manage passkeys”), or building your own UI.

   #### Using Scalekit UI

   [Section titled “Using Scalekit UI”](#using-scalekit-ui)

   To enable users to register and manage their passkeys, redirect them to the Scalekit passkey registration page.

   ![Passkey registration page in Scalekit UI](/.netlify/images?url=_astro%2Fbetter-registration-page.CMqMT27T.png\&w=2968\&h=1397\&dpl=6ae471fc143c9a4c27550463)

   Construct the URL by appending `/ui/profile/passkeys` to your Scalekit environment URL

   Passkey Registration URL

   ```js
   /ui/profile/passkeys
   ```

   This opens a page where users can:

   * Register new passkeys
   * Remove existing passkeys
   * View their registered passkeys

   Note

   Scalekit registers & authenticates user’s passkeys through the browser’s native passkey API. This API prompts users to authenticate with device-supported passkeys — such as fingerprint, PIN, or password managers.

   To show the page on your own domain instead of the default Scalekit domain, configure a [branded custom domain](/guides/custom-domain). Users then see a URL such as `https://auth.yourapp.com/ui/profile/passkeys`.

   #### In your own UI

   [Section titled “In your own UI”](#in-your-own-ui)

   If you prefer to create a custom user interface for passkey management, Scalekit offers comprehensive APIs that enable you to build a personalized experience. These APIs allow you to list registered passkeys, rename them, and remove them entirely. However registration of passkeys is only supported through the Scalekit UI.

   * Node.js

     List user's passkeys

     ```js
     // : fetch from Access Token or ID Token after identity verification
     const res = await fetch(
       '/api/v1/webauthn/credentials?user_id=',
       { headers: { Authorization: 'Bearer ' } }
     );
     const data = await res.json();
     console.log(data);
     ```

     Rename a passkey

     ```js
     // : obtained from list response (id of each passkey)
     await fetch('/api/v1/webauthn/credentials/', {
       method: 'PATCH',
       headers: {
         'Content-Type': 'application/json',
         Authorization: 'Bearer '
       },
       body: JSON.stringify({ display_name: '' })
     });
     ```

     Remove a passkey

     ```js
     // : obtained from list response (id of each passkey)
     await fetch('/api/v1/webauthn/credentials/', {
       method: 'DELETE',
       headers: { Authorization: 'Bearer ' }
     });
     ```

   * Python

     List user's passkeys

     ```python
     import requests
     # : fetch from access token or ID token after identity verification
     r = requests.get(
       '/api/v1/webauthn/credentials',
       params={'user_id': ''},
       headers={'Authorization': 'Bearer '}
     )
     print(r.json())
     ```

     Rename a passkey

     ```python
     import requests
     # : obtained from list response (id of each passkey)
     requests.patch(
         '/api/v1/webauthn/credentials/',
         json={'display_name': ''},
         headers={'Authorization': 'Bearer '}
     )
     ```

     Remove a passkey

     ```python
     import requests
     # : obtained from list response (id of each passkey)
     requests.delete(
       '/api/v1/webauthn/credentials/',
       headers={'Authorization': 'Bearer '}
     )
     ```

   * Java

     List user's passkeys

     ```java
     var client = java.net.http.HttpClient.newHttpClient();
     // : fetch from Access Token or ID Token after identity verification
     var req = java.net.http.HttpRequest.newBuilder(
       java.net.URI.create("/api/v1/webauthn/credentials?user_id=")
     )
     .header("Authorization", "Bearer ")
     .GET().build();
     var res = client.send(req, java.net.http.HttpResponse.BodyHandlers.ofString());
     System.out.println(res.body());
     ```

     Rename a passkey

     ```java
     var client = java.net.http.HttpClient.newHttpClient();
     var body = "{\"display_name\":\"\"}";
     // : obtained from list response (id of each passkey)
     var req = java.net.http.HttpRequest.newBuilder(
       java.net.URI.create("/api/v1/webauthn/credentials/")
     )
     .header("Authorization", "Bearer ")
     .header("Content-Type","application/json")
     .method("PATCH", java.net.http.HttpRequest.BodyPublishers.ofString(body))
     .build();
     client.send(req, java.net.http.HttpResponse.BodyHandlers.discarding());
     ```

     Remove a passkey

     ```java
     var client = java.net.http.HttpClient.newHttpClient();
     // : obtained from list response (id of each passkey)
     var req = java.net.http.HttpRequest.newBuilder(
       java.net.URI.create("/api/v1/webauthn/credentials/")
     )
     .header("Authorization", "Bearer ")
     .DELETE().build();
     client.send(req, java.net.http.HttpResponse.BodyHandlers.discarding());
     ```

   * Go

     List user's passkeys

     ```go
     // imports: net/http, io, fmt
     // : fetch from access token or ID token after identity verification
     req, _ := http.NewRequest("GET", "/api/v1/webauthn/credentials?user_id=", nil)
     req.Header.Set("Authorization", "Bearer ")
     resp, _ := http.DefaultClient.Do(req)
     defer resp.Body.Close()
     b, _ := io.ReadAll(resp.Body)
     fmt.Println(string(b))
     ```

     Rename a passkey

     ```go
     // imports: net/http, bytes
     payload := bytes.NewBufferString(`{"display_name":""}`)
     // : obtained from list response (id of each passkey)
     req, _ := http.NewRequest("PATCH", "/api/v1/webauthn/credentials/", payload)
     req.Header.Set("Content-Type", "application/json")
     req.Header.Set("Authorization", "Bearer ")
     http.DefaultClient.Do(req)
     ```

     Remove a passkey

     ```go
     // imports: net/http
     // : obtained from list response (id of each passkey)
     req, _ := http.NewRequest("DELETE", "/api/v1/webauthn/credentials/", nil)
     req.Header.Set("Authorization", "Bearer ")
     http.DefaultClient.Do(req)
     ```

   Note

   All API requests require an access token obtained via the OAuth 2.0 client credentials flow. Follow [Authenticate with the Scalekit API](/guides/authenticate-scalekit-api), then replace `` in the examples below.

3. ## Users can log in with passkeys

   [Section titled “Users can log in with passkeys”](#users-can-log-in-with-passkeys)

   Users who have registered passkeys can log in with them.

   This time when login page shows, users can select “Passkey” as the authentication method.

   ![Login with passkey option on sign-in page](/.netlify/images?url=_astro%2Flogin-with-passkey.ZZ6-wNXH.png\&w=2978\&h=1800\&dpl=6ae471fc143c9a4c27550463)

   During sign-up, you’ll continue to use established authentication methods like [verification codes, magic links](/authenticate/auth-methods/passwordless/) or [social logins](/authenticate/auth-methods/social-logins/). Once a user is registered, they can then add passkeys as an additional, convenient login option.

---
# DOCUMENT BOUNDARY
---

# Sign in with  magic link or Email OTP

> Enable passwordless sign-in with email verification codes or magic links

Configure Magic Link & OTP to enable passwordless authentication for your application. After completing the [quickstart guide](/authenticate/fsa/quickstart/), set up email verification codes or magic links so users can sign in without passwords.

Switch between those passwordless methods without modifying any code:

| Method                         | How it works                                                     | Best for                                     |
| ------------------------------ | ---------------------------------------------------------------- | -------------------------------------------- |
| Verification code              | Users receive a one-time code via email and enter it in your app | Applications requiring explicit verification |
| Magic link                     | Users click a link in their email to authenticate                | Quick, frictionless sign-in                  |
| Magic link + Verification code | Users choose either method                                       | Maximum flexibility and user choice          |

## Configure magic link or OTP

[Section titled “Configure magic link or OTP”](#configure-magic-link-or-otp)

In the Scalekit dashboard, go to **Authentication > Auth methods > Magic Link & OTP**

![Scalekit dashboard Magic link & OTP settings showing the login method selection and expiry period fields](/.netlify/images?url=_astro%2F1.1jnMxeBv.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

1. ### Select authentication method

   [Section titled “Select authentication method”](#select-authentication-method)

   Choose one of three methods:

   * **Verification code** - Users enter a 6-digit code sent to their email
   * **Magic link** - Users click a link in their email to authenticate
   * **Magic link + Verification code** - Users can choose either method



2. ### Set expiry period

   [Section titled “Set expiry period”](#set-expiry-period)

   Configure how long verification codes and magic links remain valid:

   * **Default**: 300 seconds (5 minutes)
   * **Range**: 60 to 3600 seconds
   * **Recommendation**: 300 seconds balances security and usability

   Note

   While shorter expiry periods enhance security by reducing the window for potential unauthorized access, they can negatively impact user experience, especially with shorter email-to-input times. Conversely, longer periods provide more convenience but increase the risk of credential misuse if intercepted.

## Enforce same browser origin

[Section titled “Enforce same browser origin”](#enforce-same-browser-origin)

When enforcing same browser origin, users are required to complete magic link authentication within the same browser where they initiated the login process. This security feature is particularly recommended for applications dealing with sensitive data or financial transactions, as it adds an extra layer of protection against potential unauthorized access attempts.

**Example scenario**: A healthcare app where a user requests a magic link on their laptop. If someone intercepts the email and tries to open it on a different device, the authentication fails.

## Regenerate credentials on resend

[Section titled “Regenerate credentials on resend”](#regenerate-credentials-on-resend)

When a user requests a new Magic Link or Email OTP, the system generates a fresh code or link while automatically invalidating the previous one. This approach is recommended for all applications as a critical security measure to prevent potential misuse of compromised credentials.

**Example scenario**: A user requests a verification code but doesn’t receive it. They request a new code. With this setting enabled, the first code becomes invalid, preventing unauthorized access if the original email was intercepted.

---
# DOCUMENT BOUNDARY
---

# Add social login to your app

> Implement authentication with Google, Microsoft, GitHub, and other social providers

First, complete the [quickstart guide](/authenticate/fsa/quickstart/) to integrate Scalekit auth into your application. Scalekit natively supports OAuth 2.0, enabling you to easily configure social login providers that will automatically appear as authentication options on your login page.

1. ## Configure social login providers

   [Section titled “Configure social login providers”](#configure-social-login-providers)

   Google login is pre-configured in all development environments for simplified testing. You can integrate additional social login providers by setting up your own connection credentials with each provider.

   Navigate to **Authentication** > **Auth Methods** > **Social logins** in your dashboard to configure these settings

   ### Google

   Enable users to sign in with their Google accounts using OAuth 2.0

   [Setup →](/guides/integrations/social-connections/google)

   ### GitHub

   Allow users to authenticate using their GitHub credentials

   [Setup →](/guides/integrations/social-connections/github)

   ### Microsoft

   Integrate Microsoft accounts for seamless user authentication

   [Setup →](/guides/integrations/social-connections/microsoft)

   ### GitLab

   Enable GitLab-based authentication for your application

   [Setup →](/guides/integrations/social-connections/gitlab)

   ### LinkedIn

   Let users sign in with their LinkedIn accounts using OAuth 2.0

   [Setup →](/guides/integrations/social-connections/linkedin)

   ### Salesforce

   Enable Salesforce-based authentication for your application

   [Setup →](/guides/integrations/social-connections/salesforce)

   ### Facebook

   Let users sign in with their Facebook accounts using OAuth 2.0

   [Setup →](/guides/integrations/social-connections/facebook/)

2. ## Test the social connection

   [Section titled “Test the social connection”](#test-the-social-connection)

   After configuration, test the social connection by clicking on “Test Connection” in the dashboard. You will be redirected to the provider’s consent screen to authorize access. A summary table will show the information that will be sent to your app.

   ![Scalekit Test Connection panel for Google showing sign-in test instructions, common error causes, and a Retry Test button](/.netlify/images?url=_astro%2Ftest-connection.8nGwOF1-.png\&w=2468\&h=1374\&dpl=6ae471fc143c9a4c27550463)

## Access social login options on your login page

[Section titled “Access social login options on your login page”](#access-social-login-options-on-your-login-page)

Your application now supports social logins.

Begin the [login process](/authenticate/fsa/implement-login/) to experience the available social login options. Users can authenticate using providers like Google, GitHub, Microsoft, and any others you have set up.

---
# DOCUMENT BOUNDARY
---

# Assign roles to users

> Learn how to assign roles to users in your application using to dashboard, SDK, or automated provisioning

After registering roles and permissions for your application, Scalekit provides multiple ways to assign roles to users. These roles allow your app to make the access control decisions as scalekit sends them to your app in the access token.

## Auto assign roles as users join organizations

[Section titled “Auto assign roles as users join organizations”](#auto-assign-roles-as-users-join-organizations)

By default, the organization creator automatically receives the `admin` role, while users who join later receive the `member` role. You can customize these defaults to match your application’s security requirements. For instance, in a CRM system, you may want to set the default role for new members to a read-only role like `viewer` to prevent accidental data modifications.

1. Go to **Dashboard** > **Roles & Permissions** > **Roles** tab
2. Select the roles available and choose defaults for organization creator and member

![Roles & Permissions page listing admin, member, and viewer roles, with a Default Roles panel for choosing the creator and member roles](/.netlify/images?url=_astro%2Ffull-page-highlighth-defaults.Cs9-9nAm.png\&w=3098\&h=1896\&dpl=6ae471fc143c9a4c27550463)

This automatically assigns these roles to every users who joins any organization in your Scalekit environment.

## Set a default role for new organization members

[Section titled “Set a default role for new organization members”](#set-a-default-role-for-new-organization-members)

You can also configure a default role that is automatically assigned to users who join a specific organization. This organization-level setting **overrides** the application-level default role described above, allowing finer-grained control per organization. ![Organization roles view with a Default organization role dropdown for assigning a role to new users joining that organization](/.netlify/images?url=_astro%2Fdefault_org_member_role.DzatyaVW.png\&w=2932\&h=1588\&dpl=6ae471fc143c9a4c27550463)

## Let users assign roles to others API

[Section titled “Let users assign roles to others ”](#let-users-assign-roles-to-others-)

Enable organization administrators to manage user roles directly within your application. By building features like “Change role” or “Assign permissions” into your app, you can provide a management experience without requiring administrators to leave your app.

To implement role assignment functionality, follow these essential prerequisites:

1. **Verify administrator permissions**: Ensure the user performing the role assignment has the `admin` role or an equivalent role with the necessary permissions. Check the `permissions` property in their access token to confirm they have role management capabilities.

   * Node.js

     Verify permissions

     ```javascript
     // Decode JWT and check admin permissions
     const decodedToken = decodeJWT(adminAccessToken);


     // Check if user has admin role or required permissions
     const isAdmin = decodedToken.roles.includes('admin');
     const hasPermission = decodedToken.permissions?.includes('users.write') ||
                         decodedToken.permissions?.includes('roles.assign');


     if (!isAdmin && !hasPermission) {
       throw new Error('Insufficient permissions to assign roles');
     }
     ```

   * Python

     Verify permissions

     ```python
     # Decode JWT and check admin permissions
     decoded_token = decode_jwt(access_token)


     # Check if user has admin role or required permissions
     is_admin = 'admin' in decoded_token.get('roles', [])
     has_permission = any(perm in decoded_token.get('permissions', [])
                       for perm in ['users.write', 'roles.assign'])


     if not is_admin and not has_permission:
         raise PermissionError("Insufficient permissions to assign roles")
     ```

   * Go

     Verify permissions

     ```go
     1
     // Decode JWT and check admin permissions
     2
     decodedToken, err := decodeJWT(accessToken)
     3
     if err != nil {
     4
         return ValidationResult{Success: false, Error: "Invalid token"}
     5
     }
     6


     7
     // Check if user has admin role or required permissions
     8
     roles := decodedToken["roles"].([]interface{})
     9
     permissions := decodedToken["permissions"].([]interface{})
     10


     11
     isAdmin := false
     12
     hasPermission := false
     13


     14
     for _, role := range roles {
     15
         if role == "admin" {
     16
             isAdmin = true
     17
             break
     18
         }
     19
     }
     20


     21
     for _, perm := range permissions {
     22
         if perm == "users.write" || perm == "roles.assign" {
     23
             hasPermission = true
     24
             break
     25
         }
     26
     }
     27


     28
     if !isAdmin && !hasPermission {
     29
         return ValidationResult{Success: false, Error: "Insufficient permissions"}
     30
     }
     ```

   * Java

     Verify permissions

     ```java
     1
     // Decode JWT and check admin permissions
     2
     Claims decodedToken = decodeJWT(accessToken);
     3


     4
     @SuppressWarnings("unchecked")
     5
     List userRoles = (List) decodedToken.get("roles");
     6
     @SuppressWarnings("unchecked")
     7
     List permissions = (List) decodedToken.get("permissions");
     8


     9
     // Check if user has admin role or required permissions
     10
     boolean isAdmin = userRoles != null && userRoles.contains("admin");
     11
     boolean hasPermission = permissions != null &&
     12
         (permissions.contains("users.write") || permissions.contains("roles.assign"));
     13


     14
     if (!isAdmin && !hasPermission) {
     15
         throw new SecurityException("Insufficient permissions to assign roles");
     16
     }
     ```

2. **Collect required identifiers**: Gather the necessary parameters for the API call:

   * `user_id`: The unique identifier of the user whose role you’re changing
   * `organization_id`: The organization where the role assignment applies
   * `roles`: An array of role names to assign to the user

   - Node.js

     Collect and validate identifiers

     ```javascript
     // Structure and validate role assignment data
     const roleAssignmentData = {
       user_id: targetUserId,
       organization_id: targetOrgId,
       roles: newRoles,
       // Additional metadata for auditing
       performed_by: decodedToken.sub,
       timestamp: new Date().toISOString()
     };


     // Validate required fields
     if (!roleAssignmentData.user_id || !roleAssignmentData.organization_id || !roleAssignmentData.roles) {
       throw new Error('Missing required identifiers for role assignment');
     }
     ```

   - Python

     Collect and validate identifiers

     ```python
     # Structure and validate role assignment data
     role_assignment_data = {
         'user_id': target_user_id,
         'organization_id': target_org_id,
         'roles': new_roles,
         # Additional metadata for auditing
         'performed_by': decoded_token.get('sub'),
         'timestamp': datetime.utcnow().isoformat()
     }


     # Validate required fields
     if not all([role_assignment_data['user_id'],
               role_assignment_data['organization_id'],
               role_assignment_data['roles']]):
         raise ValueError("Missing required identifiers for role assignment")
     ```

   - Go

     Collect and validate identifiers

     ```go
     // Structure and validate role assignment data
     roleAssignmentData := map[string]interface{}{
         "user_id":         req.UserID,
         "organization_id": req.OrganizationID,
         "roles":          req.Roles,
         // Additional metadata for auditing
         "performed_by":   decodedToken["sub"],
         "timestamp":      time.Now().UTC().Format(time.RFC3339),
     }


     // Validate required fields
     if req.UserID == "" || req.OrganizationID == "" || len(req.Roles) == 0 {
         return ValidationResult{Success: false, Error: "Missing required identifiers"}
     }
     ```

   - Java

     Collect and validate identifiers

     ```java
     // Structure and validate role assignment data
     Map roleAssignmentData = new HashMap<>();
     roleAssignmentData.put("user_id", request.userId);
     roleAssignmentData.put("organization_id", request.organizationId);
     roleAssignmentData.put("roles", request.roles);


     // Additional metadata for auditing
     roleAssignmentData.put("performed_by", decodedToken.getSubject());
     roleAssignmentData.put("timestamp", Instant.now().toString());


     // Validate required fields
     if (request.userId == null || request.organizationId == null || request.roles == null) {
         throw new IllegalArgumentException("Missing required identifiers for role assignment");
     }
     ```

3. **Call Scalekit SDK to update user role**: Use the validated data to make the API call that assigns the new roles to the user through the Scalekit membership update endpoint.

   * Node.js

     Update user role with Scalekit SDK

     ```javascript
     1
     // Use case: Update user membership after validation
     2
     const validationResult = await prepareRoleAssignment(
     3
       adminAccessToken,
     4
       targetUserId,
     5
       targetOrgId,
     6
       newRoles
     7
     );
     8


     9
     if (!validationResult.success) {
     10
       return res.status(403).json({ error: validationResult.error });
     11
     }
     12


     13
     // Initialize Scalekit client (reference installation guide for setup)
     14
     const scalekit = new ScalekitClient(
     15
       process.env.SCALEKIT_ENVIRONMENT_URL,
     16
       process.env.SCALEKIT_CLIENT_ID,
     17
       process.env.SCALEKIT_CLIENT_SECRET
     18
     );
     19


     20
     // Make the API call to update user roles
     21
     try {
     22
       const result = await scalekit.user.updateMembership({
     23
         user_id: validationResult.data.user_id,
     24
         organization_id: validationResult.data.organization_id,
     25
         roles: validationResult.data.roles
     26
       });
     27


     28
       console.log(`Role assigned successfully:`, result);
     29
       return res.json({
     30
         success: true,
     31
         message: "Role updated successfully",
     32
         data: result
     33
       });
     34
     } catch (error) {
     35
       console.error(`Failed to assign role: ${error.message}`);
     36
       return res.status(500).json({
     37
         error: "Failed to update role",
     38
         details: error.message
     39
       });
     40
     }
     ```

   * Python

     Update user role with Scalekit SDK

     ```python
     1
     # Use case: Update user membership after validation
     2
     validation_result = prepare_role_assignment(
     3
         access_token,
     4
         target_user_id,
     5
         target_org_id,
     6
         new_roles
     7
     )
     8


     9
     if not validation_result['success']:
     10
         return jsonify({'error': validation_result['error']}), 403
     11


     12
     # Initialize Scalekit client (reference installation guide for setup)
     13
     scalekit_client = ScalekitClient(
     14
         env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
     15
         client_id=os.getenv("SCALEKIT_CLIENT_ID"),
     16
         client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
     17
     )
     18


     19
     # Make the API call to update user roles
     20
     try:
     21
         from scalekit.v1.users.users_pb2 import UpdateMembershipRequest
     22


     23
         request = UpdateMembershipRequest(
     24
             user_id=validation_result['data']['user_id'],
     25
             organization_id=validation_result['data']['organization_id'],
     26
             roles=validation_result['data']['roles']
     27
         )
     28


     29
         result = scalekit_client.users.update_membership(request=request)
     30
         print(f"Role assigned successfully: {result}")
     31


     32
         return jsonify({
     33
             'success': True,
     34
             'message': 'Role updated successfully',
     35
             'data': str(result)
     36
         })
     37


     38
     except Exception as error:
     39
         print(f"Failed to assign role: {error}")
     40
         return jsonify({
     41
             'error': 'Failed to update role',
     42
             'details': str(error)
     43
         }), 500
     ```

   * Go

     Update user role with Scalekit SDK

     ```go
     1
     // Use case: Update user membership after validation
     2
     validationResult := prepareRoleAssignment(ctx, accessToken, req)
     3


     4
     if !validationResult.Success {
     5
         http.Error(w, validationResult.Error, http.StatusForbidden)
     6
         return
     7
     }
     8


     9
     // Initialize Scalekit client (reference installation guide for setup)
     10
     scalekitClient := scalekit.NewScalekitClient(
     11
         os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
     12
         os.Getenv("SCALEKIT_CLIENT_ID"),
     13
         os.Getenv("SCALEKIT_CLIENT_SECRET"),
     14
     )
     15


     16
     // Make the API call to update user roles
     17
     data := validationResult.Data.(map[string]interface{})
     18
     updateRequest := &scalekit.UpdateMembershipRequest{
     19
         UserId:         data["user_id"].(string),
     20
         OrganizationId: data["organization_id"].(string),
     21
         Roles:          data["roles"].([]string),
     22
     }
     23


     24
     result, err := scalekitClient.Membership().UpdateMembership(ctx, updateRequest)
     25
     if err != nil {
     26
         log.Printf("Failed to assign role: %v", err)
     27
         http.Error(w, "Failed to update role", http.StatusInternalServerError)
     28
         return
     29
     }
     30


     31
     log.Printf("Role assigned successfully: %+v", result)
     32
     json.NewEncoder(w).Encode(map[string]interface{}{
     33
         "success": true,
     34
         "message": "Role updated successfully",
     35
         "data":    result,
     36
     })
     ```

   * Java

     Update user role with Scalekit SDK

     ```java
     1
     // Use case: Update user membership after validation
     2
     ValidationResult validationResult = prepareRoleAssignment(accessToken, request);
     3


     4
     if (!validationResult.success) {
     5
         return ResponseEntity.status(403).body(Map.of("error", validationResult.error));
     6
     }
     7


     8
     // Initialize Scalekit client (reference installation guide for setup)
     9
     ScalekitClient scalekitClient = new ScalekitClient(
     10
         System.getenv("SCALEKIT_ENVIRONMENT_URL"),
     11
         System.getenv("SCALEKIT_CLIENT_ID"),
     12
         System.getenv("SCALEKIT_CLIENT_SECRET")
     13
     );
     14


     15
     // Make the API call to update user roles
     16
     try {
     17
         @SuppressWarnings("unchecked")
     18
         Map data = (Map) validationResult.data;
     19


     20
         UpdateMembershipRequest updateRequest = UpdateMembershipRequest.newBuilder()
     21
             .setUserId((String) data.get("user_id"))
     22
             .setOrganizationId((String) data.get("organization_id"))
     23
             .addAllRoles((List) data.get("roles"))
     24
             .build();
     25


     26
         UpdateMembershipResponse response = scalekitClient.users().updateMembership(updateRequest);
     27
         System.out.println("Role assigned successfully: " + response);
     28


     29
         return ResponseEntity.ok(Map.of(
     30
             "success", true,
     31
             "message", "Role updated successfully",
     32
             "data", response.toString()
     33
         ));
     34


     35
     } catch (Exception e) {
     36
         System.err.println("Failed to assign role: " + e.getMessage());
     37
         return ResponseEntity.status(500).body(Map.of(
     38
             "error", "Failed to update role",
     39
             "details", e.getMessage()
     40
         ));
     41
     }
     ```

4. **Handle response and provide feedback**: Return appropriate success/error responses to the administrator and update your application’s UI accordingly.

   * Node.js

     Handle API response

     ```javascript
     1
     // Success response handling
     2
     if (result.success) {
     3
       // Update UI to reflect role change
     4
       await updateUserInterface(targetUserId, newRoles);
     5


     6
       // Send notification to user (optional)
     7
       await notifyUserOfRoleChange(targetUserId, newRoles);
     8


     9
       // Log the action for audit purposes
     10
       await logRoleChange({
     11
         performed_by: decodedToken.sub,
     12
         target_user: targetUserId,
     13
         organization: targetOrgId,
     14
         old_roles: previousRoles,
     15
         new_roles: newRoles,
     16
         timestamp: new Date().toISOString()
     17
       });
     18
     }
     ```

   * Python

     Handle API response

     ```python
     1
     # Success response handling
     2
     if result.get('success'):
     3
         # Update UI to reflect role change
     4
         await update_user_interface(target_user_id, new_roles)
     5


     6
         # Send notification to user (optional)
     7
         await notify_user_of_role_change(target_user_id, new_roles)
     8


     9
         # Log the action for audit purposes
     10
         await log_role_change({
     11
             'performed_by': decoded_token.get('sub'),
     12
             'target_user': target_user_id,
     13
             'organization': target_org_id,
     14
             'old_roles': previous_roles,
     15
             'new_roles': new_roles,
     16
             'timestamp': datetime.utcnow().isoformat()
     17
         })
     ```

   * Go

     Handle API response

     ```go
     1
     // Success response handling
     2
     if success {
     3
         // Update UI to reflect role change
     4
         updateUserInterface(targetUserID, newRoles)
     5


     6
         // Send notification to user (optional)
     7
         notifyUserOfRoleChange(targetUserID, newRoles)
     8


     9
         // Log the action for audit purposes
     10
         logRoleChange(map[string]interface{}{
     11
             "performed_by": decodedToken["sub"],
     12
             "target_user": targetUserID,
     13
             "organization": targetOrgID,
     14
             "old_roles": previousRoles,
     15
             "new_roles": newRoles,
     16
             "timestamp": time.Now().UTC().Format(time.RFC3339),
     17
         })
     18
     }
     ```

   * Java

     Handle API response

     ```java
     1
     // Success response handling
     2
     if (response.getBody().containsKey("success") &&
     3
         Boolean.TRUE.equals(response.getBody().get("success"))) {
     4


     5
         // Update UI to reflect role change
     6
         updateUserInterface(targetUserId, newRoles);
     7


     8
         // Send notification to user (optional)
     9
         notifyUserOfRoleChange(targetUserId, newRoles);
     10


     11
         // Log the action for audit purposes
     12
         logRoleChange(Map.of(
     13
             "performed_by", decodedToken.getSubject(),
     14
             "target_user", targetUserId,
     15
             "organization", targetOrgId,
     16
             "old_roles", previousRoles,
     17
             "new_roles", newRoles,
     18
             "timestamp", Instant.now().toString()
     19
         ));
     20
     }
     ```

---
# DOCUMENT BOUNDARY
---

# Create and manage roles and permissions

> Set up roles and permissions to control access in your application

Before writing any code, take a moment to plan your application’s authorization model. A well-designed structure for roles and permissions is crucial for security and maintainability. Start by considering the following questions:

* What are the actions your users can perform?
* How many distinct roles does your application need?

Your application’s use cases will determine the answers. Here are a few common patterns:

* **Simple roles**: Some applications, like an online whiteboarding tool, may only need a few roles with implicit permissions. For example, `Admin`, `Editor`, and `Viewer`. In this case, you might not even need to define granular permissions.

* **Pre-defined roles and permissions**: Many applications have a fixed set of roles built from specific permissions. For a project management tool, you could define permissions like `projects:create` and `tasks:assign`, then group them into roles like `Project Manager` and `Team Member`.

* **Customer-defined Roles**: For complex applications, you might allow organization owners to create custom roles with a specific set of permissions. These roles are specific to an organization rather than global to your application.

Scalekit provides the flexibility to build authorization for any of these use cases. Once you have a clear plan, you can start creating your permissions and roles.

Define the permissions your application needs by registering them with Scalekit. Use the `resource:action` format for clear, self-documenting permission names. You can skip this step, in case permissions may not fit your app’s authorization model.

1. ## Define the actions your users can perform as permissions

   [Section titled “Define the actions your users can perform as permissions”](#define-the-actions-your-users-can-perform-as-permissions)

   * Node.js

     Create permissions

     ```javascript
     9 collapsed lines
     1
     // Initialize Scalekit client
     2
     // Use case: Register all available actions in your project management app
     3
     import { ScalekitClient } from "@scalekit-sdk/node";
     4


     5
     const scalekit = new ScalekitClient(
     6
       process.env.SCALEKIT_ENVIRONMENT_URL,
     7
       process.env.SCALEKIT_CLIENT_ID,
     8
       process.env.SCALEKIT_CLIENT_SECRET
     9
     );
     10


     11
     // Define your application's permissions
     12
     const permissions = [
     13
       {
     14
         name: "projects:create",
     15
         description: "Allows users to create new projects"
     16
       },
     17
       {
     18
         name: "projects:read",
     19
         description: "Allows users to view project details"
     20
       },
     21
       {
     22
         name: "projects:update",
     23
         description: "Allows users to modify existing projects"
     24
       },
     25
       {
     26
         name: "projects:delete",
     27
         description: "Allows users to remove projects"
     28
       },
     29
       {
     30
         name: "tasks:assign",
     31
         description: "Allows users to assign tasks to team members"
     32
       }
     33
     ];
     34


     35
     // Register each permission with Scalekit
     36
     for (const permission of permissions) {
     37
       await scalekit.permission.createPermission(permission);
     38
       console.log(`Created permission: ${permission.name}`);
     39
     }
     40


     41
     // Your application's permissions are now registered with Scalekit
     ```

   * Python

     Create permissions

     ```python
     12 collapsed lines
     1
     # Initialize Scalekit client
     2
     # Use case: Register all available actions in your project management app
     3
     from scalekit import ScalekitClient
     4


     5
     scalekit_client = ScalekitClient(
     6
         env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
     7
         client_id=os.getenv("SCALEKIT_CLIENT_ID"),
     8
         client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
     9
     )
     10


     11
     # Define your application's permissions
     12
     from scalekit.v1.roles.roles_pb2 import CreatePermission
     13


     14
     permissions = [
     15
         CreatePermission(
     16
             name="projects:create",
     17
             description="Allows users to create new projects"
     18
         ),
     19
         CreatePermission(
     20
             name="projects:read",
     21
             description="Allows users to view project details"
     22
         ),
     23
         CreatePermission(
     24
             name="projects:update",
     25
             description="Allows users to modify existing projects"
     26
         ),
     27
         CreatePermission(
     28
             name="projects:delete",
     29
             description="Allows users to remove projects"
     30
         ),
     31
         CreatePermission(
     32
             name="tasks:assign",
     33
             description="Allows users to assign tasks to team members"
     34
         )
     35
     ]
     36


     37
     # Register each permission with Scalekit
     38
     for permission in permissions:
     39
         scalekit_client.permissions.create_permission(permission=permission)
     40
         print(f"Created permission: {permission.name}")
     41


     42
     # Your application's permissions are now registered with Scalekit
     ```

   * Go

     Create permissions

     ```go
     17 collapsed lines
     1
     // Initialize Scalekit client
     2
     // Use case: Register all available actions in your project management app
     3
     package main
     4


     5
     import (
     6
         "context"
     7
         "log"
     8
         "github.com/scalekit-inc/scalekit-sdk-go"
     9
     )
     10


     11
     func main() {
     12
         sc := scalekit.NewScalekitClient(
     13
             os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
     14
             os.Getenv("SCALEKIT_CLIENT_ID"),
     15
             os.Getenv("SCALEKIT_CLIENT_SECRET"),
     16
         )
     17


     18
         // Define your application's permissions
     19
         permissions := []*scalekit.CreatePermission{
     20
             {
     21
                 Name:        "projects:create",
     22
                 Description: "Allows users to create new projects",
     23
             },
     24
             {
     25
                 Name:        "projects:read",
     26
                 Description: "Allows users to view project details",
     27
             },
     28
             {
     29
                 Name:        "projects:update",
     30
                 Description: "Allows users to modify existing projects",
     31
             },
     32
             {
     33
                 Name:        "projects:delete",
     34
                 Description: "Allows users to remove projects",
     35
             },
     36
             {
     37
                 Name:        "tasks:assign",
     38
                 Description: "Allows users to assign tasks to team members",
     39
             },
     40
         }
     41


     42
         // Register each permission with Scalekit
     43
         for _, permission := range permissions {
     44
             _, err := sc.Permission().CreatePermission(ctx, permission)
     45
             if err != nil {
     46
                 log.Printf("Failed to create permission: %s", permission.Name)
     47
                 continue
     48
             }
     49
             fmt.Printf("Created permission: %s\n", permission.Name)
     50
         }
     51


     52
         // Your application's permissions are now registered with Scalekit
     53
     }
     ```

   * Java

     Create permissions

     ```java
     11 collapsed lines
     1
     // Initialize Scalekit client
     2
     // Use case: Register all available actions in your project management app
     3
     import com.scalekit.ScalekitClient;
     4
     import com.scalekit.grpc.scalekit.v1.roles.*;
     5


     6
     ScalekitClient scalekitClient = new ScalekitClient(
     7
         System.getenv("SCALEKIT_ENVIRONMENT_URL"),
     8
         System.getenv("SCALEKIT_CLIENT_ID"),
     9
         System.getenv("SCALEKIT_CLIENT_SECRET")
     10
     );
     11


     12
     // Define your application's permissions
     13
     List permissions = Arrays.asList(
     14
         CreatePermission.newBuilder()
     15
             .setName("projects:create")
     16
             .setDescription("Allows users to create new projects")
     17
             .build(),
     18
         CreatePermission.newBuilder()
     19
             .setName("projects:read")
     20
             .setDescription("Allows users to view project details")
     21
             .build(),
     22
         CreatePermission.newBuilder()
     23
             .setName("projects:update")
     24
             .setDescription("Allows users to modify existing projects")
     25
             .build(),
     26
         CreatePermission.newBuilder()
     27
             .setName("projects:delete")
     28
             .setDescription("Allows users to remove projects")
     29
             .build(),
     30
         CreatePermission.newBuilder()
     31
             .setName("tasks:assign")
     32
             .setDescription("Allows users to assign tasks to team members")
     33
             .build()
     34
     );
     35


     36
     // Register each permission with Scalekit
     37
     for (CreatePermission permission : permissions) {
     38
         try {
     39
             CreatePermissionRequest request = CreatePermissionRequest.newBuilder()
     40
                 .setPermission(permission)
     41
                 .build();
     42


     43
             scalekitClient.permissions().createPermission(request);
     44
             System.out.println("Created permission: " + permission.getName());
     45
         } catch (Exception e) {
     46
             System.err.println("Error creating permission: " + e.getMessage());
     47
         }
     48
     }
     49


     50
     // Your application's permissions are now registered with Scalekit
     ```

2. ## Register roles your applications will use

   [Section titled “Register roles your applications will use”](#register-roles-your-applications-will-use)

   Once you have defined permissions, group them into roles that match your application’s access patterns.

   * Node.js

     Create roles with permissions

     ```javascript
     1
     // Define roles with their associated permissions
     2
     // Use case: Create standard roles for your project management application
     3
     const roles = [
     4
       {
     5
         name: 'project_admin',
     6
         display_name: 'Project Administrator',
     7
         description: 'Full access to manage projects and team members',
     8
         permissions: [
     9
           'projects:create', 'projects:read', 'projects:update', 'projects:delete',
     10
           'tasks:assign'
     11
         ]
     12
       },
     13
       {
     14
         name: 'project_manager',
     15
         display_name: 'Project Manager',
     16
         description: 'Can manage projects and assign tasks',
     17
         permissions: [
     18
           'projects:create', 'projects:read', 'projects:update',
     19
           'tasks:assign'
     20
         ]
     21
       },
     22
       {
     23
         name: 'team_member',
     24
         display_name: 'Team Member',
     25
         description: 'Can view projects and participate in tasks',
     26
         permissions: [
     27
           'projects:read'
     28
         ]
     29
       }
     30
     ];
     31


     32
     // Register each role with Scalekit
     33
     for (const role of roles) {
     34
       await scalekit.role.createRole(role);
     35
       console.log(`Created role: ${role.name}`);
     36
     }
     37


     38
     // Your application's roles are now registered with Scalekit
     ```

   * Python

     Create roles with permissions

     ```python
     1
     # Define roles with their associated permissions
     2
     # Use case: Create standard roles for your project management application
     3
     from scalekit.v1.roles.roles_pb2 import CreateRole
     4


     5
     roles = [
     6
         CreateRole(
     7
             name="project_admin",
     8
             display_name="Project Administrator",
     9
             description="Full access to manage projects and team members",
     10
             permissions=["projects:create", "projects:read", "projects:update", "projects:delete", "tasks:assign"]
     11
         ),
     12
         CreateRole(
     13
             name="project_manager",
     14
             display_name="Project Manager",
     15
             description="Can manage projects and assign tasks",
     16
             permissions=["projects:create", "projects:read", "projects:update", "tasks:assign"]
     17
         ),
     18
         CreateRole(
     19
             name="team_member",
     20
             display_name="Team Member",
     21
             description="Can view projects and participate in tasks",
     22
             permissions=["projects:read"]
     23
         )
     24
     ]
     25


     26
     # Register each role with Scalekit
     27
     for role in roles:
     28
         scalekit_client.roles.create_role(role=role)
     29
         print(f"Created role: {role.name}")
     30


     31
     # Your application's roles are now registered with Scalekit
     ```

   * Go

     Create roles with permissions

     ```go
     1
     // Define roles with their associated permissions
     2
     // Use case: Create standard roles for your project management application
     3
     roles := []*scalekit.CreateRole{
     4
         {
     5
             Name:        "project_admin",
     6
             DisplayName: "Project Administrator",
     7
             Description: "Full access to manage projects and team members",
     8
             Permissions: []string{"projects:create", "projects:read", "projects:update", "projects:delete", "tasks:assign"},
     9
         },
     10
         {
     11
             Name:        "project_manager",
     12
             DisplayName: "Project Manager",
     13
             Description: "Can manage projects and assign tasks",
     14
             Permissions: []string{"projects:create", "projects:read", "projects:update", "tasks:assign"},
     15
         },
     16
         {
     17
             Name:        "team_member",
     18
             DisplayName: "Team Member",
     19
             Description: "Can view projects and participate in tasks",
     20
             Permissions: []string{"projects:read"},
     21
         },
     22
     }
     23


     24
     // Register each role with Scalekit
     25
     for _, role := range roles {
     26
         _, err := sc.Role().CreateRole(ctx, role)
     27
         if err != nil {
     28
             log.Printf("Failed to create role: %s", role.Name)
     29
             continue
     30
         }
     31
         fmt.Printf("Created role: %s\n", role.Name)
     32
     }
     33


     34
     // Your application's roles are now registered with Scalekit
     ```

   * Java

     Create roles with permissions

     ```java
     1
     // Define roles with their associated permissions
     2
     // Use case: Create standard roles for your project management application
     3
     List roles = Arrays.asList(
     4
         CreateRole.newBuilder()
     5
             .setName("project_admin")
     6
             .setDisplayName("Project Administrator")
     7
             .setDescription("Full access to manage projects and team members")
     8
             .addAllPermissions(Arrays.asList("projects:create", "projects:read", "projects:update", "projects:delete", "tasks:assign"))
     9
             .build(),
     10
         CreateRole.newBuilder()
     11
             .setName("project_manager")
     12
             .setDisplayName("Project Manager")
     13
             .setDescription("Can manage projects and assign tasks")
     14
             .addAllPermissions(Arrays.asList("projects:create", "projects:read", "projects:update", "tasks:assign"))
     15
             .build(),
     16
         CreateRole.newBuilder()
     17
             .setName("team_member")
     18
             .setDisplayName("Team Member")
     19
             .setDescription("Can view projects and participate in tasks")
     20
             .addPermissions("projects:read")
     21
             .build()
     22
     );
     23


     24
     // Register each role with Scalekit
     25
     for (CreateRole role : roles) {
     26
         try {
     27
             CreateRoleRequest request = CreateRoleRequest.newBuilder()
     28
                 .setRole(role)
     29
                 .build();
     30


     31
             scalekitClient.roles().createRole(request);
     32
             System.out.println("Created role: " + role.getName());
     33
         } catch (Exception e) {
     34
             System.err.println("Error creating role: " + e.getMessage());
     35
         }
     36
     }
     37


     38
     // Your application's roles are now registered with Scalekit
     ```

## Inherit permissions through roles

[Section titled “Inherit permissions through roles”](#inherit-permissions-through-roles)

Large applications with extensive feature sets require sophisticated role and permission management. Scalekit enables role inheritance, allowing you to create a hierarchical access control system. Permissions can be grouped into roles, and new roles can be derived from existing base roles, providing a flexible and scalable approach to defining user access.

![Role inheritance: each role inherits the permissions of the role below it](/d2/docs/authenticate/authz/create-roles-permissions-0.svg)

Role assignment in Scalekit automatically grants a user all permissions defined within that role.

This is how you can implement use it:

1. Your app defines the permissions and assigns to a role. Let’s say `viewer` role.
2. When creating new role called `editor`, you specify that it inherits the permissions from the `viewer` role.
3. When creating new role called `project_owner`, you specify that it inherits the permissions from the `editor` role.

Take a look at our [Roles and Permissions APIs](https://docs.scalekit.com/apis/#tag/roles/GET/api/v1/roles).

## Manage roles and permissions in the dashboard

[Section titled “Manage roles and permissions in the dashboard”](#manage-roles-and-permissions-in-the-dashboard)

For most applications, the simplest way to create and manage roles and permissions is through the Scalekit dashboard. This approach works well when you have a fixed set of roles and permissions that don’t need to be modified by users in your application. You can set up your authorization model once during application configuration and manage it through the dashboard going forward.

![Scalekit dashboard Roles & Permissions page showing the Manage Roles table with admin, member, and viewer roles and a Default Roles panel](/.netlify/images?url=_astro%2Fdefault-roles.DC1cIuRF.png\&w=3200\&h=1612\&dpl=6ae471fc143c9a4c27550463)

1. Navigate to **Dashboard** > **Roles & Permissions** > **Permissions** to create permissions:

   * Click **Create Permission** and provide:

     * **Name** - Machine-friendly identifier (e.g., `projects:create`)
     * **Display Name** - Human-readable label (e.g., “Create Projects”)
     * **Description** - Clear explanation of what this permission allows

2. Go to **Dashboard** > **Roles & Permissions** > **Roles** to create roles:

   * Click **Create Role** and provide:

     * **Name** - Machine-friendly identifier (e.g., `project_manager`)
     * **Display Name** - Human-readable label (e.g., “Project Manager”)
     * **Description** - Clear explanation of the role’s purpose
     * **Permissions** - Select the permissions to include in this role

3. Configure default roles for new users who join organizations

4. Organization administrators can create organization-specific roles by going to **Dashboard** > **Organizations** > **Select organization** > **Roles**

Now that you have created roles and permissions in Scalekit, the next step is to assign these roles to users in your application.

### Configure organization specific roles

[Section titled “Configure organization specific roles”](#configure-organization-specific-roles)

Organization-level roles let organization administrators create custom roles that apply only within their specific organization. These roles are separate from any application-level roles you define.

![Organization Roles tab for a single organization, showing application roles, organization roles, and a default organization role selector](/.netlify/images?url=_astro%2Fadd-organization-role.D9e4-Diz.png\&w=2934\&h=1586\&dpl=6ae471fc143c9a4c27550463)

You can create organization-level roles from the Scalekit Dashboard:

* Go to **Organizations → Select an organization → Roles**

* In **Organization roles** section, Click **+ Add role** and provide:

  * **Display name**: Human-readable name (e.g., “Manager”)
  * **Name (key)**: Machine-friendly identifier (e.g., `manager`)
  * **Description**: Clear explanation of what users with this role can do

---
# DOCUMENT BOUNDARY
---

# Implement access control

> Verify permissions and roles in your application code to control user access

After configuring permissions and roles, the next critical step is implementing access control directly within your application code. This is achieved by carefully examining the roles and permissions embedded in the user’s access token to make authorization decisions.

Scalekit conveniently packages these authorization details during the authentication process, providing you with a comprehensive set of data to make precise access control decisions without requiring additional API calls.

Review the authorization flow

![Access control sequence: the app checks the user's roles and permissions before returning a protected resource or an error](/d2/docs/authenticate/authz/implement-access-control-0.svg)

This section focuses on implementing access control, which naturally follows user authentication. We recommend completing the authentication [quickstart](/authenticate/fsa/quickstart) before diving into these access control implementation details.

## Start by inspecting the access token

[Section titled “Start by inspecting the access token”](#start-by-inspecting-the-access-token)

When you [exchange the code for a user profile](/authenticate/fsa/complete-login/), Scalekit also adds additional information that help your app determine the access control decisions.

* Auth result

  ```js
  1
  {
  2
    user: {
  3
      email: "john.doe@example.com",
  4
      emailVerified: true,
  5
      givenName: "John",
  6
      name: "John Doe",
  7
      id: "usr_74599896446906854"
  8
    },
  9
    idToken: "eyJhbGciO..", // Decode for full user details
  10


  11
    accessToken: "eyJhbGciOi..",
  12
    refreshToken: "rt_8f7d6e5c4b3a2d1e0f9g8h7i6j..",
  13
    expiresIn: 299 // in seconds
  14
  }
  ```

* Decoded ID token

  ID token decoded

  ```json
  1
  {
  2
    "at_hash": "ec_jU2ZKpFelCKLTRWiRsg",
  3
    "aud": [
  4
      "skc_58327482062864390"
  5
    ],
  6
    "azp": "skc_58327482062864390",
  7
    "c_hash": "6wMreK9kWQQY6O5R0CiiYg",
  8
    "client_id": "skc_58327482062864390",
  9
    "email": "john.doe@example.com",
  10
    "email_verified": true,
  11
    "exp": 1742975822,
  12
    "family_name": "Doe",
  13
    "given_name": "John",
  14
    "iat": 1742974022,
  15
    "iss": "https://scalekit-z44iroqaaada-dev.scalekit.cloud",
  16
    "name": "John Doe",
  17
    "oid": "org_59615193906282635",
  18
    "sid": "ses_65274187031249433",
  19
    "sub": "usr_63261014140912135"
  20
  }
  ```

* Decoded access token

  Decoded access token

  ```json
  1
  {
  2
      "aud": [
  3
        "prd_skc_7848964512134X699"
  4
      ],
  5
      "client_id": "prd_skc_7848964512134X699",
  6
      "exp": 1758265247,
  7
      "iat": 1758264947,
  8
      "iss": "https://login.devramp.ai",
  9
      "jti": "tkn_90928731115292X63",
  10
      "nbf": 1758264947,
  11
      "oid": "org_89678001X21929734",
  12
      "permissions": [
  13
        "workspace_data:write",
  14
        "workspace_data:read"
  15
      ],
  16
      "roles": [
  17
        "admin"
  18
      ],
  19
      "sid": "ses_90928729571723X24",
  20
      "sub": "usr_8967800122X995270",
  21
      // External identifiers if updated on Scalekit
  22
      "xoid": "ext_org_123", // Organization ID
  23
      "xuid": "ext_usr_456", // User ID
  24
  }
  ```

Let’s closely look at the access token:

Decoded access token

```json
{
  "aud": ["skc_987654321098765432"],
  "client_id": "skc_987654321098765432",
  "exp": 1750850145,
  "iat": 1750849845,
  "iss": "http://example.localhost:8889",
  "jti": "tkn_987654321098765432",
  "nbf": 1750849845,
  "roles": ["project_manager", "member"],
  "oid": "org_69615647365005430",
  "permissions": ["projects:create", "projects:read", "projects:update", "tasks:assign"],
  "sid": "ses_987654321098765432",
  "sub": "usr_987654321098765432"
}
```

The `roles` and `permissions` values provide runtime insights into the user’s access constraints directly within the access token, eliminating the need for additional API requests. Crucially, always validate the token’s integrity before relying on the embedded authorization details.

* Node.js

  Validate and decode access token in middleware

  ```javascript
  1
  // Middleware to validate tokens and extract authorization data
  2
  const validateAndExtractAuth = async (req, res, next) => {
  3
    try {
  4
      // Extract access token from cookie (decrypt if needed)
  5
      const accessToken = decrypt(req.cookies.accessToken);
  6


  7
      // Validate the token using Scalekit SDK
  8
      const isValid = await scalekit.validateAccessToken(accessToken);
  9


  10
      if (!isValid) {
  11
        return res.status(401).json({ error: 'Invalid or expired token' });
  12
      }
  13


  14
      // Decode token to get roles and permissions using any JWT decode library
  15
      const tokenData = await decodeAccessToken(accessToken);
  16


  17
      // Make authorization data available to route handlers
  18
      req.user = {
  19
        id: tokenData.sub,
  20
        organizationId: tokenData.oid,
  21
        roles: tokenData.roles || [],
  22
        permissions: tokenData.permissions || []
  23
      };
  24


  25
      next();
  26
    } catch (error) {
  27
      return res.status(401).json({ error: 'Authentication failed' });
  28
    }
  29
  };
  ```

* Python

  Validate and decode access token

  ```python
  4 collapsed lines
  1
  from scalekit import ScalekitClient
  2
  from functools import wraps
  3
  import jwt
  4


  5
  scalekit_client = ScalekitClient(/* your credentials */)
  6


  7
  def validate_and_extract_auth(f):
  8
      @wraps(f)
  9
      def decorated_function(*args, **kwargs):
  10
          try:
  11
              # Extract access token from cookie (decrypt if needed)
  12
              access_token = decrypt(request.cookies.get('accessToken'))
  13


  14
              # Validate the token using Scalekit SDK
  15
              is_valid = scalekit_client.validate_access_token(access_token)
  16


  17
              if not is_valid:
  18
                  return jsonify({'error': 'Invalid or expired token'}), 401
  19


  20
              # Decode token to get roles and permissions
  21
              token_data = scalekit_client.decode_access_token(access_token)
  22


  23
              # Make authorization data available to route handlers
  24
              request.user = {
  25
                  'id': token_data.get('sub'),
  26
                  'organization_id': token_data.get('oid'),
  27
                  'roles': token_data.get('roles', []),
  28
                  'permissions': token_data.get('permissions', [])
  29
              }
  30


  31
              return f(*args, **kwargs)
  32
          except Exception as e:
  33
              return jsonify({'error': 'Authentication failed'}), 401
  34


  35
      return decorated_function
  ```

* Go

  Validate and decode access token

  ```go
  7 collapsed lines
  1
  import (
  2
      "context"
  3
      "encoding/json"
  4
      "net/http"
  5
      "github.com/scalekit-inc/scalekit-sdk-go"
  6
  )
  7


  8
  scalekitClient := scalekit.NewScalekitClient(/* your credentials */)
  9


  10
  func validateAndExtractAuth(next http.HandlerFunc) http.HandlerFunc {
  11
      return func(w http.ResponseWriter, r *http.Request) {
  12
          // Extract access token from cookie (decrypt if needed)
  13
          cookie, err := r.Cookie("accessToken")
  14
          if err != nil {
  15
              http.Error(w, `{"error": "No access token provided"}`, http.StatusUnauthorized)
  16
              return
  17
          }
  18


  19
          accessToken, err := decrypt(cookie.Value)
  20
          if err != nil {
  21
              http.Error(w, `{"error": "Token decryption failed"}`, http.StatusUnauthorized)
  22
              return
  23
          }
  24


  25
          // Validate the token using Scalekit SDK
  26
          isValid, err := scalekitClient.ValidateAccessToken(r.Context(), accessToken)
  27
          if err != nil || !isValid {
  28
              http.Error(w, `{"error": "Invalid or expired token"}`, http.StatusUnauthorized)
  29
              return
  30
          }
  31


  32
          // Decode token to get roles and permissions using any JWT decode lib
  33
          tokenData, err := DecodeAccessToken(accessToken)
  34
          if err != nil {
  35
              http.Error(w, `{"error": "Token decode failed"}`, http.StatusUnauthorized)
  36
              return
  37
          }
  38


  39
          // Add authorization data to request context
  40
          user := map[string]interface{}{
  41
              "id":             tokenData["sub"],
  42
              "organization_id": tokenData["oid"],
  43
              "roles":          tokenData["roles"],
  44
              "permissions":    tokenData["permissions"],
  45
          }
  46


  47
          ctx := context.WithValue(r.Context(), "user", user)
  48
          next(w, r.WithContext(ctx))
  49
      }
  50
  }
  ```

* Java

  Validate and decode access token

  ```java
  7 collapsed lines
  1
  import com.scalekit.ScalekitClient;
  2
  import javax.servlet.http.HttpServletRequest;
  3
  import javax.servlet.http.HttpServletResponse;
  4
  import org.springframework.web.servlet.HandlerInterceptor;
  5
  import java.util.Map;
  6
  import java.util.HashMap;
  7


  8
  @Component
  9
  public class AuthorizationInterceptor implements HandlerInterceptor {
  10
      private final ScalekitClient scalekit;
  11


  12
      @Override
  13
      public boolean preHandle(
  14
          HttpServletRequest request,
  15
          HttpServletResponse response,
  16
          Object handler
  17
      ) throws Exception {
  18
          try {
  19
              // Extract access token from cookie (decrypt if needed)
  20
              String accessToken = getCookieValue(request, "accessToken");
  21
              String decryptedToken = decrypt(accessToken);
  22


  23
              // Validate the token using Scalekit SDK
  24
              boolean isValid = scalekit.authentication().validateAccessToken(decryptedToken);
  25


  26
              if (!isValid) {
  27
                  response.setStatus(HttpStatus.UNAUTHORIZED.value());
  28
                  response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
  29
                  return false;
  30
              }
  31


  32
              // Decode token to get roles and permissions using any JWT decode lib
  33
              Map tokenData = decodeAccessToken(decryptedToken);
  34


  35
              // Make authorization data available to controllers
  36
              Map user = new HashMap<>();
  37
              user.put("id", tokenData.get("sub"));
  38
              user.put("organizationId", tokenData.get("oid"));
  39
              user.put("roles", tokenData.get("roles"));
  40
              user.put("permissions", tokenData.get("permissions"));
  41


  42
              request.setAttribute("user", user);
  43
              return true;
  44


  45
          } catch (Exception e) {
  46
              response.setStatus(HttpStatus.UNAUTHORIZED.value());
  47
              response.getWriter().write("{\"error\": \"Authentication failed\"}");
  48
              return false;
  49
          }
  50
      }
  51
  }
  ```

This approach makes user roles and permissions available throughout different routes of your application, enabling consistent and secure access control across all endpoints.

## Verify user’s role to allow access to protected resources

[Section titled “Verify user’s role to allow access to protected resources”](#verify-users-role-to-allow-access-to-protected-resources)

Role-based access control (RBAC) provides a straightforward way to manage permissions by grouping them into logical roles. Instead of checking individual permissions for every action, your application can simply verify if the user has the required role, making access control decisions more efficient and easier to maintain.

Tip

Use roles for broad access control patterns like admin access, management privileges, or user tiers. Reserve permissions for fine-grained control over specific actions and resources.

* Node.js

  Role-based access control

  ```javascript
  17 collapsed lines
  1
  // Helper function to check roles
  2
  function hasRole(user, requiredRole) {
  3
    return user.roles && user.roles.includes(requiredRole);
  4
  }
  5


  6
  // Middleware to require specific roles
  7
  function requireRole(role) {
  8
    return (req, res, next) => {
  9
      if (!hasRole(req.user, role)) {
  10
        return res.status(403).json({
  11
          error: `Access denied. Required role: ${role}`
  12
        });
  13
      }
  14
      next();
  15
    };
  16
  }
  17


  18
  // Admin-only routes
  19
  app.get('/api/admin/users', validateAndExtractAuth, requireRole('admin'), (req, res) => {
  20
    // Only admin users can access this endpoint
  21
    res.json(getAllUsers(req.user.organizationId));
  22
  });
  23


  24
  // Multiple role check
  25
  app.post('/api/admin/invite-user', validateAndExtractAuth, (req, res) => {
  26
    const user = req.user;
  27


  28
    // Allow admins or managers to invite users
  29
    if (!hasRole(user, 'admin') && !hasRole(user, 'manager')) {
  30
      return res.status(403).json({ error: 'Only admins and managers can invite users' });
  31
    }
  32


  33
    const invitation = createUserInvitation(req.body, user.organizationId);
  34
    res.json(invitation);
  35
  });
  ```

* Python

  Role-based access control

  ```python
  17 collapsed lines
  1
  # Helper function to check roles
  2
  def has_role(user, required_role):
  3
      roles = user.get('roles', [])
  4
      return required_role in roles
  5


  6
  # Decorator to require specific roles
  7
  def require_role(role):
  8
      def decorator(f):
  9
          @wraps(f)
  10
          def decorated_function(*args, **kwargs):
  11
              user = getattr(request, 'user', {})
  12
              if not has_role(user, role):
  13
                  return jsonify({'error': f'Access denied. Required role: {role}'}), 403
  14
              return f(*args, **kwargs)
  15
          return decorated_function
  16
      return decorator
  17


  18
  # Admin-only routes
  19
  @app.route('/api/admin/users')
  20
  @validate_and_extract_auth
  21
  @require_role('admin')
  22
  def get_all_users():
  23
      # Only admin users can access this endpoint
  24
      return jsonify(get_all_users_for_org(request.user['organization_id']))
  25


  26
  # Multiple role check
  27
  @app.route('/api/admin/invite-user', methods=['POST'])
  28
  @validate_and_extract_auth
  29
  def invite_user():
  30
      user = request.user
  31


  32
      # Allow admins or managers to invite users
  33
      if not has_role(user, 'admin') and not has_role(user, 'manager'):
  34
          return jsonify({'error': 'Only admins and managers can invite users'}), 403
  35


  36
      invitation = create_user_invitation(request.json, user['organization_id'])
  37
      return jsonify(invitation)
  ```

* Go

  Role-based access control

  ```go
  31 collapsed lines
  1
  // Helper function to check roles
  2
  func hasRole(user map[string]interface{}, requiredRole string) bool {
  3
      roles, ok := user["roles"].([]interface{})
  4
      if !ok {
  5
          return false
  6
      }
  7


  8
      for _, role := range roles {
  9
          if roleStr, ok := role.(string); ok && roleStr == requiredRole {
  10
              return true
  11
          }
  12
      }
  13
      return false
  14
  }
  15


  16
  // Middleware to require specific roles
  17
  func requireRole(role string) func(http.HandlerFunc) http.HandlerFunc {
  18
      return func(next http.HandlerFunc) http.HandlerFunc {
  19
          return func(w http.ResponseWriter, r *http.Request) {
  20
              user := r.Context().Value("user").(map[string]interface{})
  21


  22
              if !hasRole(user, role) {
  23
                  http.Error(w, fmt.Sprintf(`{"error": "Access denied. Required role: %s"}`, role), http.StatusForbidden)
  24
                  return
  25
              }
  26


  27
              next(w, r)
  28
          }
  29
      }
  30
  }
  31


  32
  // Admin-only routes
  33
  func getAllUsersHandler(w http.ResponseWriter, r *http.Request) {
  34
      user := r.Context().Value("user").(map[string]interface{})
  35
      orgId := user["organization_id"].(string)
  36


  37
      // Only admin users can access this endpoint
  38
      users := getAllUsersForOrg(orgId)
  39
      json.NewEncoder(w).Encode(users)
  40
  }
  41


  42
  // Route setup with role middleware
  43
  http.HandleFunc("/api/admin/users", validateAndExtractAuth(requireRole("admin")(getAllUsersHandler)))
  ```

* Java

  Role-based access control

  ```java
  1
  @RestController
  2
  public class AdminController {
  7 collapsed lines
  3


  4
      // Helper method to check roles
  5
      private boolean hasRole(Map user, String requiredRole) {
  6
          List roles = (List) user.get("roles");
  7
          return roles != null && roles.contains(requiredRole);
  8
      }
  9


  10
      // Admin-only endpoint
  11
      @GetMapping("/api/admin/users")
  12
      public ResponseEntity> getAllUsers(HttpServletRequest request) {
  13
          Map user = (Map) request.getAttribute("user");
  14


  15
          // Check for admin role
  16
          if (!hasRole(user, "admin")) {
  17
              return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
  18
          }
  19


  20
          String orgId = (String) user.get("organizationId");
  21
          List users = userService.getAllUsersForOrg(orgId);
  22
          return ResponseEntity.ok(users);
  23
      }
  24


  25
      @PostMapping("/api/admin/invite-user")
  26
      public ResponseEntity inviteUser(
  27
          @RequestBody InviteUserRequest request,
  28
          HttpServletRequest httpRequest
  29
      ) {
  30
          Map user = (Map) httpRequest.getAttribute("user");
  31


  32
          // Allow admins or managers to invite users
  33
          if (!hasRole(user, "admin") && !hasRole(user, "manager")) {
  34
              return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
  35
          }
  36


  37
          String orgId = (String) user.get("organizationId");
  38
          Invitation invitation = userService.createInvitation(request, orgId);
  39
          return ResponseEntity.ok(invitation);
  40
      }
  41
  }
  ```

## Verify user’s permissions to allow specific actions

[Section titled “Verify user’s permissions to allow specific actions”](#verify-users-permissions-to-allow-specific-actions)

Permission-based access control provides granular control over specific actions and resources within your application. While roles offer broad access patterns, permissions allow you to define exactly what operations users can perform, enabling precise security controls and the principle of least privilege.

Note

Permissions are typically formatted as `resource:action` (e.g., `projects:create`, `users:read`, `reports:delete`) to provide clear, consistent naming conventions that make your access control logic more readable and maintainable.

* Node.js

  Permission-based access control

  ```javascript
  17 collapsed lines
  1
  // Helper function to check permissions
  2
  function hasPermission(user, requiredPermission) {
  3
    return user.permissions && user.permissions.includes(requiredPermission);
  4
  }
  5


  6
  // Middleware to require specific permissions
  7
  function requirePermission(permission) {
  8
    return (req, res, next) => {
  9
      if (!hasPermission(req.user, permission)) {
  10
        return res.status(403).json({
  11
          error: `Access denied. Required permission: ${permission}`
  12
        });
  13
      }
  14
      next();
  15
    };
  16
  }
  17


  18
  // Protected routes with permission checks
  19
  app.get('/api/projects', validateAndExtractAuth, requirePermission('projects:read'), (req, res) => {
  20
    // User has projects:read permission - allow access
  21
    res.json(getProjects(req.user.organizationId));
  22
  });
  23


  24
  app.post('/api/projects', validateAndExtractAuth, requirePermission('projects:create'), (req, res) => {
  25
    // User has projects:create permission - allow creation
  26
    const newProject = createProject(req.body, req.user.organizationId);
  27
    res.json(newProject);
  28
  });
  29


  30
  // Multiple permission check
  31
  app.delete('/api/projects/:id', validateAndExtractAuth, (req, res) => {
  32
    const user = req.user;
  33


  34
    // Check if user has either admin role or specific delete permission
  35
    if (!hasPermission(user, 'projects:delete') && !user.roles.includes('admin')) {
  36
      return res.status(403).json({ error: 'Cannot delete projects' });
  37
    }
  38


  39
    deleteProject(req.params.id, user.organizationId);
  40
    res.json({ success: true });
  41
  });
  ```

* Python

  Permission-based access control

  ```python
  17 collapsed lines
  1
  # Helper function to check permissions
  2
  def has_permission(user, required_permission):
  3
      permissions = user.get('permissions', [])
  4
      return required_permission in permissions
  5


  6
  # Decorator to require specific permissions
  7
  def require_permission(permission):
  8
      def decorator(f):
  9
          @wraps(f)
  10
          def decorated_function(*args, **kwargs):
  11
              user = getattr(request, 'user', {})
  12
              if not has_permission(user, permission):
  13
                  return jsonify({'error': f'Access denied. Required permission: {permission}'}), 403
  14
              return f(*args, **kwargs)
  15
          return decorated_function
  16
      return decorator
  17


  18
  # Protected routes with permission checks
  19
  @app.route('/api/projects')
  20
  @validate_and_extract_auth
  21
  @require_permission('projects:read')
  22
  def get_projects():
  23
      # User has projects:read permission - allow access
  24
      return jsonify(get_projects_for_org(request.user['organization_id']))
  25


  26
  @app.route('/api/projects', methods=['POST'])
  27
  @validate_and_extract_auth
  28
  @require_permission('projects:create')
  29
  def create_project():
  30
      # User has projects:create permission - allow creation
  31
      new_project = create_project_for_org(request.json, request.user['organization_id'])
  32
      return jsonify(new_project)
  33


  34
  # Multiple permission check
  35
  @app.route('/api/projects/', methods=['DELETE'])
  36
  @validate_and_extract_auth
  37
  def delete_project(project_id):
  38
      user = request.user
  39


  40
      # Check if user has either admin role or specific delete permission
  41
      if not has_permission(user, 'projects:delete') and 'admin' not in user.get('roles', []):
  42
          return jsonify({'error': 'Cannot delete projects'}), 403
  43


  44
      delete_project_from_org(project_id, user['organization_id'])
  45
      return jsonify({'success': True})
  ```

* Go

  Permission-based access control

  ```go
  1
  // Helper function to check permissions
  2
  func hasPermission(user map[string]interface{}, requiredPermission string) bool {
  3
      permissions, ok := user["permissions"].([]interface{})
  4
      if !ok {
  5
          return false
  6
      }
  7


  8
      for _, perm := range permissions {
  9
          if permStr, ok := perm.(string); ok && permStr == requiredPermission {
  10
              return true
  11
          }
  12
      }
  13
      return false
  14
  }
  15


  16
  // Middleware to require specific permissions
  17
  func requirePermission(permission string) func(http.HandlerFunc) http.HandlerFunc {
  18
      return func(next http.HandlerFunc) http.HandlerFunc {
  19
          return func(w http.ResponseWriter, r *http.Request) {
  20
              user := r.Context().Value("user").(map[string]interface{})
  21


  22
              if !hasPermission(user, permission) {
  23
                  http.Error(w, fmt.Sprintf(`{"error": "Access denied. Required permission: %s"}`, permission), http.StatusForbidden)
  24
                  return
  25
              }
  26


  27
              next(w, r)
  28
          }
  29
      }
  30
  }
  31


  32
  // Protected routes with permission checks
  33
  func getProjectsHandler(w http.ResponseWriter, r *http.Request) {
  34
      user := r.Context().Value("user").(map[string]interface{})
  35
      orgId := user["organization_id"].(string)
  36


  37
      // User has projects:read permission - allow access
  38
      projects := getProjectsForOrg(orgId)
  39
      json.NewEncoder(w).Encode(projects)
  40
  }
  41


  42
  func createProjectHandler(w http.ResponseWriter, r *http.Request) {
  43
      user := r.Context().Value("user").(map[string]interface{})
  44
      orgId := user["organization_id"].(string)
  45


  46
      // User has projects:create permission - allow creation
  47
      var projectData map[string]interface{}
  48
      json.NewDecoder(r.Body).Decode(&projectData)
  49


  50
      newProject := createProjectForOrg(projectData, orgId)
  51
      json.NewEncoder(w).Encode(newProject)
  52
  }
  53


  54
  // Route setup with middleware
  55
  http.HandleFunc("/api/projects", validateAndExtractAuth(requirePermission("projects:read")(getProjectsHandler)))
  56
  http.HandleFunc("/api/projects/create", validateAndExtractAuth(requirePermission("projects:create")(createProjectHandler)))
  ```

* Java

  Permission-based access control

  ```java
  1
  @RestController
  2
  public class ProjectController {
  3


  4
      // Helper method to check permissions
  5
      private boolean hasPermission(Map user, String requiredPermission) {
  6
          List permissions = (List) user.get("permissions");
  7
          return permissions != null && permissions.contains(requiredPermission);
  8
      }
  9


  10
      // Annotation-based permission checking
  11
      @GetMapping("/api/projects")
  12
      @PreAuthorize("hasPermission('projects:read')")
  13
      public ResponseEntity> getProjects(HttpServletRequest request) {
  14
          Map user = (Map) request.getAttribute("user");
  15
          String orgId = (String) user.get("organizationId");
  16


  17
          // User has projects:read permission - allow access
  18
          List projects = projectService.getProjectsForOrg(orgId);
  19
          return ResponseEntity.ok(projects);
  20
      }
  21


  22
      @PostMapping("/api/projects")
  23
      public ResponseEntity createProject(
  24
          @RequestBody CreateProjectRequest request,
  25
          HttpServletRequest httpRequest
  26
      ) {
  27
          Map user = (Map) httpRequest.getAttribute("user");
  28


  29
          // Check permission manually
  30
          if (!hasPermission(user, "projects:create")) {
  31
              return ResponseEntity.status(HttpStatus.FORBIDDEN)
  32
                  .body(null);
  33
          }
  34


  35
          String orgId = (String) user.get("organizationId");
  36
          Project newProject = projectService.createProject(request, orgId);
  37
          return ResponseEntity.ok(newProject);
  38
      }
  39


  40
      @DeleteMapping("/api/projects/{projectId}")
  41
      public ResponseEntity> deleteProject(
  42
          @PathVariable String projectId,
  43
          HttpServletRequest request
  44
      ) {
  45
          Map user = (Map) request.getAttribute("user");
  46
          List roles = (List) user.get("roles");
  47


  48
          // Check if user has either admin role or specific delete permission
  49
          if (!hasPermission(user, "projects:delete") && !roles.contains("admin")) {
  50
              return ResponseEntity.status(HttpStatus.FORBIDDEN)
  51
                  .body(Map.of("error", true));
  52
          }
  53


  54
          String orgId = (String) user.get("organizationId");
  55
          projectService.deleteProject(projectId, orgId);
  56
          return ResponseEntity.ok(Map.of("success", true));
  57
      }
  58
  }
  ```

By implementing both role-based and permission-based access control, your application now has a comprehensive security framework that protects different routes and endpoints. You can combine both approaches to create fine-grained access control that matches your application’s specific requirements.

**Admin bypass pattern**: Allow users with `admin` role to bypass certain permission checks while maintaining granular control for other users

**Resource ownership pattern**: Combine role/permission checks with resource ownership verification (e.g., users can only edit their own projects unless they have admin role)

**Time-based access pattern**: Consider implementing time-based restrictions for sensitive operations, especially for roles with elevated permissions

Caution

Never implement authorization logic solely on the client side. Always perform server-side validation of roles and permissions, as client-side checks can be bypassed by malicious users.

---
# DOCUMENT BOUNDARY
---

# SaaSKit code samples

> Full stack auth code samples demonstrating complete authentication implementations with hosted login and session management

### [Full Stack Auth with Next.js](https://github.com/scalekit-inc/scalekit-nextjs-auth-example)

[Complete authentication solution for Next.js apps. Includes hosted login pages, session management, and protected routes](https://github.com/scalekit-inc/scalekit-nextjs-auth-example)

### [Full Stack Auth with FastAPI](https://github.com/scalekit-inc/scalekit-fastapi-auth-example)

[Authentication template for FastAPI projects. Featuring integrated user sessions, hosted login flow, and ready-to-use route protection specifically tailored for Python web backends.](https://github.com/scalekit-inc/scalekit-fastapi-auth-example)

### [Full Stack Auth with Flask](https://github.com/scalekit-inc/scalekit-flask-auth-example)

[Authentication template for Flask applications. Features session management, hosted login flow, and decorator-based route protection](https://github.com/scalekit-inc/scalekit-flask-auth-example)

### [Full Stack Auth with Django](https://github.com/scalekit-inc/scalekit-django-auth-example)

[Authentication template for Django projects. Features session management, hosted login flow, and middleware-based route protection](https://github.com/scalekit-inc/scalekit-django-auth-example)

### [Full Stack Auth with Express](https://github.com/scalekit-inc/scalekit-express-auth-example)

[Complete authentication solution for Express.js applications. Includes hosted login pages, session management, and middleware-protected routes](https://github.com/scalekit-inc/scalekit-express-auth-example)

### [Full Stack Auth with Spring Boot](https://github.com/scalekit-inc/scalekit-springboot-auth-example)

[End-to-end authentication for Java applications. Features Spring Security integration, hosted login, and session handling](https://github.com/scalekit-inc/scalekit-springboot-auth-example)

### [Full Stack Auth with Laravel](https://github.com/scalekit-inc/scalekit-laravel-auth-example)

[Complete authentication solution for Laravel applications. Includes hosted login pages, session management, and middleware-protected routes](https://github.com/scalekit-inc/scalekit-laravel-auth-example)

### End to end full stack auth demo

Coffee Desk App

Complete coffee shop management application with full stack. Features workspaces, organization switcher, and mulitple auth methods

[View demo](https://dashboard.coffeedesk.app/) | [View code](https://github.com/scalekit-inc/coffee-desk-demo)

---
# DOCUMENT BOUNDARY
---

# Implement logout

> Terminate user sessions across your application and Scalekit

When implementing logout functionality, you need to consider three session layers where user authentication state is maintained:

1. **Application session layer**: Your application stores session tokens (access tokens, refresh tokens, ID tokens) in browser cookies. You control this layer completely.

2. **Scalekit session layer**: Scalekit maintains a session for the user and stores their information. When users return to Scalekit’s authentication page, their information is remembered for a smoother experience.

3. **Identity provider session layer**: When users authenticate with external providers (for example, Okta through enterprise SSO), those providers maintain their own sessions. Users won’t be prompted to sign in again if they’re already signed into the provider.

Clear the application session layer and invalidate the Scalekit session layer in a single logout endpoint.

![Logout flow showing three session layers](/.netlify/images?url=_astro%2F1.DR4kQkNT.png\&w=4056\&h=2344\&dpl=6ae471fc143c9a4c27550463)

1. ## Create a logout endpoint

   [Section titled “Create a logout endpoint”](#create-a-logout-endpoint)

   Create a `/logout` endpoint in your application that handles the complete logout flow: extracting the ID token, generating the Scalekit logout URL, clearing session cookies, and redirecting to Scalekit.

   * Node.js

     * Express

       ```html
       Log out
       ```

       `auth.router` already registered `GET /logout`.

     * Next.js

       app/logout/route.ts

       ```ts
       import { auth } from '../../lib/auth';
       export const GET = auth.createLogoutHandler();
       ```

       ```html
       Log out
       ```

   * Python

     ```html
     Log out
     ```

     `auth.router` already registered `GET /logout`.

   * Go

     app/logout/route.ts

     ```ts
     import { auth } from '../../lib/auth';
     export const GET = auth.createLogoutHandler();
     ```

     ```html
     Log out
     ```

   * Java

     * Flask

       ```html
       Log out
       ```

       `ScalekitAuth` already registered `GET /logout`.

     * FastAPI

       ```html
       Log out
       ```

       `auth.install(app)` already registered `GET /logout`.

   * Express

     ```html
     Log out
     ```

     `ScalekitAuth` already registered `GET /logout`.

   * Next.js

     ```html
     Log out
     ```

     `auth.install(app)` already registered `GET /logout`.

   * Flask

     Gin

     ```go
     1
          func logoutHandler(c *gin.Context) {
     2
              // Step 1: Extract the ID token (needed for Scalekit logout)
     3
              idToken, _ := c.Cookie("idToken")
     4
              postLogoutRedirectURI := "http://localhost:3000/login"
     5


     6
              // Step 2: Generate the Scalekit logout URL (points to /oidc/logout endpoint)
     7
              logoutURL, err := scalekitClient.GetLogoutUrl(LogoutUrlOptions{
     8
                  IdTokenHint: idToken,
     9
                  PostLogoutRedirectUri: postLogoutRedirectURI,
     10
              })
     11
              if err != nil {
     12
                  c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
     13
                  return
     14
              }
     15


     16
              // Step 3: Clear all session cookies
     17
              c.SetCookie("accessToken", "", -1, "/", "", true, true)
     18
              c.SetCookie("refreshToken", "", -1, "/", "", true, true)
     19
              c.SetCookie("idToken", "", -1, "/", "", true, true)  // Clear AFTER using it for logout URL
     20


     21
              // Step 4: Redirect to Scalekit to invalidate the session
     22
              c.Redirect(http.StatusFound, logoutURL.String())
     23
          }
     ```

   * FastAPI

     Spring Boot

     ```java
     1
     @GetMapping("/logout")
     2
     public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException {
     3
         // Step 1: Extract the ID token (needed for Scalekit logout)
     4
         String idToken = request.getCookies() != null ?
     5
             Arrays.stream(request.getCookies())
     6
                 .filter(c -> c.getName().equals("idToken"))
     7
                 .findFirst()
     8
                 .map(Cookie::getValue)
     9
                 .orElse(null) : null;
     10


     11
         String postLogoutRedirectUri = "http://localhost:3000/login";
     12


     13
         // Step 2: Generate the Scalekit logout URL (points to /oidc/logout endpoint)
     14
         LogoutUrlOptions options = new LogoutUrlOptions();
     15
         options.setIdTokenHint(idToken);
     16
         options.setPostLogoutRedirectUri(postLogoutRedirectUri);
     17
         URL logoutUrl = scalekitClient.authentication().getLogoutUrl(options);
     18


     19
         // Step 3: Clear all session cookies with security attributes
     20
         Cookie accessTokenCookie = new Cookie("accessToken", null);
     21
         accessTokenCookie.setMaxAge(0);
     22
         accessTokenCookie.setPath("/");
     23
         accessTokenCookie.setHttpOnly(true);
     24
         accessTokenCookie.setSecure(true);
     25
         response.addCookie(accessTokenCookie);
     26


     27
         Cookie refreshTokenCookie = new Cookie("refreshToken", null);
     28
         refreshTokenCookie.setMaxAge(0);
     29
         refreshTokenCookie.setPath("/");
     30
         refreshTokenCookie.setHttpOnly(true);
     31
         refreshTokenCookie.setSecure(true);
     32
         response.addCookie(refreshTokenCookie);
     33


     34
         Cookie idTokenCookie = new Cookie("idToken", null);
     35
         idTokenCookie.setMaxAge(0);
     36
         idTokenCookie.setPath("/");
     37
         idTokenCookie.setHttpOnly(true);
     38
         idTokenCookie.setSecure(true);
     39
         response.addCookie(idTokenCookie);  // Clear AFTER using it for logout URL
     40


     41
         // Step 4: Redirect to Scalekit to invalidate the session
     42
         response.sendRedirect(logoutUrl.toString());
     43
     }
     ```

   Session middleware clears `sk_session` and ends the Scalekit session. If you build logout yourself, extract the ID token before you clear cookies.

   Build it yourself — getLogoutUrl and clear cookies

   Use this path when you do not use session middleware. Extract the ID token first. Then call `getLogoutUrl`. Then clear cookies. Then redirect the browser.

   Express.js

   ```javascript
   app.get('/logout', (req, res) => {
     const idTokenHint = req.cookies.idToken;
     const postLogoutRedirectUri = 'http://localhost:3000/login';
     const logoutUrl = scalekit.getLogoutUrl({
       idTokenHint,
       postLogoutRedirectUri,
     });
     res.clearCookie('accessToken');
     res.clearCookie('refreshToken');
     res.clearCookie('idToken');
     res.redirect(logoutUrl);
   });
   ```

   Flask

   ```python
   1
   from flask import request, redirect, make_response
   2
   from scalekit.common.scalekit import LogoutUrlOptions
   3


   4
   @app.route('/logout')
   5
   def logout():
   6
       id_token = request.cookies.get('idToken')
   7
       logout_url = scalekit_client.get_logout_url(
   8
           LogoutUrlOptions(
   9
               id_token_hint=id_token,
   10
               post_logout_redirect_uri='http://localhost:3000/login',
   11
           )
   12
       )
   13
       response = make_response(redirect(logout_url))
   14
       response.set_cookie('accessToken', '', max_age=0)
   15
       response.set_cookie('refreshToken', '', max_age=0)
   16
       response.set_cookie('idToken', '', max_age=0)
   17
       return response
   ```

   Why must logout be a browser redirect?

   You must redirect to the `/oidc/logout` endpoint using a **browser redirect**, not through an API call. Redirecting the browser to Scalekit’s logout URL ensures the session cookie is automatically sent with the request, allowing Scalekit to correctly identify and end the user’s session.

2. ## Configure post-logout redirect URL

   [Section titled “Configure post-logout redirect URL”](#configure-post-logout-redirect-url)

   After users log out, Scalekit redirects them to the URL you specify in the `post_logout_redirect_uri` parameter. This URL must be registered in your Scalekit dashboard under **Dashboard > Authentication > Redirects > Post Logout URL**.

   Scalekit only redirects to URLs from your allow list. This prevents unauthorized redirects and protects your users. If you need different redirect URLs for different applications, you can register multiple post-logout URLs in your dashboard.

Logout security checklist

Session middleware: send a browser navigation to `/logout`. Register the post-logout URL in the dashboard.

If you build logout yourself: extract the ID token before you clear cookies, then redirect the browser to the URL from `getLogoutUrl`.

## Common logout scenarios

[Section titled “Common logout scenarios”](#common-logout-scenarios)

Which endpoint should I use for logout?

Use `GET /logout` from session middleware. That handler redirects the browser to Scalekit’s `/oidc/logout` endpoint and clears `sk_session`.

If you build logout yourself, call `getLogoutUrl` and redirect the browser to the returned URL. Do not call `/oidc/logout` with `fetch`.

Why must logout be a browser redirect?

You need to route to the `/oidc/logout` endpoint through a **browser redirect**, not with an API request. Redirecting the browser to Scalekit’s logout URL ensures the session cookie is sent automatically, so Scalekit can correctly locate and end the user’s session.

**❌ Doesn’t work - API call from frontend:**

```javascript
fetch('https://your-env.scalekit.dev/oidc/logout', {
  method: 'POST',
  body: JSON.stringify({ id_token_hint: idToken })
});
// Session cookie is NOT included, Scalekit can't identify the session
```

**✅ Works - Browser redirect:**

```javascript
const logoutUrl = scalekit.getLogoutUrl({
  idTokenHint: idToken,
  postLogoutRedirectUri,
});
window.location.href = logoutUrl;
// Browser includes session cookies automatically
```

**Why:** Your user session is stored in an HttpOnly cookie. API requests from JavaScript or backend servers don’t include this cookie, so Scalekit can’t identify which session to terminate.

Session not clearing after logout?

If clicking login after logout bypasses the login screen and logs you back in automatically, check the following:

1. **Verify the logout method** - Open browser DevTools → Network tab and trigger logout:

   * ✅ Type should show **“document”** (navigation)
   * ❌ Type should **NOT** show “fetch” or “xhr”
   * Check that the `Cookie` header is present in the request

2. **Check post-logout redirect URI** - Ensure it’s registered in **Dashboard > Authentication > Redirects > Post Logout URL**.

---
# DOCUMENT BOUNDARY
---

# Manage user sessions

> Store tokens safely with proper cookie security, validate on every request, and refresh with rotation to keep sessions secure

User sessions determine how long users stay signed in to your application. After users successfully authenticate, you receive session tokens that manage their access. These tokens control session duration, multi-device access, and cross-product authentication within your company’s ecosystem.

Store those tokens securely, validate them on every request, and refresh them before they expire. Session middleware writes one encrypted `sk_session` cookie and does that work for you. You can still store and refresh tokens yourself if you prefer.

Review the session management sequence

![User session management flow diagram showing how access tokens and refresh tokens work together](/.netlify/images?url=_astro%2F1.DV2_NThh.png\&w=3056\&h=3924\&dpl=6ae471fc143c9a4c27550463)

1. ## Store the session

   [Section titled “Store the session”](#store-the-session)

   After hosted login, session middleware writes one encrypted `sk_session` cookie. You do not set `accessToken` or `refreshToken` cookies.

   Add `COOKIE_ENCRYPTION_SECRET` to `.env`. Generate it with `openssl rand -base64 32`. Use the same value on every server.

   * Node.js

     * Express

       Install `@scalekit-sdk/node` 2.12.0 or later. Mount `auth.router`. It registers `/login`, `/callback`, and `/logout`. The callback writes `sk_session`.

       server.ts

       ```ts
       import express from 'express';
       import { ScalekitAuth } from '@scalekit-sdk/node/express';


       const auth = new ScalekitAuth({
         envUrl: process.env.SCALEKIT_ENVIRONMENT_URL,
         clientId: process.env.SCALEKIT_CLIENT_ID,
         clientSecret: process.env.SCALEKIT_CLIENT_SECRET,
         redirectUri: process.env.REDIRECT_URI,
         cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET,
       });


       const app = express();
       app.use(auth.router);
       ```

     * Next.js

       Install `@scalekit-sdk/node` 2.12.0 or later. Create one `auth` instance. Export login, callback, and logout Route Handlers. The callback writes `sk_session`.

       lib/auth.ts

       ```ts
       import { ScalekitClient } from '@scalekit-sdk/node';
       import { ScalekitAuthNext } from '@scalekit-sdk/node/next';


       const scalekit = new ScalekitClient(
         process.env.SCALEKIT_ENVIRONMENT_URL!,
         process.env.SCALEKIT_CLIENT_ID!,
         process.env.SCALEKIT_CLIENT_SECRET!
       );


       export const auth = new ScalekitAuthNext({
         client: scalekit,
         redirectUri: process.env.REDIRECT_URI!,
         cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET!,
       });
       ```

       app/login/route.ts

       ```ts
       import { auth } from '../../lib/auth';
       export const GET = auth.createLoginHandler();
       ```

       app/callback/route.ts

       ```ts
       import { auth } from '../../lib/auth';
       export const GET = auth.createCallbackHandler();
       ```

       app/logout/route.ts

       ```ts
       import { auth } from '../../lib/auth';
       export const GET = auth.createLogoutHandler();
       ```

       Pin `runtime: 'nodejs'` in `middleware.ts`, or pass [ScalekitEdgeClient](/saaskit/sdks/nextjs/#scalekitedgeclient) for Edge Runtime.

   * Python

     Install `@scalekit-sdk/node` 2.12.0 or later. Mount `auth.router`. It registers `/login`, `/callback`, and `/logout`. The callback writes `sk_session`.

     server.ts

     ```ts
     import express from 'express';
     import { ScalekitAuth } from '@scalekit-sdk/node/express';


     const auth = new ScalekitAuth({
       envUrl: process.env.SCALEKIT_ENVIRONMENT_URL,
       clientId: process.env.SCALEKIT_CLIENT_ID,
       clientSecret: process.env.SCALEKIT_CLIENT_SECRET,
       redirectUri: process.env.REDIRECT_URI,
       cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET,
     });


     const app = express();
     app.use(auth.router);
     ```

   * Go

     Install `@scalekit-sdk/node` 2.12.0 or later. Create one `auth` instance. Export login, callback, and logout Route Handlers. The callback writes `sk_session`.

     lib/auth.ts

     ```ts
     import { ScalekitClient } from '@scalekit-sdk/node';
     import { ScalekitAuthNext } from '@scalekit-sdk/node/next';


     const scalekit = new ScalekitClient(
       process.env.SCALEKIT_ENVIRONMENT_URL!,
       process.env.SCALEKIT_CLIENT_ID!,
       process.env.SCALEKIT_CLIENT_SECRET!
     );


     export const auth = new ScalekitAuthNext({
       client: scalekit,
       redirectUri: process.env.REDIRECT_URI!,
       cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET!,
     });
     ```

     app/login/route.ts

     ```ts
     import { auth } from '../../lib/auth';
     export const GET = auth.createLoginHandler();
     ```

     app/callback/route.ts

     ```ts
     import { auth } from '../../lib/auth';
     export const GET = auth.createCallbackHandler();
     ```

     app/logout/route.ts

     ```ts
     import { auth } from '../../lib/auth';
     export const GET = auth.createLogoutHandler();
     ```

     Pin `runtime: 'nodejs'` in `middleware.ts`, or pass [ScalekitEdgeClient](/saaskit/sdks/nextjs/#scalekitedgeclient) for Edge Runtime.

   * Java

     * Flask

       Install `scalekit-sdk-python` 2.17.0 or later with the Flask extra. `ScalekitAuth` registers `/login`, `/callback`, and `/logout` and writes `sk_session`.

       app.py

       ```py
       1
       import os
       2
       from flask import Flask
       3
       from scalekit.frameworks.flask import ScalekitAuth
       4


       5
       app = Flask(__name__)
       6
       auth = ScalekitAuth(
       7
           app,
       8
           env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
       9
           client_id=os.environ["SCALEKIT_CLIENT_ID"],
       10
           client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
       11
           redirect_uri=os.environ["REDIRECT_URI"],
       12
           cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"],
       13
           # HTTP localhost drops a Secure cookie, so the session never sticks.
       14
           # A missing Secure flag on real HTTP lets the browser send sk_session in cleartext.
       15
           cookie_secure=False,
       16
       )
       ```

     * FastAPI

       Install `scalekit-sdk-python` 2.17.0 or later with the FastAPI extra. Call `auth.install(app)` to register routes and write `sk_session`.

       app.py

       ```py
       1
       import os
       2
       from fastapi import FastAPI
       3
       from scalekit.frameworks.fastapi import ScalekitAuth
       4


       5
       app = FastAPI()
       6
       auth = ScalekitAuth(
       7
           env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
       8
           client_id=os.environ["SCALEKIT_CLIENT_ID"],
       9
           client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
       10
           redirect_uri=os.environ["REDIRECT_URI"],
       11
           cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"],
       12
           # HTTP localhost drops a Secure cookie, so the session never sticks.
       13
           # A missing Secure flag on real HTTP lets the browser send sk_session in cleartext.
       14
           cookie_secure=False,
       15
       )
       16
       auth.install(app)
       ```

     Set cookie_secure=False on local HTTP

     `cookie_secure` defaults to `True`. Some browsers drop a `Secure` cookie on plain `http://localhost`, which looks like a session that never sticks. Set `cookie_secure=False` for local HTTP. Set it `True` in production.

   * Express

     Install `scalekit-sdk-python` 2.17.0 or later with the Flask extra. `ScalekitAuth` registers `/login`, `/callback`, and `/logout` and writes `sk_session`.

     app.py

     ```py
     1
     import os
     2
     from flask import Flask
     3
     from scalekit.frameworks.flask import ScalekitAuth
     4


     5
     app = Flask(__name__)
     6
     auth = ScalekitAuth(
     7
         app,
     8
         env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
     9
         client_id=os.environ["SCALEKIT_CLIENT_ID"],
     10
         client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
     11
         redirect_uri=os.environ["REDIRECT_URI"],
     12
         cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"],
     13
         # HTTP localhost drops a Secure cookie, so the session never sticks.
     14
         # A missing Secure flag on real HTTP lets the browser send sk_session in cleartext.
     15
         cookie_secure=False,
     16
     )
     ```

   * Next.js

     Install `scalekit-sdk-python` 2.17.0 or later with the FastAPI extra. Call `auth.install(app)` to register routes and write `sk_session`.

     app.py

     ```py
     1
     import os
     2
     from fastapi import FastAPI
     3
     from scalekit.frameworks.fastapi import ScalekitAuth
     4


     5
     app = FastAPI()
     6
     auth = ScalekitAuth(
     7
         env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
     8
         client_id=os.environ["SCALEKIT_CLIENT_ID"],
     9
         client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
     10
         redirect_uri=os.environ["REDIRECT_URI"],
     11
         cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"],
     12
         # HTTP localhost drops a Secure cookie, so the session never sticks.
     13
         # A missing Secure flag on real HTTP lets the browser send sk_session in cleartext.
     14
         cookie_secure=False,
     15
     )
     16
     auth.install(app)
     ```

   * Flask

     Gin

     ```go
     7 collapsed lines
     1
     import (
     2
       "net/http"
     3
       "os"
     4
       "time"
     5
       "github.com/gin-gonic/gin"
     6
     )
     7


     8
     // Extract authentication data from the successful authentication response
     9
     accessToken := authResult.AccessToken
     10
     expiresIn := authResult.ExpiresIn
     11
     refreshToken := authResult.RefreshToken
     12
     user := authResult.User
     13


     14
     // Encrypt tokens before storing to add an additional security layer
     15
     encryptedAccessToken := encrypt(accessToken)
     16
     encryptedRefreshToken := encrypt(refreshToken)
     17


     18
     // Set SameSite mode for CSRF protection
     19
     c.SetSameSite(http.SameSiteStrictMode) // Prevents CSRF attacks
     20


     21
     // Store encrypted access token in HttpOnly cookie
     22
     c.SetCookie(
     23
       "accessToken",
     24
       encryptedAccessToken,
     25
       expiresIn-60, // Subtract 60s buffer for clock skew (seconds in Gin)
     26
       "/",          // Available on all routes
     27
       "",
     28
       os.Getenv("GIN_MODE") == "release", // HTTPS-only in production
     29
       true, // Prevents JavaScript access to mitigate XSS attacks
     30
     )
     31


     32
     // Store encrypted refresh token in separate HttpOnly cookie
     33
     c.SetCookie(
     34
       "refreshToken",
     35
       encryptedRefreshToken,
     36
       0,    // No expiry for refresh token cookie (session lifetime controlled server-side)
     37
       "/",  // Available on all routes
     38
       "",
     39
       os.Getenv("GIN_MODE") == "release", // HTTPS-only in production
     40
       true, // Prevents JavaScript access to mitigate XSS attacks
     41
     )
     ```

   * FastAPI

     Spring

     ```java
     6 collapsed lines
     1
     import javax.servlet.http.Cookie;
     2
     import javax.servlet.http.HttpServletResponse;
     3
     import org.springframework.core.env.Environment;
     4
     @Autowired
     5
     private Environment env;
     6


     7
     // Extract authentication data from the successful authentication response
     8
     String accessToken = authResult.getAccessToken();
     9
     int expiresIn = authResult.getExpiresIn();
     10
     String refreshToken = authResult.getRefreshToken();
     11
     User user = authResult.getUser();
     12


     13
     // Encrypt tokens before storing to add an additional security layer
     14
     String encryptedAccessToken = encrypt(accessToken);
     15
     String encryptedRefreshToken = encrypt(refreshToken);
     16


     17
     // Store encrypted access token in HttpOnly cookie
     18
     Cookie accessTokenCookie = new Cookie("accessToken", encryptedAccessToken);
     19
     accessTokenCookie.setMaxAge(expiresIn - 60); // Subtract 60s buffer for clock skew (seconds in Spring)
     20
     accessTokenCookie.setHttpOnly(true); // Prevents JavaScript access to mitigate XSS attacks
     21
     accessTokenCookie.setSecure("production".equals(env.getActiveProfiles()[0])); // HTTPS-only in production
     22
     accessTokenCookie.setPath("/"); // Available on all routes
     23
     response.addCookie(accessTokenCookie);
     24
     response.setHeader("Set-Cookie",
     25
       response.getHeader("Set-Cookie") + "; SameSite=Strict"); // Prevents CSRF attacks
     26


     27
     // Store encrypted refresh token in separate HttpOnly cookie
     28
     Cookie refreshTokenCookie = new Cookie("refreshToken", encryptedRefreshToken);
     29
     refreshTokenCookie.setHttpOnly(true); // Prevents JavaScript access to mitigate XSS attacks
     30
     refreshTokenCookie.setSecure("production".equals(env.getActiveProfiles()[0])); // HTTPS-only in production
     31
     refreshTokenCookie.setPath("/"); // Available on all routes
     32
     response.addCookie(refreshTokenCookie);
     ```

2. ## Check the session before handling requests

   [Section titled “Check the session before handling requests”](#check-the-session-before-handling-requests)

   Session middleware refreshes `sk_session` and redirects to `/login` when the session is missing. A missing session returns **302**, not JSON **401**.

   * Node.js

     * Express

       Guard a route with `auth.requiresAuth`. On success, `req.scalekitUser` is access-token claims. `sub` is always present.

       server.ts

       ```ts
       app.get('/account', auth.requiresAuth, (req, res) => {
         res.json({ sub: req.scalekitUser?.sub });
       });
       ```

     * Next.js

       Wrap a Route Handler with `withAuth`. Use `createMiddleware()` so every other path fails closed. List public paths in `publicRoutes`.

       app/account/route.ts

       ```ts
       import { auth } from '../../lib/auth';


       export const GET = auth.withAuth(async (request, { user }) => {
         return Response.json({ sub: user?.sub });
       });
       ```

       middleware.ts

       ```ts
       import { auth } from './lib/auth';


       export default auth.createMiddleware({
         publicRoutes: ['/', '/pricing'],
       });


       export const config = {
         runtime: 'nodejs',
         matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
       };
       ```

   * Python

     Guard a route with `auth.requiresAuth`. On success, `req.scalekitUser` is access-token claims. `sub` is always present.

     server.ts

     ```ts
     app.get('/account', auth.requiresAuth, (req, res) => {
       res.json({ sub: req.scalekitUser?.sub });
     });
     ```

   * Go

     Wrap a Route Handler with `withAuth`. Use `createMiddleware()` so every other path fails closed. List public paths in `publicRoutes`.

     app/account/route.ts

     ```ts
     import { auth } from '../../lib/auth';


     export const GET = auth.withAuth(async (request, { user }) => {
       return Response.json({ sub: user?.sub });
     });
     ```

     middleware.ts

     ```ts
     import { auth } from './lib/auth';


     export default auth.createMiddleware({
       publicRoutes: ['/', '/pricing'],
     });


     export const config = {
       runtime: 'nodejs',
       matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
     };
     ```

   * Java

     * Flask

       Guard a view with `@auth.requires_auth`. `auth.current_user` is access-token claims.

       app.py

       ```py
       @app.route("/account")
       @auth.requires_auth
       def account():
           return {"sub": auth.current_user["sub"]}
       ```

     * FastAPI

       Protect a route with `Depends(auth.requires_auth)`. The dependency is the user claims.

       app.py

       ```py
       from fastapi import Depends


       @app.get("/account")
       async def account(user: dict = Depends(auth.requires_auth)):
           return {"sub": user["sub"]}
       ```

   * Express

     Guard a view with `@auth.requires_auth`. `auth.current_user` is access-token claims.

     app.py

     ```py
     @app.route("/account")
     @auth.requires_auth
     def account():
         return {"sub": auth.current_user["sub"]}
     ```

   * Next.js

     Protect a route with `Depends(auth.requires_auth)`. The dependency is the user claims.

     app.py

     ```py
     from fastapi import Depends


     @app.get("/account")
     async def account(user: dict = Depends(auth.requires_auth)):
         return {"sub": user["sub"]}
     ```

   * Flask

     middleware/auth.go

     ```go
     5 collapsed lines
     1
     import (
     2
       "net/http"
     3
       "os"
     4
       "github.com/gin-gonic/gin"
     5
     )
     6
     func VerifyToken() gin.HandlerFunc {
     7
       return func(c *gin.Context) {
     8
         // Extract encrypted tokens from request cookies
     9
         accessToken, err := c.Cookie("accessToken")
     10
         if err != nil || accessToken == "" {
     11
           c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
     12
           c.Abort()
     13
           return
     14
         }
     15


     16
         // Decrypt the access token before validation
     17
         decryptedAccessToken := decrypt(accessToken)
     18


     19
         // Verify token validity using Scalekit's validation method
     20
         isValid, err := scalekitClient.ValidateAccessToken(c.Request.Context(), decryptedAccessToken)
     21


     22
         if (err != nil || !isValid) {
     23
           // Token expired - attempt transparent refresh
     24
           refreshToken, err := c.Cookie("refreshToken")
     25
           if err == nil && refreshToken != "" {
     26
             decryptedRefreshToken := decrypt(refreshToken)
     27
             authResult, err := scalekitClient.RefreshAccessToken(c.Request.Context(), decryptedRefreshToken)
     28


     29
             if err == nil {
     30
               // Encrypt and store new tokens
     31
               c.SetSameSite(http.SameSiteStrictMode)
     32
               c.SetCookie(
     33
                 "accessToken",
     34
                 encrypt(authResult.AccessToken),
     35
                 authResult.ExpiresIn-60,
     36
                 "/",
     37
                 "",
     38
                 os.Getenv("GIN_MODE") == "release",
     39
                 true,
     40
               )
     41
               c.SetCookie(
     42
                 "refreshToken",
     43
                 encrypt(authResult.RefreshToken),
     44
                 0,
     45
                 "/",
     46
                 "",
     47
                 os.Getenv("GIN_MODE") == "release",
     48
                 true,
     49
               )
     50
               c.Next()
     51
               return
     52
             }
     53
           }
     54


     55
           c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired. Please sign in again."})
     56
           c.Abort()
     57
           return
     58
         }
     59


     60
         // Token is valid, proceed to the next handler in the chain
     61
         c.Next()
     62
       }
     63
     }
     ```

   * FastAPI

     middleware/AuthInterceptor.java

     ```java
     5 collapsed lines
     1
     import javax.servlet.http.HttpServletRequest;
     2
     import javax.servlet.http.HttpServletResponse;
     3
     import javax.servlet.http.Cookie;
     4
     import org.springframework.web.servlet.HandlerInterceptor;
     5
     import org.springframework.core.env.Environment;
     6


     7
     /**
     8
      * Intercepts HTTP requests to verify authentication tokens.
     9
      * Transparently refreshes expired tokens to maintain user sessions.
     10
      */
     11
     @Component
     12
     public class AuthInterceptor implements HandlerInterceptor {
     13
       @Autowired
     14
       private Environment env;
     15


     16
       @Override
     17
       public boolean preHandle(
     18
         HttpServletRequest request,
     19
         HttpServletResponse response,
     20
         Object handler
     21
       ) throws Exception {
     7 collapsed lines
     22
         // Extract encrypted tokens from cookies
     23
         String accessToken = getCookieValue(request, "accessToken");
     24
         String refreshToken = getCookieValue(request, "refreshToken");
     25


     26
         if (accessToken == null) {
     27
           response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     28
           response.getWriter().write("{\"error\": \"Authentication required\"}");
     29
           return false;
     30
         }
     31


     32
         try {
     33
           // Decrypt the access token before validation
     34
           String decryptedAccessToken = decrypt(accessToken);
     35


     36
           // Verify token validity using Scalekit's validation method
     37
           boolean isValid = scalekitClient.validateAccessToken(decryptedAccessToken);
     38


     39
           if (!isValid && refreshToken != null) {
     40
             // Token expired - refresh it transparently
     41
             String decryptedRefreshToken = decrypt(refreshToken);
     42
             AuthResult authResult = scalekitClient.authentication().refreshToken(decryptedRefreshToken);
     43


     44
             // Encrypt and store new tokens
     20 collapsed lines
     45
             Cookie accessTokenCookie = new Cookie("accessToken", encrypt(authResult.getAccessToken()));
     46
             accessTokenCookie.setMaxAge(authResult.getExpiresIn() - 60);
     47
             accessTokenCookie.setHttpOnly(true);
     48
             accessTokenCookie.setSecure("production".equals(env.getActiveProfiles()[0]));
     49
             accessTokenCookie.setPath("/");
     50
             response.addCookie(accessTokenCookie);
     51


     52
             Cookie refreshTokenCookie = new Cookie("refreshToken", encrypt(authResult.getRefreshToken()));
     53
             refreshTokenCookie.setHttpOnly(true);
     54
             refreshTokenCookie.setSecure("production".equals(env.getActiveProfiles()[0]));
     55
             refreshTokenCookie.setPath("/");
     56
             response.addCookie(refreshTokenCookie);
     57
             response.setHeader("Set-Cookie", response.getHeader("Set-Cookie") + "; SameSite=Strict");
     58


     59
             return true;
     60
           }
     61


     62
           if (!isValid) {
     63
             response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     64
             response.getWriter().write("{\"error\": \"Session expired. Please sign in again.\"}");
     65
             return false;
     66
           }
     67


     68
           // Token is valid, allow request to proceed
     69
           return true;
     70
         } catch (Exception e) {
     71
           response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
     72
           response.getWriter().write("{\"error\": \"Authentication failed\"}");
     73
           return false;
     74
         }
     75
       }
     76


     77
       private String getCookieValue(HttpServletRequest request, String cookieName) {
     78
         Cookie[] cookies = request.getCookies();
     79
         if (cookies != null) {
     80
           for (Cookie cookie : cookies) {
     81
             if (cookieName.equals(cookie.getName())) {
     82
               return cookie.getValue();
     83
             }
     84
           }
     85
         }
     86
         return null;
     87
       }
     88
      }
     ```

   Build it yourself — store cookies and call validateAccessToken

   Use this path when you do not use session middleware. Request `offline_access` so you receive a refresh token. Scalekit rotates refresh tokens. Store the new refresh token and discard the old one.

   Store the access token in an HttpOnly cookie. Store the refresh token in a separate HttpOnly cookie. Encrypt both values.

   Express.js — set cookies

   ```javascript
   const { accessToken, expiresIn, refreshToken } = authResult;
   res.cookie('accessToken', encrypt(accessToken), {
     maxAge: (expiresIn - 60) * 1000,
     httpOnly: true,
     secure: process.env.NODE_ENV === 'production',
     sameSite: 'lax',
   });
   res.cookie('refreshToken', encrypt(refreshToken), {
     httpOnly: true,
     secure: process.env.NODE_ENV === 'production',
     sameSite: 'lax',
   });
   ```

   Express.js — verify and refresh

   ```javascript
   1
   async function verifyToken(req, res, next) {
   2
     const { accessToken, refreshToken } = req.cookies;
   3
     if (!accessToken) {
   4
       return res.status(401).json({ error: 'Authentication required' });
   5
     }
   6
     const decryptedAccessToken = decrypt(accessToken);
   7
     const isValid = await scalekit.validateAccessToken(decryptedAccessToken);
   8
     if (!isValid && refreshToken) {
   9
       const authResult = await scalekit.refreshAccessToken(decrypt(refreshToken));
   10
       res.cookie('accessToken', encrypt(authResult.accessToken), {
   11
         maxAge: (authResult.expiresIn - 60) * 1000,
   12
         httpOnly: true,
   13
         secure: process.env.NODE_ENV === 'production',
   14
         sameSite: 'lax',
   15
       });
   16
       res.cookie('refreshToken', encrypt(authResult.refreshToken), {
   17
         httpOnly: true,
   18
         secure: process.env.NODE_ENV === 'production',
   19
         sameSite: 'lax',
   20
       });
   21
       return next();
   22
     }
   23
     if (!isValid) {
   24
       return res.status(401).json({ error: 'Session expired. Please sign in again.' });
   25
     }
   26
     next();
   27
   }
   ```

   Flask — verify and refresh

   ```python
   1
   import os
   2
   from functools import wraps
   3
   from flask import request, jsonify, make_response
   4


   5
   def verify_token(f):
   6
       @wraps(f)
   7
       def decorated_function(*args, **kwargs):
   8
           access_token = request.cookies.get("accessToken")
   9
           refresh_token = request.cookies.get("refreshToken")
   10
           if not access_token:
   11
               return jsonify({"error": "Authentication required"}), 401
   12
           is_valid = scalekit_client.validate_access_token(decrypt(access_token))
   13
           if not is_valid and refresh_token:
   14
               auth_result = scalekit_client.refresh_access_token(decrypt(refresh_token))
   15
               response = make_response(f(*args, **kwargs))
   16
               response.set_cookie(
   17
                   "accessToken",
   18
                   encrypt(auth_result.access_token),
   19
                   max_age=auth_result.expires_in - 60,
   20
                   httponly=True,
   21
                   secure=os.environ.get("FLASK_ENV") == "production",
   22
                   samesite="Lax",
   23
               )
   24
               response.set_cookie(
   25
                   "refreshToken",
   26
                   encrypt(auth_result.refresh_token),
   27
                   httponly=True,
   28
                   secure=os.environ.get("FLASK_ENV") == "production",
   29
                   samesite="Lax",
   30
               )
   31
               return response
   32
           if not is_valid:
   33
               return jsonify({"error": "Session expired. Please sign in again."}), 401
   34
           return f(*args, **kwargs)
   35
       return decorated_function
   ```

   TypeScript: get typed claims from validateToken

   Use a generic type parameter to get properly typed claims instead of `unknown`. Pass `JWTPayload` from `jose` for access tokens, or `IdTokenClaim` from `@scalekit-sdk/node` for ID tokens:

   ```typescript
   import type { JWTPayload } from 'jose';
   import type { IdTokenClaim } from '@scalekit-sdk/node';


   // Access token — typed as JWTPayload
   const claims = await scalekit.validateToken(accessToken);
   console.log(claims.sub); // user ID


   // ID token — typed with full user profile claims
   const idClaims = await scalekit.validateToken(idToken);
   console.log(idClaims.email);
   ```

3. ## Configure session security and duration

   [Section titled “Configure session security and duration”](#configure-session-security-and-duration)

   Manage user session behavior directly from your Scalekit dashboard without modifying application code. Configure session durations and authentication frequency to balance security and user experience for your application.

   ![Scalekit Session Policy page with fields for absolute session timeout, idle session timeout, and access token lifetime](/.netlify/images?url=_astro%2Fsession-policies-dashboard.CedTumlx.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

   In your Scalekit dashboard, the **Session settings** page lets you set these options:

   * **Absolute session timeout**: This is the maximum time a user can stay signed in, no matter what. After this time, they must log in again. For example, if you set it to 30 minutes, users will be logged out after 30 minutes, even if they are still using your app.

   * **Idle session timeout**: This is the time your app waits before logging out a user who is not active. If you turn this on, the session will end if the user does nothing for the set time. For example, if you set it to 10 minutes, and the user does not click or type for 10 minutes, they will be logged out.

   * **Access token lifetime**: This is how long an access token is valid. When it expires, your app needs to get a new token (using the refresh token) so the user can keep using the app without logging in again. For example, if you set it to 5 minutes, your app will need to refresh the token every 5 minutes.

   Shorter timeouts provide better security, while longer timeouts reduce authentication interruptions.

   Need different session policies per customer?

   If specific enterprise customers require stricter session timeouts than your application defaults, you can override the absolute and idle session timeouts on a per-organization basis. See [Organization session policy](/authenticate/manage-organizations/organization-session-policy/) to configure custom policies for individual organizations.

4. ## Manage sessions remotely API

   [Section titled “Manage sessions remotely ”](#manage-sessions-remotely-)

   Beyond client-side session management, Scalekit provides powerful APIs to manage user sessions remotely from your backend application. This enables you to build features like active session management in user account settings, security incident response, or administrative session control.

   These APIs are particularly useful for:

   * Displaying all active sessions in user account settings
   * Allowing users to revoke specific sessions from unfamiliar devices
   * Security incident response and suspicious session termination

   - Node.js

     Session Management SDK

     ```javascript
     1
     // Get details for a specific session
     2
     const sessionDetails = await scalekit.session.getSession('ses_1234567890123456');
     3


     4
     // List all sessions for a user with optional filtering
     5
     const userSessions = await scalekit.session.getUserSessions('usr_1234567890123456', {
     6
       pageSize: 10,
     7
       filter: {
     8
         status: ['ACTIVE'], // Filter for active sessions only
     9
         startTime: new Date('2025-01-01T00:00:00Z'),
     10
         endTime: new Date('2025-12-31T23:59:59Z')
     11
       }
     12
     });
     13


     14
     // Revoke a specific session (useful for "Sign out this device" functionality)
     15
     const revokedSession = await scalekit.session.revokeSession('ses_1234567890123456');
     16


     17
     // Revoke all sessions for a user (useful for "Sign out all devices" functionality)
     18
     const revokedSessions = await scalekit.session.revokeAllUserSessions('usr_1234567890123456');
     19
     console.log(`Revoked sessions for user`);
     ```

   - Python

     Session Management SDK

     ```python
     1
     # Get details for a specific session
     2
     session_details = scalekit_client.sessions.get_session(session_id="ses_1234567890123456")
     3


     4
     # List all sessions for a user with optional filtering
     5
     from google.protobuf.timestamp_pb2 import Timestamp
     6
     from datetime import datetime
     7


     8
     start_time = Timestamp()
     9
     start_time.FromDatetime(datetime(2025, 1, 1))
     10
     end_time = Timestamp()
     11
     end_time.FromDatetime(datetime(2025, 12, 31))
     12


     13
     filter_obj = scalekit_client.sessions.create_session_filter(
     14
         status=["ACTIVE"], start_time=start_time, end_time=end_time
     15
     )
     16
     user_sessions = scalekit_client.sessions.get_user_sessions(
     17
         user_id="usr_1234567890123456", page_size=10, filter=filter_obj
     18
     )
     19


     20
     # Revoke a specific session (useful for "Sign out this device" functionality)
     21
     revoked_session = scalekit_client.sessions.revoke_session(session_id="ses_1234567890123456")
     22


     23
     # Revoke all sessions for a user (useful for "Sign out all devices" functionality)
     24
     revoked_sessions = scalekit_client.sessions.revoke_all_user_sessions(user_id="usr_1234567890123456")
     25
     print(f"Revoked sessions for user")
     ```

   - Go

     Session Management SDK

     ```go
     1
     // Get details for a specific session
     2
     sessionDetails, err := scalekitClient.Session().GetSession(ctx, "ses_1234567890123456")
     3
     if err != nil {
     4
         log.Fatal(err)
     5
     }
     6


     7
     // List all sessions for a user with optional filtering
     8
     // import "time", sessionsv1 "...", "google.golang.org/protobuf/types/known/timestamppb"
     9
     startTime, _ := time.Parse(time.RFC3339, "2025-01-01T00:00:00Z")
     10
     endTime, _ := time.Parse(time.RFC3339, "2025-12-31T23:59:59Z")
     11
     filter := &sessionsv1.UserSessionFilter{
     12
         Status:    []string{"ACTIVE"}, // Filter for active sessions only
     13
         StartTime: timestamppb.New(startTime),
     14
         EndTime:   timestamppb.New(endTime),
     15
     }
     16
     userSessions, err := scalekitClient.Session().GetUserSessions(ctx, "usr_1234567890123456", 10, "", filter)
     17
     if err != nil {
     18
         log.Fatal(err)
     19
     }
     20


     21
     // Revoke a specific session (useful for "Sign out this device" functionality)
     22
     revokedSession, err := scalekitClient.Session().RevokeSession(ctx, "ses_1234567890123456")
     23
     if err != nil {
     24
         log.Fatal(err)
     25
     }
     26


     27
     // Revoke all sessions for a user (useful for "Sign out all devices" functionality)
     28
     revokedSessions, err := scalekitClient.Session().RevokeAllUserSessions(ctx, "usr_1234567890123456")
     29
     if err != nil {
     30
         log.Fatal(err)
     31
     }
     32
     fmt.Printf("Revoked sessions for user")
     ```

   - Java

     Session Management SDK

     ```java
     1
     // Get details for a specific session
     2
     SessionDetails sessionDetails = scalekitClient.sessions().getSession("ses_1234567890123456");
     3


     4
     // List all sessions for a user with optional filtering
     5
     // import UserSessionFilter, Timestamp, Instant
     6
     UserSessionFilter filter = UserSessionFilter.newBuilder()
     7
         .addStatus("ACTIVE")
     8
         .setStartTime(Timestamp.newBuilder().setSeconds(Instant.parse("2025-01-01T00:00:00Z").getEpochSecond()).build())
     9
         .setEndTime(Timestamp.newBuilder().setSeconds(Instant.parse("2025-12-31T23:59:59Z").getEpochSecond()).build())
     10
         .build();
     11
     UserSessionDetails userSessions = scalekitClient.sessions().getUserSessions("usr_1234567890123456", 10, "", filter);
     12


     13
     // Revoke a specific session (useful for "Sign out this device" functionality)
     14
     RevokeSessionResponse revokedSession = scalekitClient.sessions().revokeSession("ses_1234567890123456");
     15


     16
     // Revoke all sessions for a user (useful for "Sign out all devices" functionality)
     17
     RevokeAllUserSessionsResponse revokedSessions = scalekitClient.sessions().revokeAllUserSessions("usr_1234567890123456");
     18
     System.out.println("Revoked sessions for user");
     ```

After you add the helper, open `/account` in the browser. A missing session goes to `/login`. After login, the browser returns to `/account`.

Session middleware validates `sk_session` on each request and refreshes it before expiry. If the refresh token is invalid, the helper sends the browser to `/login`.

---
# DOCUMENT BOUNDARY
---

# Manage applications

> Register and manage applications in your shared authentication system

Register and manage applications in Scalekit. Each application gets its own OAuth client and configuration while sharing the same underlying user session across your web, mobile, and desktop apps.

1. ## Navigate to Applications

   [Section titled “Navigate to Applications”](#navigate-to-applications)

   1. Sign in to ****
   2. From the left sidebar, go to **Developers > Applications**

   You will see a list of applications already created for the selected environment.

   Scalekit creates a default web application

   Scalekit creates a **default web application** for every environment at creation time to help developers get started quickly. This app is environment-scoped and **cannot be deleted**.

2. ## Create a new application

   [Section titled “Create a new application”](#create-a-new-application)

   Click **Create Application** to add a new app. You’ll be asked to provide:

   * **Application name** — A human-readable name for identifying the app
   * **Application type** — Determines how authentication and credentials work

   Available application types:

   * **Web Application** — Server-side applications that can securely store secrets
   * **Single Page Application (SPA)** — Browser-based applications; public clients with PKCE enforced
   * **Native Application** — Desktop or mobile apps; public clients with PKCE enforced

   ![Create application modal showing app name and type selection](/.netlify/images?url=_astro%2Fweb-modal.BXg9RPmN.png\&w=1124\&h=944\&dpl=6ae471fc143c9a4c27550463)

   Once created, Scalekit generates a **Client ID**. Only Web Applications can generate **Client Secrets**.

3. ## Application configuration

   [Section titled “Application configuration”](#application-configuration)

   ### Application details

   [Section titled “Application details”](#application-details)

   Open an application to view and edit its configuration.

   * **Allow Scalekit Management API access** — Enables this application’s credentials to call Scalekit Management APIs. Applicable only to **Web Applications**.
   * **Enforce PKCE** — Requires PKCE for authorization requests. Always enabled and not editable for **SPA** and **Native** applications.
   * **Access token expiry time** — Overrides the environment default access token lifetime for this application.

   Access token expiry must be shorter than idle session timeout

   If tokens outlive the session, users may encounter inconsistent logout behavior across apps. When the session expires but the access token is still valid, subsequent token refresh attempts will fail because the underlying session no longer exists.

   ![Application details page with configuration options](/.netlify/images?url=_astro%2Fweb-app-details.BZtG_A3x.png\&w=1640\&h=1100\&dpl=6ae471fc143c9a4c27550463)

   ### Client credentials

   [Section titled “Client credentials”](#client-credentials)

   Each application has a unique **Client ID**. When you generate a new client secret, Scalekit shows it **only once**. Copy and store it securely.

   Treat client secrets like passwords

   Anyone with access to your client secret can authenticate as your application and obtain tokens for any user. Never commit secrets to version control, expose them in client-side code, or share them in plain text. Use environment variables or a secrets manager.

   * **Web Applications**

     * Can generate a **Client Secret**
     * A maximum of **two active secrets** is allowed at a time
     * Generating a new secret always creates a **new value**, enabling safe rotation

   ![Client credentials section showing Client ID and secret management](/.netlify/images?url=_astro%2Fweb-client-creds.aNZmxstS.png\&w=1214\&h=628\&dpl=6ae471fc143c9a4c27550463)

   * **SPA and Native Applications**

     * Do not have client secrets
     * Authenticate using Authorization Code with PKCE only

   ![SPA client ID section without client secret option](/.netlify/images?url=_astro%2Fspa-client-id.DFzivdPM.png\&w=1168\&h=412\&dpl=6ae471fc143c9a4c27550463)

4. ## Configure redirect URLs

   [Section titled “Configure redirect URLs”](#configure-redirect-urls)

   Open the **Redirects** tab for an application to manage redirect endpoints. These URLs act as an allowlist and control where Scalekit can redirect users during authentication flows.

   ### Redirect URL types

   [Section titled “Redirect URL types”](#redirect-url-types)

   * **Post login URLs** — Allowed values for `redirect_uri` used with `/oauth/authorize`
   * **Initiate login URL** — Where Scalekit redirects users when authentication starts outside your app
   * **Post logout URLs** — Where users are redirected after a successful logout
   * **Back-channel logout URL** — A secure endpoint that Scalekit calls to notify your application that a user session has been revoked

   ![Redirect URLs configuration tab with URL types](/.netlify/images?url=_astro%2Fweb-app-redirects.CqgtckPK.png\&w=2604\&h=1396\&dpl=6ae471fc143c9a4c27550463)

   Back-channel logout is only available for Web Applications

   Back-channel logout requires a backend endpoint to receive notifications from Scalekit. SPA and Native applications cannot receive back-channel logout notifications because they don’t have a persistent server.

   For definitions, validation rules, custom URI schemes, and environment-specific behavior, see [Redirect URL configuration](/guides/dashboard/redirects/).

5. ## Delete an application

   [Section titled “Delete an application”](#delete-an-application)

   Delete applications from the bottom of the configuration page.

   ![Delete application button at bottom of configuration page](/.netlify/images?url=_astro%2Fdelete-app.Bz8WrFNb.png\&w=2556\&h=194\&dpl=6ae471fc143c9a4c27550463)

   Deleting an application is permanent

   This action is **permanent and irreversible**. Existing refresh tokens associated with the application will no longer be valid, and users will need to re-authenticate. Ensure you have communicated this change to affected users before deleting.

---
# DOCUMENT BOUNDARY
---

# Mobile & desktop applications

> Implement Multi-App Authentication for mobile and desktop apps using Authorization Code with PKCE

Implement login, token management, and logout in your mobile or desktop application using Authorization Code with PKCE. Native apps are public OAuth clients that cannot securely store a `client_secret` in the application binary, so they use PKCE to protect the authorization flow. This guide covers initiating login through the system browser, handling deep link callbacks, managing tokens in secure storage, and implementing logout.

Tip

[**Check out the example apps on GitHub**](https://github.com/scalekit-inc/multiapp-demo) to see Web, SPA, Desktop, and Mobile apps sharing a single Scalekit session.

## Prerequisites

[Section titled “Prerequisites”](#prerequisites)

Before you begin, ensure you have:

* A Scalekit account with an environment configured

* Your environment URL (`ENV_URL`), e.g., `https://yourenv.scalekit.com`

* A native application registered in Scalekit with a `client_id` ([Create one](/authenticate/fsa/multiapp/manage-apps))

* A callback URI configured:

  * **Mobile**: Custom URI scheme (e.g., `myapp://callback`) or universal/app links
  * **Desktop**: Custom URI scheme or loopback address (e.g., `http://127.0.0.1:PORT/callback`)

## High-level flow

[Section titled “High-level flow”](#high-level-flow)

![Native app login flow: the app opens the system browser for Scalekit login with PKCE and gets control back through a deep link or loopback redirect](/d2/docs/authenticate/fsa/multiapp/native-app-0.svg)

## Step-by-step implementation

[Section titled “Step-by-step implementation”](#step-by-step-implementation)

1. ## Initiate login or signup

   [Section titled “Initiate login or signup”](#initiate-login-or-signup)

   Initiate login by opening the system browser with the authorization URL. Always use the system browser rather than an embedded WebView — this lets users leverage existing sessions and provides a familiar, secure authentication experience.

   ```sh
   /oauth/authorize?
     response_type=code&
     client_id=&
     redirect_uri=&
     scope=openid+profile+email+offline_access&
     state=&
     code_challenge=&
     code_challenge_method=S256
   ```

   Generate and store these values before opening the browser:

   * `state` — Validate this on callback to prevent CSRF attacks
   * `code_verifier` — A cryptographically random string you keep in the app
   * `code_challenge` — Derived from the verifier using S256 hashing; send this in the authorization URL

   Why PKCE is required for native apps

   Native apps cannot keep a `client_secret` secure because the secret would be embedded in the application binary and could be extracted through reverse engineering. PKCE protects against authorization code interception attacks where malware on the device captures the authorization code from the callback URI.

   For detailed parameter definitions, see [Initiate signup/login](/authenticate/fsa/implement-login).

2. ## Handle the callback and complete login

   [Section titled “Handle the callback and complete login”](#handle-the-callback-and-complete-login)

   After authentication, Scalekit redirects the user back to your application using the registered callback mechanism.

   Common callback patterns:

   * **Mobile apps** — Custom URI schemes (e.g., `myapp://callback`) or universal links (iOS) / app links (Android)
   * **Desktop apps** — Custom URI schemes or a temporary HTTP server on localhost

   Your callback handler must:

   * Validate the returned `state` matches what you stored — this confirms the response is for your original request
   * Handle any error parameters before processing
   * Exchange the authorization code for tokens by including the `code_verifier`

   ```sh
   POST /oauth/token
   Content-Type: application/x-www-form-urlencoded


   grant_type=authorization_code&
   client_id=&
   code=&
   redirect_uri=&
   code_verifier=
   ```

   ```json
   {
     "access_token": "...",
     "refresh_token": "...",
     "id_token": "...",
     "expires_in": 299
   }
   ```

   Authorization codes expire after one use

   Authorization codes are single-use and expire quickly (approximately 10 minutes). If you attempt to reuse a code or it expires, start a new login flow to obtain a fresh authorization code.

3. ## Manage sessions and token refresh

   [Section titled “Manage sessions and token refresh”](#manage-sessions-and-token-refresh)

   Store tokens in platform-specific secure storage and validate them on each request. When access tokens expire, use the refresh token to obtain new ones without requiring the user to re-authenticate.

   **Token roles**

   * **Access token** — Short-lived token (default 5 minutes) for authenticated API requests
   * **Refresh token** — Long-lived token to obtain new access tokens
   * **ID token** — JWT containing user identity claims; required for logout

   Store tokens using secure, OS-backed storage appropriate for each platform. See [Token storage security](#token-storage-security) for platform-specific recommendations.

   When an access token expires, request new tokens:

   ```sh
   POST /oauth/token
   Content-Type: application/x-www-form-urlencoded


   grant_type=refresh_token&
   client_id=&
   refresh_token=
   ```

   Validate access tokens by verifying:

   * Token signature using Scalekit’s public keys (JWKS endpoint)
   * `iss` matches your Scalekit environment URL
   * `aud` includes your `client_id`
   * `exp` and `iat` are valid timestamps

   Public keys for signature verification:

   ```sh
   /keys
   ```

4. ## Implement logout

   [Section titled “Implement logout”](#implement-logout)

   Clear your local session and redirect the system browser to Scalekit’s logout endpoint to invalidate the shared session.

   Your logout action must:

   * Extract the ID token before clearing local storage
   * Clear tokens from secure storage
   * Open the system browser to Scalekit’s logout endpoint

   ```sh
   /oidc/logout?
     id_token_hint=&
     post_logout_redirect_uri=
   ```

   Logout must open the system browser

   Use the system browser to navigate to the `/oidc/logout` endpoint, not a backend API call. The browser ensures Scalekit’s session cookie is sent with the request, allowing Scalekit to identify and terminate the correct session.

## Handle errors

[Section titled “Handle errors”](#handle-errors)

When authentication fails, Scalekit redirects to your callback URI with error parameters instead of an authorization code:

```plaintext
myapp://callback?error=access_denied&error_description=User+denied+access&state=
```

Check for errors before processing the authorization code:

* Check if the `error` parameter exists in the callback URI
* Log the `error` and `error_description` for debugging
* Display a user-friendly message in your app
* Provide an option to retry login

Common error codes:

| Error             | Description                                                  |
| ----------------- | ------------------------------------------------------------ |
| `access_denied`   | User denied the authorization request                        |
| `invalid_request` | Missing or invalid parameters (e.g., invalid PKCE challenge) |
| `server_error`    | Scalekit encountered an unexpected error                     |

## Token storage security

[Section titled “Token storage security”](#token-storage-security)

Native apps have access to platform-specific secure storage mechanisms that encrypt tokens at rest and protect them from other applications. Unlike browser storage, these mechanisms provide strong protection against token theft from device compromise or malware.

Use platform-specific secure storage for each platform:

| Platform | Recommended Storage                    |
| -------- | -------------------------------------- |
| iOS      | Keychain Services                      |
| Android  | EncryptedSharedPreferences or Keystore |
| macOS    | Keychain                               |
| Windows  | Windows Credential Manager or DPAPI    |
| Linux    | Secret Service API (libsecret)         |

**Recommendations:**

* Never store tokens in plain text files, shared preferences, or unencrypted databases — these can be read by any application with storage access
* Use biometric or device PIN protection for sensitive token access when available — this adds a second factor for token access
* Clear tokens from secure storage on logout — this ensures a clean state for the next authentication

Never embed secrets in your application binary

Credentials embedded in application code or configuration files can be extracted through reverse engineering. Always use PKCE for native apps instead of relying on a `client_secret`. If you need to make authenticated API calls from your backend, use a separate web application with proper secret management.

## What’s next

[Section titled “What’s next”](#whats-next)

* [Set up a custom domain](/guides/custom-domain) for your authentication pages
* [Add enterprise SSO](/authenticate/auth-methods/enterprise-sso/) to support SAML and OIDC with your customers’ identity providers

---
# DOCUMENT BOUNDARY
---

# Single page application

> Implement Multi-App Authentication for single page apps using Authorization Code with PKCE

Implement login, token management, and logout in your single page application (SPA) using Authorization Code with PKCE. SPAs run entirely in the browser and cannot securely store a `client_secret`, so they use PKCE (Proof Key for Code Exchange) to protect the authorization flow. This guide covers initiating login from your SPA, exchanging authorization codes for tokens, managing sessions, and implementing logout.

Tip

[**Check out the example apps on GitHub**](https://github.com/scalekit-inc/multiapp-demo) to see Web, SPA, Desktop, and Mobile apps sharing a single Scalekit session.

## Prerequisites

[Section titled “Prerequisites”](#prerequisites)

Before you begin, ensure you have:

* A Scalekit account with an environment configured
* Your environment URL (`ENV_URL`), e.g., `https://yourenv.scalekit.com`
* A SPA registered in Scalekit with a `client_id` ([Create one](/authenticate/fsa/multiapp/manage-apps))
* At least one redirect URL configured in **Dashboard > Developers > Applications > \[Your App] > Redirects**

## High-level flow

[Section titled “High-level flow”](#high-level-flow)

![Single-page app login flow: the browser app redirects to Scalekit and exchanges the code with PKCE](/d2/docs/authenticate/fsa/multiapp/single-page-app-0.svg)

## Step-by-step implementation

[Section titled “Step-by-step implementation”](#step-by-step-implementation)

1. ## Initiate login or signup

   [Section titled “Initiate login or signup”](#initiate-login-or-signup)

   Initiate login by redirecting the user to Scalekit’s hosted login page. Include the PKCE code challenge in the authorization request to protect against authorization code interception attacks.

   ```sh
   /oauth/authorize?
     response_type=code&
     client_id=&
     redirect_uri=&
     scope=openid+profile+email+offline_access&
     state=&
     code_challenge=&
     code_challenge_method=S256
   ```

   Generate and store these values before redirecting:

   * `state` — Validate this on callback to prevent CSRF attacks
   * `code_verifier` — A cryptographically random string you keep locally
   * `code_challenge` — Derived from the verifier using S256 hashing; send this in the authorization URL

   Why PKCE is required for SPAs

   SPAs are public clients that cannot keep a `client_secret` secure because all code runs in the browser. PKCE protects against authorization code interception attacks where an attacker captures the authorization code from the redirect URI. Without PKCE, anyone who intercepts the code could exchange it for tokens.

   For detailed parameter definitions, see [Initiate signup/login](/authenticate/fsa/implement-login).

2. ## Handle the callback and complete login

   [Section titled “Handle the callback and complete login”](#handle-the-callback-and-complete-login)

   After authentication, Scalekit redirects the user back to your callback URL with an authorization `code` and the `state` you sent.

   Your callback handler must:

   * Validate the returned `state` matches what you stored — this confirms the response is for your original request
   * Handle any error parameters before processing
   * Exchange the authorization code for tokens by including the `code_verifier`

   ```sh
   POST /oauth/token
   Content-Type: application/x-www-form-urlencoded


   grant_type=authorization_code&
   client_id=&
   code=&
   redirect_uri=&
   code_verifier=
   ```

   ```json
   {
     "access_token": "...",
     "refresh_token": "...",
     "id_token": "...",
     "expires_in": 299
   }
   ```

   Authorization codes expire after one use

   Authorization codes are single-use and expire quickly (approximately 10 minutes). If you attempt to reuse a code or it expires, start a new login flow to obtain a fresh authorization code.

3. ## Manage sessions and token refresh

   [Section titled “Manage sessions and token refresh”](#manage-sessions-and-token-refresh)

   Store tokens and validate them on each request. When access tokens expire, use the refresh token to obtain new ones without requiring the user to authenticate again.

   **Token roles**

   * **Access token** — Short-lived token (default 5 minutes) for authenticated API requests
   * **Refresh token** — Long-lived token to obtain new access tokens
   * **ID token** — JWT containing user identity claims; required for logout

   Store tokens client-side based on your security requirements. See [Token storage security](#token-storage-security) for guidance on choosing the right storage mechanism.

   When an access token expires, request new tokens:

   ```sh
   POST /oauth/token
   Content-Type: application/x-www-form-urlencoded


   grant_type=refresh_token&
   client_id=&
   refresh_token=
   ```

   Validate access tokens by verifying:

   * Token signature using Scalekit’s public keys (JWKS endpoint)
   * `iss` matches your Scalekit environment URL
   * `aud` includes your `client_id`
   * `exp` and `iat` are valid timestamps

   Public keys for signature verification:

   ```sh
   /keys
   ```

4. ## Implement logout

   [Section titled “Implement logout”](#implement-logout)

   Clear your local session and redirect to Scalekit’s logout endpoint to invalidate the shared session.

   Your logout action must:

   * Extract the ID token before clearing local storage
   * Clear locally stored tokens from memory or storage
   * Redirect the browser to Scalekit’s logout endpoint

   ```sh
   /oidc/logout?
     id_token_hint=&
     post_logout_redirect_uri=
   ```

   Logout must be a browser redirect

   Use a browser redirect to the `/oidc/logout` endpoint, not an API call. The redirect ensures Scalekit’s session cookie is sent with the request, allowing Scalekit to identify and terminate the correct session. API calls from JavaScript do not include the session cookie.

## Handle errors

[Section titled “Handle errors”](#handle-errors)

When authentication fails, Scalekit redirects to your callback URL with error parameters instead of an authorization code:

```sh
/callback?error=access_denied&error_description=User+denied+access&state=
```

Check for errors before processing the authorization code:

* Check if the `error` parameter exists in the URL
* Log the `error` and `error_description` for debugging
* Display a user-friendly message
* Provide an option to retry login

Common error codes:

| Error             | Description                                                  |
| ----------------- | ------------------------------------------------------------ |
| `access_denied`   | User denied the authorization request                        |
| `invalid_request` | Missing or invalid parameters (e.g., invalid PKCE challenge) |
| `server_error`    | Scalekit encountered an unexpected error                     |

## Token storage security

[Section titled “Token storage security”](#token-storage-security)

SPAs run entirely in the browser where tokens are vulnerable to cross-site scripting (XSS) attacks. An attacker who successfully injects malicious JavaScript can read tokens from any accessible storage and use them to impersonate the user.

Choose a storage strategy based on your security requirements:

| Storage                      | Security                                | Trade-off                                            |
| ---------------------------- | --------------------------------------- | ---------------------------------------------------- |
| Memory (JavaScript variable) | Most secure — not accessible to XSS     | Tokens lost on page refresh; requires silent refresh |
| Session storage              | Moderate — cleared when tab closes      | Accessible to XSS; persists during session           |
| Local storage                | Least secure — persists across sessions | Accessible to XSS; long exposure window              |

**Recommendations:**

* For high-security applications, store tokens in memory and use silent refresh (iframe-based token renewal) to maintain sessions across page loads
* Always sanitize user inputs and use Content Security Policy (CSP) headers to mitigate XSS attacks
* Never log tokens or include them in error messages

Never store tokens in local storage for sensitive applications

Local storage is accessible to any JavaScript running on your page. If an attacker exploits an XSS vulnerability, they can read all tokens from local storage and fully compromise user accounts. For applications handling sensitive data, prefer memory storage with silent refresh.

## What’s next

[Section titled “What’s next”](#whats-next)

* [Set up a custom domain](/guides/custom-domain) for your authentication pages
* [Add enterprise SSO](/authenticate/auth-methods/enterprise-sso/) to support SAML and OIDC with your customers’ identity providers

---
# DOCUMENT BOUNDARY
---

# Web application

> Implement Multi-App Authentication for web apps using Authorization Code flow with client_id and client_secret

Implement login, token management, and logout in your web application using the Authorization Code flow. Web applications have a backend server that can securely store a `client_secret`, allowing them to authenticate directly with Scalekit’s token endpoint. This guide covers initiating login from your backend, exchanging authorization codes for tokens, managing sessions with secure cookies, and implementing logout.

Tip

[**Check out the example apps on GitHub**](https://github.com/scalekit-inc/multiapp-demo) to see Web, SPA, Desktop, and Mobile apps sharing a single Scalekit session.

## Prerequisites

[Section titled “Prerequisites”](#prerequisites)

Before you begin, ensure you have:

* A Scalekit account with an environment configured
* Your environment URL (`ENV_URL`), e.g., `https://yourenv.scalekit.com`
* A web application registered in Scalekit with `client_id` and `client_secret` ([Create one](/authenticate/fsa/multiapp/manage-apps))
* At least one redirect URL configured in **Dashboard > Developers > Applications > \[Your App] > Redirects**

## High-level flow

[Section titled “High-level flow”](#high-level-flow)

![Web app login flow: the frontend redirects to Scalekit and the backend exchanges the authorization code](/d2/docs/authenticate/fsa/multiapp/web-app-0.svg)

## Step-by-step implementation

[Section titled “Step-by-step implementation”](#step-by-step-implementation)

1. ## Initiate login or signup

   [Section titled “Initiate login or signup”](#initiate-login-or-signup)

   Initiate login by redirecting the user to Scalekit’s hosted login page from your backend. Generate and store a `state` parameter before redirecting to validate the callback.

   ```sh
   /oauth/authorize?
     response_type=code&
     client_id=&
     redirect_uri=&
     scope=openid+profile+email+offline_access&
     state=
   ```

   Why web apps use client_secret

   Web applications store the `client_secret` on the backend server where it cannot be accessed by browsers or end users. This allows your backend to authenticate directly with Scalekit’s token endpoint without needing PKCE. Never expose the `client_secret` to the frontend or include it in client-side JavaScript.

   For detailed parameter definitions, see [Initiate signup/login](/authenticate/fsa/implement-login).

2. ## Handle the callback and complete login

   [Section titled “Handle the callback and complete login”](#handle-the-callback-and-complete-login)

   After authentication, Scalekit redirects the user back to your callback endpoint with an authorization `code` and the `state` you sent.

   Your backend must:

   * Validate the returned `state` matches what you stored — this confirms the response is for your original request and prevents CSRF attacks
   * Handle any error parameters before processing
   * Exchange the authorization code for tokens using your `client_secret`

   ```sh
   POST /oauth/token
   Content-Type: application/x-www-form-urlencoded


   grant_type=authorization_code&
   client_id=&
   client_secret=&
   code=&
   redirect_uri=
   ```

   ```json
   {
     "access_token": "...",
     "refresh_token": "...",
     "id_token": "...",
     "expires_in": 299
   }
   ```

   Authorization codes expire after one use

   Authorization codes are single-use and expire quickly (approximately 10 minutes). If you attempt to reuse a code or it expires, start a new login flow to obtain a fresh authorization code.

3. ## Manage sessions and token refresh

   [Section titled “Manage sessions and token refresh”](#manage-sessions-and-token-refresh)

   Store tokens in secure cookies and validate the access token on each request. When access tokens expire, use the refresh token to obtain new ones without requiring the user to re-authenticate.

   **Token roles**

   * **Access token** — Short-lived token (default 5 minutes) for authenticated API requests
   * **Refresh token** — Long-lived token to obtain new access tokens
   * **ID token** — JWT containing user identity claims; required for logout

   Store tokens in secure, HttpOnly cookies with appropriate path scoping to limit exposure.

   When an access token expires, request new tokens:

   ```sh
   POST /oauth/token
   Content-Type: application/x-www-form-urlencoded


   grant_type=refresh_token&
   client_id=&
   client_secret=&
   refresh_token=
   ```

   Validate access tokens by verifying:

   * Token signature using Scalekit’s public keys (JWKS endpoint)
   * `iss` matches your Scalekit environment URL
   * `aud` includes your `client_id`
   * `exp` and `iat` are valid timestamps

   Public keys for signature verification:

   ```sh
   /keys
   ```

4. ## Implement logout

   [Section titled “Implement logout”](#implement-logout)

   Clear your application session and redirect to Scalekit’s logout endpoint to invalidate the shared session.

   Your logout endpoint must:

   * Extract the ID token before clearing cookies
   * Clear application session cookies
   * Redirect the browser to Scalekit’s logout endpoint

   ```sh
   /oidc/logout?
     id_token_hint=&
     post_logout_redirect_uri=
   ```

   Logout must be a browser redirect

   Use a browser redirect to the `/oidc/logout` endpoint, not an API call. The redirect ensures Scalekit’s session cookie is sent with the request, allowing Scalekit to identify and terminate the correct session.

   Configure [backchannel logout](/guides/dashboard/redirects/#back-channel-logout-url) URLs to receive notifications when a logout is performed from another application sharing the same user session.

## Handle errors

[Section titled “Handle errors”](#handle-errors)

When authentication fails, Scalekit redirects to your callback URL with error parameters instead of an authorization code:

```sh
/callback?error=access_denied&error_description=User+denied+access&state=
```

Check for errors before processing the authorization code:

* Check if the `error` parameter exists in the URL
* Log the `error` and `error_description` for debugging
* Display a user-friendly message
* Provide an option to retry login

Common error codes:

| Error             | Description                              |
| ----------------- | ---------------------------------------- |
| `access_denied`   | User denied the authorization request    |
| `invalid_request` | Missing or invalid parameters            |
| `server_error`    | Scalekit encountered an unexpected error |

## (Optional) Use Scalekit Management APIs

[Section titled “(Optional) Use Scalekit Management APIs”](#optional-use-scalekit-management-apis)

In addition to handling user authentication, web applications can call Scalekit’s Management APIs from the backend. These APIs allow your application to interact with Scalekit-managed resources such as users, organizations, memberships, and roles.

Typical use cases include:

* Fetching the currently authenticated user
* Listing organizations the user belongs to
* Managing organization membership or roles

Management APIs are Scalekit-owned APIs intended for server-side use only. Enable Management API access in your application:

1. Go to **app.scalekit.com**
2. Navigate to **Developers > Applications**
3. Select your **Web Application**
4. Enable **Allow Scalekit Management API Access**

Management API access is only available for web applications

This option is only available for web applications because they can securely store credentials. When enabled, your backend can authenticate to Scalekit’s Management APIs using the application’s credentials. These calls are independent of end-user access tokens and are designed for trusted, server-side workflows.

## What’s next

[Section titled “What’s next”](#whats-next)

* [Configure backchannel logout](/guides/dashboard/redirects/#back-channel-logout-url) to receive notifications when a user logs out from another app
* [Set up a custom domain](/guides/custom-domain) for your authentication pages
* [Add enterprise SSO](/authenticate/auth-methods/enterprise-sso/) to support SAML and OIDC with your customers’ identity providers

---
# DOCUMENT BOUNDARY
---

# Customize session token claims

> Add custom claims from a user profile and organization to the JWT access token Scalekit issues at login and session refresh

A session token claim adds a field from a user’s profile or their organization directly onto the JWT access token Scalekit issues at login and session refresh. Your API reads it straight off the token, instead of looking it up from your own database on every request.

Claims from your own systems

Session token claims come from data Scalekit holds: the user’s profile and their organization. To add values only your application knows, such as a subscription plan, return them from a [`PRE_SESSION_CREATION` interceptor](/authenticate/interceptors/auth-flow-interceptors/#pre-session-creation). Its claims are set at login and kept on refresh.

## How it works

[Section titled “How it works”](#how-it-works)

Every access token Scalekit issues carries a working set of default claims, resolved fresh each time a session starts or refreshes:

Default access token claims

```jsonc
{
  "sub": "usr_96194455173923084",              // Scalekit user ID
  "iss": "https://your-app.scalekit.com",       // Token issuer
  "aud": ["https://api.your-app.com"],          // Intended audience
  "exp": 1730000600,                            // Expiration time
  "iat": 1730000000,                            // Issued-at time
  "nbf": 1730000000,                            // Not valid before
  "jti": "tkn_2b91b6b8f1e94b2c",                // Unique token ID
  "sid": "ses_5f3a9c1d7e2b4a68",                // Session ID
  "oid": "org_84213988219092123",               // Organization ID
  "roles": ["admin"],                           // Roles in the organization
  "permissions": ["users:read", "users:write"]  // Permissions granted
}
```

A custom claim works the same way. You define an expression once, against `user` or `organization`, and Scalekit resolves it for every session from then on, adding the result alongside the default claims above. These claims are also listed in your Scalekit dashboard, under **Dashboard > Authentication > Token Claims**.

## Add a custom claim

[Section titled “Add a custom claim”](#add-a-custom-claim)

To add a custom claim:

1. Navigate to **Dashboard > Authentication > Token Claims** and click **Add custom claim**.

2. Configure the new claim:

   * **Claim key** - the name it appears under in the token, for example `plan`.
   * **Expression** - the dot-path to the value, evaluated against `user` or `organization`, for example `organization.metadata.plan`.

3. Click **Add**. Scalekit applies the claim immediately, there’s no separate publish step.

Reserved claim keys

`sub`, `iss`, `aud`, `exp`, `iat`, `nbf`, `jti`, `oid`, `sid`, `xoid`, `xuid`, `roles`, and `permissions` are already set by Scalekit. Pick a claim key that doesn’t collide with these, like `plan` or `a_user_email`.

![Add custom claim panel with a claim key field and an expression field.](/.netlify/images?url=_astro%2Fadd-custom-claim.CvEzhmu5.png\&w=3432\&h=1922\&dpl=6ae471fc143c9a4c27550463)

Scalekit adds the resolved value to every token going forward:

Access token with a custom claim

```diff
{
  "sub": "usr_96194455173923084",               // Scalekit user ID
  "iss": "https://your-app.scalekit.com",        // Token issuer
  "aud": ["https://api.your-app.com"],           // Intended audience
  "exp": 1730000600,                             // Expiration time
  "iat": 1730000000,                             // Issued-at time
  "nbf": 1730000000,                             // Not valid before
  "jti": "tkn_2b91b6b8f1e94b2c",                 // Unique token ID
  "sid": "ses_5f3a9c1d7e2b4a68",                 // Session ID
  "oid": "org_84213988219092123",                // Organization ID
  "roles": ["admin"],                            // Roles in the organization
  "permissions": ["users:read", "users:write"],  // Permissions granted
  "plan": "enterprise"                           // Custom claim from organization.metadata.plan
}
```

Custom claims are access token extensions you can precisely configure for your application’s authorization needs. For example, as a multi-tenant SaaS platform, you might add a claim that exposes the organization’s plan, so your API can gate a premium feature without a database lookup on every request. Use the pencil icon next to a claim to edit its expression, or the trash icon to remove it.

Keep claims non-sensitive

Anyone holding the access token can decode and read its claims, and tokens are often logged or forwarded to other services. Include only the minimal context your API needs to authorize a request, such as a plan tier or a role, and never a secret, password, or unnecessary personal detail.

## Preview before you rely on it

[Section titled “Preview before you rely on it”](#preview-before-you-rely-on-it)

The **Token preview** panel on the Token Claims page resolves every claim, default and custom, against a real session. Select a user and their organization, and the JSON view updates with the exact payload that session’s token would carry.

![Token Claims page with a user and an organization selected, showing the resolved JWT payload in the preview panel.](/.netlify/images?url=_astro%2Ftoken-claims-page.BQw4NTat.png\&w=3200\&h=1612\&dpl=6ae471fc143c9a4c27550463)

For a true end-to-end check, sign in through your OIDC flow and decode the access token you receive. A custom claim only reflects the data at the time the token was issued, so an existing token keeps its old value until the user logs in again or the token refreshes.

A claim can resolve to nothing

If an expression’s field is missing for a given user (no `metadata.department` set, for example), Scalekit leaves that claim out of the token rather than adding an empty one. A login never fails because of a claim expression; worst case, the claim just isn’t there. Preview against a few different users to catch this before you depend on the claim in production.

## User and organization fields

[Section titled “User and organization fields”](#user-and-organization-fields)

Every expression reads from one of two objects: `user` or `organization`.

**User fields:**

| Field                                                                                  | Holds                                                    |
| -------------------------------------------------------------------------------------- | -------------------------------------------------------- |
| `user.id`                                                                              | Scalekit user ID                                         |
| `user.email`, `user.email_verified`                                                    | Primary email and its verification state                 |
| `user.name`, `user.given_name`, `user.family_name`                                     | Display, first, and last name                            |
| `user.preferred_username`                                                              | Preferred username                                       |
| `user.phone_number`, `user.phone_number_verified`                                      | Phone number and its verification state                  |
| `user.picture`                                                                         | URL of the profile picture                               |
| `user.gender`                                                                          | Gender                                                   |
| `user.locale`                                                                          | Language and region preference                           |
| `user.groups`                                                                          | Groups the user belongs to                               |
| `user.external_id`                                                                     | Your system’s identifier for this user                   |
| `user.metadata`                                                                        | Key-value data you’ve attached to the user               |
| `user.custom_attributes`                                                               | Custom profile attributes                                |
| `user.created_at`, `user.updated_at`                                                   | When the user account was created and last updated       |
| `user.last_login_time`                                                                 | When the user last logged in                             |
| `user.current_membership.organization_id`, `user.current_membership.organization_name` | ID and name of the organization the user is signing into |
| `user.current_membership.roles`                                                        | Roles the user holds in that organization                |
| `user.current_membership.status`                                                       | Membership status, for example `active`                  |
| `user.current_membership.metadata`                                                     | Key-value data on the membership itself                  |

**Organization fields:**

| Field                      | Holds                                              |
| -------------------------- | -------------------------------------------------- |
| `organization.id`          | Scalekit organization ID                           |
| `organization.name`        | Organization display name                          |
| `organization.external_id` | Your system’s identifier for this organization     |
| `organization.metadata`    | Key-value data you’ve attached to the organization |

## Write the expression

[Section titled “Write the expression”](#write-the-expression)

The simplest expression is a field path, like `user.email`. Scalekit evaluates it against the signed-in user and their organization, and the result becomes the claim’s value.

Chain a fallback with `??` for when the first field might be missing, for example `user.email ?? user.preferred_username`. It only falls back on a missing or `nil` value, not an empty string.

Once a chain goes more than one level below a field that might not exist, add `?.` at that hop instead of a plain `.`. `?.` short-circuits to a missing value the moment the left side is missing, so `??` can still catch it; a plain `.` on a missing value causes a runtime error instead, which `??` can’t resolve. You only need `?.` below the uncertain field, not before it: `organization.metadata` is always present, so `organization.metadata.billing` is safe as-is, but `billing` itself might be missing, so reaching into it needs `organization.metadata.billing?.tier`.

A few more patterns:

| What you want                                            | Expression                                      |
| -------------------------------------------------------- | ----------------------------------------------- |
| A profile field                                          | `user.email`                                    |
| Something from your own metadata                         | `organization.metadata.plan`                    |
| A default when a field might be unset                    | `organization.metadata.plan ?? "free"`          |
| A default two levels below a field that might be missing | `organization.metadata.billing?.tier ?? "free"` |

A few string and array helpers are also available (`split`, `trim`, `len`, `filter`, and more) for anything more involved than a plain lookup.

Once your claims are in place, see [Manage user sessions](/authenticate/fsa/manage-session/) for how your application stores, validates, and refreshes the token they’re attached to.

---
# DOCUMENT BOUNDARY
---

# User management settings

> Configure user management settings, including user attributes and configuration options from to Scalekit dashboard.

User management settings allow you to configure how user data is handled in the environment and what attributes are available for users in your application. These settings are accessible from the **User Management** section in the Scalekit dashboard.

The Configuration tab provides several important settings that control user registration, organization limits, and branding.

![Scalekit dashboard User Management settings tab with toggles for sign-up, user limit per organization, invitation expiry, and organization meta name](/.netlify/images?url=_astro%2F2-configuration.BBcHzaot.png\&w=2786\&h=1746\&dpl=6ae471fc143c9a4c27550463)

### Sign-up for your application

[Section titled “Sign-up for your application”](#sign-up-for-your-application)

Control whether users can sign up and create new organizations. When enabled, users can register for your application and automatically create a new organization.

### Organization creation limit per user

[Section titled “Organization creation limit per user”](#organization-creation-limit-per-user)

Define the maximum number of organizations a single user can create. This helps prevent abuse and manage resource usage across your application.

### Limit user sign-ups in an organization

[Section titled “Limit user sign-ups in an organization”](#limit-user-sign-ups-in-an-organization)

Use this when you need seat caps per organization. For example, when organizations map to departments or when plans include per-org seat limits.

To set a limit from the dashboard:

![Organization User limit setting enabled with a maximum of 25 users, including active users and pending invitations](/.netlify/images?url=_astro%2Flimit-org-users.F8VX5klf.png\&w=2454\&h=618\&dpl=6ae471fc143c9a4c27550463)

1. Go to Organizations → Select an Organization → User management
2. Find Organization limits and set max users per organization. Save changes.

New users provisioning to this organizations are blocked until limits are increased. Configure them by updating the organization settings.

Note

This limit includes users in states “active” and “pending invite”. Expired invites do not count toward the limit.

### Invitation expiry

[Section titled “Invitation expiry”](#invitation-expiry)

Configure how long user invitation links remain valid. The default setting of **15 days** ensures that invitations don’t remain active indefinitely, improving security while giving invitees reasonable time to accept.

### Organization meta name

[Section titled “Organization meta name”](#organization-meta-name)

Customize what you call an “Organization” in your application. This meta name appears throughout all Scalekit-hosted pages. For example, you might call it:

* “Company” for B2B applications
* “Team” for collaboration tools
* “Workspace” for productivity apps
* “Account” for multi-tenant systems

## User attributes

[Section titled “User attributes”](#user-attributes)

The User Attributes tab allows you to define custom fields that will be available for user profiles. These attributes help you collect and store additional information about your users beyond the standard profile fields.

![Scalekit dashboard Users page listing application users with their email and active or inactive status](/.netlify/images?url=_astro%2F1-user-profile.CQCsGgPh.png\&w=2786\&h=1746\&dpl=6ae471fc143c9a4c27550463)

When you define custom user attributes, they become part of the user’s profile data that your application can access. This allows you to:

* Collect additional information during user registration
* Store application-specific user data
* Personalize user experiences based on these attributes
* Use the data for application logic and user management

---
# DOCUMENT BOUNDARY
---

# Handle webhook events in your application

> Receive real-time notifications about authentication events in your application using Scalekit webhooks

Webhooks provide real-time notifications about authentication and user management events in your Scalekit environment. Instead of polling for changes, your application receives instant notifications when users sign up, log in, join organizations, or when other important events occur.

![Webhook delivery: Scalekit queues an event, posts the payload to your endpoint, and your app verifies the signature before processing](/d2/docs/authenticate/implement-workflows/implement-webhooks-0.svg)

Webhooks enable your app to react immediately to changes in your auth stack through:

* **Real-time updates**: Get notified immediately when events occur
* **Reduced API calls**: No need to poll for changes
* **Event-driven architecture**: Build responsive workflows that react to user actions
* **Reliable delivery**: Scalekit ensures webhook delivery with automatic retries

## Webhook event object

[Section titled “Webhook event object”](#webhook-event-object)

All webhook payloads follow a standardized structure with metadata and event-specific data in the `data` field.

User created event payload

```json
{
  "spec_version": "1", // The version of the event specification format. Currently "1".
  "id": "evt_123456789", // A unique identifier for the event (e.g., evt_123456789).
  "object": "DirectoryUser", // The type of object, such as "DirectoryUser", "Directory" or "Connection". Domain events omit it.
  "environment_id": "env_123456789", // The ID of the environment where the event occurred.
  "occurred_at": "2024-08-21T10:20:17.072Z", // ISO 8601 timestamp indicating when the event occurred.
  "organization_id": "org_123456789", // The organization the event is about. Not sent on user.* events; read data.organization.id or data.user_session.organization_id instead.
  "type": "organization.directory.user_created", // The specific event type (e.g., "organization.directory.user_created").
  "data": { // Event-specific payload. Its fields depend on the event type.
    "id": "diruser_123456789",
    "organization_id": "org_123456789",
    "email": "user@example.com",
    "name": "John Doe"
    // ...more user fields
  }
}
```

## Configure webhooks in the dashboard

[Section titled “Configure webhooks in the dashboard”](#configure-webhooks-in-the-dashboard)

Set up webhook endpoints and select which events you want to receive through the Scalekit dashboard.

1. In your Scalekit dashboard, navigate to **Developers** > **Webhooks**

2. Click **Add Endpoint** and provide:

   * **Endpoint URL** - Your application’s webhook handler URL (e.g., `https://yourapp.com/webhooks/scalekit`)
   * **Description** - Optional description for this endpoint

3. Choose which events you want to receive. For example:

   * **User events**: `user.signup`, `user.login`, `user.logout`, `user.organization_invitation`, and `user.organization_membership_created`, `_updated` and `_deleted`
   * **Organization events**: `organization.created`, `organization.updated`, `organization.deleted`
   * **SSO events**: `organization.sso_created`, `organization.sso_enabled`, `organization.sso_disabled`, `organization.sso_deleted`. See the [SSO webhooks reference](/sso/reference/sso-events/)
   * **Directory events**: `organization.directory_created`, `organization.directory_enabled` and siblings, plus `organization.directory.user_*` and `organization.directory.group_*`

   See the [webhooks reference](/apis/#webhooks) for every event and its payload.



4. Copy the **Signing Secret** - you’ll use this to verify webhook authenticity in your application

5. Use the **Send Test Event** button to verify your endpoint is working correctly

Webhook response requirements

Your webhook endpoint must return a `2xx` status code within 30 seconds for a delivery to count as successful. Scalekit recommends `201`. Any other status code, or no response within 30 seconds, counts as a failure. Scalekit retries failed deliveries for about 27 hours. See the [retry schedule](#handle-error-responses).

## Implement webhook handlers

[Section titled “Implement webhook handlers”](#implement-webhook-handlers)

Create secure webhook handlers in your application to process incoming events from Scalekit.

1. ### Set up webhook endpoint

   [Section titled “Set up webhook endpoint”](#set-up-webhook-endpoint)

   Create an HTTP POST endpoint in your application to receive webhook payloads from Scalekit.

   * Node.js

     Express.js webhook handler

     ```javascript
     3 collapsed lines
     1
     import express from 'express'
     2
     import { Scalekit } from '@scalekit-sdk/node'
     3


     4
     const app = express()
     5
     const scalekit = new Scalekit(
     6
       process.env.SCALEKIT_ENVIRONMENT_URL,
     7
       process.env.SCALEKIT_CLIENT_ID,
     8
       process.env.SCALEKIT_CLIENT_SECRET
     9
     )
     10


     11
     // HMAC is over the raw bytes. Parsing JSON first changes whitespace and fails verify.
     12
     app.use('/webhooks/scalekit', express.raw({ type: 'application/json' }))
     13


     14
     app.post('/webhooks/scalekit', async (req, res) => {
     15
       const secret = process.env.SCALEKIT_WEBHOOK_SECRET
     16
       const payload = req.body.toString()
     17
       const headers = {
     18
         'webhook-id': req.headers['webhook-id'],
     19
         'webhook-timestamp': req.headers['webhook-timestamp'],
     20
         'webhook-signature': req.headers['webhook-signature'],
     21
       }
     22


     23
       try {
     24
         scalekit.verifyWebhookPayload(secret, headers, payload)
     25
       } catch (error) {
     26
         console.error('Invalid webhook signature')
     27
         return res.status(401).json({ error: 'Invalid signature' })
     28
       }
     29


     30
       const event = JSON.parse(payload)
     31
       await processWebhookEvent(event)
     32
       res.status(201).json({ received: true })
     33
     })
     ```

   * Python

     Flask webhook handler

     ```python
     5 collapsed lines
     1
     import json
     2
     import os
     3


     4
     from flask import Flask, jsonify, request
     5
     from scalekit import ScalekitClient
     6


     7
     app = Flask(__name__)
     8
     scalekit_client = ScalekitClient(
     9
         env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
     10
         client_id=os.environ["SCALEKIT_CLIENT_ID"],
     11
         client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
     12
     )
     13


     14
     @app.route("/webhooks/scalekit", methods=["POST"])
     15
     def handle_webhook():
     16
         secret = os.environ.get("SCALEKIT_WEBHOOK_SECRET")
     17
         raw_body = request.get_data()
     18
         headers = {
     19
             "webhook-id": request.headers.get("webhook-id"),
     20
             "webhook-timestamp": request.headers.get("webhook-timestamp"),
     21
             "webhook-signature": request.headers.get("webhook-signature"),
     22
         }
     23


     24
         try:
     25
             scalekit_client.verify_webhook_payload(secret, headers, raw_body)
     26
         except Exception:
     27
             print("Invalid webhook signature")
     28
             return jsonify({"error": "Invalid signature"}), 401
     29


     30
         event = json.loads(raw_body.decode("utf-8"))
     31
         process_webhook_event(event)
     32
         return jsonify({"received": True}), 201
     ```

   * Go

     Gin webhook handler

     ```go
     11 collapsed lines
     1
     package main
     2


     3
     import (
     4
         "encoding/json"
     5
         "io"
     6
         "net/http"
     7
         "os"
     8


     9
         "github.com/gin-gonic/gin"
     10
         scalekit "github.com/scalekit-inc/scalekit-sdk-go"
     11
     )
     12


     13
     var scalekitClient = scalekit.NewScalekitClient(
     14
         os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
     15
         os.Getenv("SCALEKIT_CLIENT_ID"),
     16
         os.Getenv("SCALEKIT_CLIENT_SECRET"),
     17
     )
     18


     19
     func handleWebhook(c *gin.Context) {
     20
         rawBody, err := io.ReadAll(c.Request.Body)
     21
         if err != nil {
     22
             c.JSON(http.StatusBadRequest, gin.H{"error": "Failed to read body"})
     23
             return
     24
         }
     25


     26
         headers := map[string]string{
     27
             "webhook-id":        c.GetHeader("webhook-id"),
     28
             "webhook-timestamp": c.GetHeader("webhook-timestamp"),
     29
             "webhook-signature": c.GetHeader("webhook-signature"),
     30
         }
     31


     32
         valid, err := scalekitClient.VerifyWebhookPayload(
     33
             os.Getenv("SCALEKIT_WEBHOOK_SECRET"),
     34
             headers,
     35
             rawBody,
     36
         )
     37
         if err != nil || !valid {
     38
             c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid signature"})
     39
             return
     40
         }
     41


     42
         var event map[string]interface{}
     43
         if err := json.Unmarshal(rawBody, &event); err != nil {
     44
             c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid JSON"})
     45
             return
     46
         }
     47


     48
         processWebhookEvent(event)
     49
         c.JSON(http.StatusCreated, gin.H{"received": true})
     50
     }
     ```

   * Java

     Spring webhook handler

     ```java
     8 collapsed lines
     1
     import com.fasterxml.jackson.databind.ObjectMapper;
     2
     import com.scalekit.ScalekitClient;
     3
     import java.nio.charset.StandardCharsets;
     4
     import java.util.HashMap;
     5
     import java.util.Map;
     6
     import org.springframework.http.HttpStatus;
     7
     import org.springframework.http.ResponseEntity;
     8
     import org.springframework.web.bind.annotation.PostMapping;
     9
     import org.springframework.web.bind.annotation.RequestBody;
     10
     import org.springframework.web.bind.annotation.RequestHeader;
     11
     import org.springframework.web.bind.annotation.RestController;
     12


     13
     @RestController
     14
     public class WebhookController {
     15


     16
       private final ScalekitClient scalekitClient;
     17
       private final ObjectMapper objectMapper = new ObjectMapper();
     18


     19
       @PostMapping("/webhooks/scalekit")
     20
       public ResponseEntity> handleWebhook(
     21
         @RequestBody String rawBody,
     22
         @RequestHeader Map requestHeaders
     23
       ) {
     24
         Map headers = new HashMap<>();
     25
         headers.put("webhook-id", requestHeaders.get("webhook-id"));
     26
         headers.put("webhook-timestamp", requestHeaders.get("webhook-timestamp"));
     27
         headers.put("webhook-signature", requestHeaders.get("webhook-signature"));
     28


     29
         try {
     30
           scalekitClient
     31
             .webhook()
     32
             .verifyWebhookPayload(
     33
               System.getenv("SCALEKIT_WEBHOOK_SECRET"),
     34
               headers,
     35
               rawBody.getBytes(StandardCharsets.UTF_8)
     36
             );
     37
         } catch (Exception error) {
     38
           return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(
     39
             Map.of("error", "Invalid signature")
     40
           );
     41
         }
     42


     43
         try {
     44
           Map event = objectMapper.readValue(rawBody, Map.class);
     45
           processWebhookEvent(event);
     46
           return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("received", true));
     47
         } catch (Exception error) {
     48
           return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
     49
             Map.of("error", "Webhook processing failed")
     50
           );
     51
         }
     52
       }
     53
     }
     ```

2. ### Process webhook events

   [Section titled “Process webhook events”](#process-webhook-events)

   Handle different event types based on your application’s needs.

   * Node.js

     Process webhook events

     ```javascript
     1
     async function processWebhookEvent(event) {
     2
       console.log(`Processing event: ${event.type}`);
     3


     4
       switch (event.type) {
     5
         case 'user.organization_membership_created':
     6
           // A user joined an organization. data has the user and the organization.
     7
           await handleMemberAdded(event.data.user, event.data.organization);
     8
           break;
     9


     10
         case 'user.organization_membership_deleted':
     11
           // A user left an organization
     12
           await revokeAccess(event.data.user.id, event.data.organization.id);
     13
           break;
     14


     15
         case 'organization.created':
     16
           // data is the organization
     17
           await handleOrganizationCreated(event.data);
     18
           break;
     19


     20
         default:
     21
           console.log(`Unhandled event type: ${event.type}`);
     22
       }
     23
     }
     24


     25
     async function handleMemberAdded(user, organization) {
     26
       // Use case: Sync the user to your database, send a welcome email, set up defaults
     27
       console.log(`New member: ${user.email} in org: ${organization.display_name}`);
     28


     29
       // Sync to your database
     30
       await syncUserToDatabase(user, organization);
     31


     32
       // Send a welcome email. Profile fields are in user.user_profile.
     33
       await sendWelcomeEmail(user.email, user.user_profile.given_name);
     34


     35
       // Set up the user's workspace or default settings
     36
       await setupUserDefaults(user.id, organization.id);
     37
     }
     ```

   * Python

     Process webhook events

     ```python
     1
     def process_webhook_event(event):
     2
         print(f'Processing event: {event["type"]}')
     3


     4
         event_type = event['type']
     5
         event_data = event['data']
     6


     7
         if event_type == 'user.organization_membership_created':
     8
             # A user joined an organization. data has the user and the organization.
     9
             handle_member_added(event_data['user'], event_data['organization'])
     10
         elif event_type == 'user.organization_membership_deleted':
     11
             # A user left an organization
     12
             revoke_access(event_data['user']['id'], event_data['organization']['id'])
     13
         elif event_type == 'organization.created':
     14
             # data is the organization
     15
             handle_organization_created(event_data)
     16
         else:
     17
             print(f'Unhandled event type: {event_type}')
     18


     19
     def handle_member_added(user, organization):
     20
         # Use case: Sync the user to your database, send a welcome email, set up defaults
     21
         print(f'New member: {user["email"]} in org: {organization["display_name"]}')
     22


     23
         # Sync to your database
     24
         sync_user_to_database(user, organization)
     25


     26
         # Send a welcome email. Profile fields are in user['user_profile'].
     27
         send_welcome_email(user['email'], user['user_profile']['given_name'])
     28


     29
         # Set up the user's workspace or default settings
     30
         setup_user_defaults(user['id'], organization['id'])
     ```

   * Go

     Process webhook events

     ```go
     1
     func processWebhookEvent(event map[string]interface{}) {
     2
         eventType := event["type"].(string)
     3
         eventData := event["data"].(map[string]interface{})
     4


     5
         fmt.Printf("Processing event: %s\n", eventType)
     6


     7
         switch eventType {
     8
         case "user.organization_membership_created":
     9
             // A user joined an organization. data has the user and the organization.
     10
             user := eventData["user"].(map[string]interface{})
     11
             organization := eventData["organization"].(map[string]interface{})
     12
             handleMemberAdded(user, organization)
     13


     14
         case "user.organization_membership_deleted":
     15
             // A user left an organization
     16
             user := eventData["user"].(map[string]interface{})
     17
             organization := eventData["organization"].(map[string]interface{})
     18
             revokeAccess(user["id"].(string), organization["id"].(string))
     19


     20
         case "organization.created":
     21
             // data is the organization
     22
             handleOrganizationCreated(eventData)
     23


     24
         default:
     25
             fmt.Printf("Unhandled event type: %s\n", eventType)
     26
         }
     27
     }
     28


     29
     func handleMemberAdded(user, organization map[string]interface{}) {
     30
         // Use case: Sync the user to your database, send a welcome email, set up defaults
     31
         fmt.Printf("New member: %s in org: %s\n",
     32
             user["email"], organization["display_name"])
     33


     34
         // Sync to your database
     35
         syncUserToDatabase(user, organization)
     36


     37
         // Send a welcome email. Profile fields are in user["user_profile"].
     38
         profile := user["user_profile"].(map[string]interface{})
     39
         sendWelcomeEmail(user["email"].(string), profile["given_name"].(string))
     40


     41
         // Set up the user's workspace or default settings
     42
         setupUserDefaults(user["id"].(string), organization["id"].(string))
     43
     }
     ```

   * Java

     Process webhook events

     ```java
     1
     private void processWebhookEvent(Map event) {
     2
         String eventType = (String) event.get("type");
     3
         Map eventData = (Map) event.get("data");
     4


     5
         System.out.println("Processing event: " + eventType);
     6


     7
         switch (eventType) {
     8
             case "user.organization_membership_created": {
     9
                 // A user joined an organization. data has the user and the organization.
     10
                 Map user = (Map) eventData.get("user");
     11
                 Map organization = (Map) eventData.get("organization");
     12
                 handleMemberAdded(user, organization);
     13
                 break;
     14
             }
     15


     16
             case "user.organization_membership_deleted": {
     17
                 // A user left an organization
     18
                 Map user = (Map) eventData.get("user");
     19
                 Map organization = (Map) eventData.get("organization");
     20
                 revokeAccess((String) user.get("id"), (String) organization.get("id"));
     21
                 break;
     22
             }
     23


     24
             case "organization.created":
     25
                 // data is the organization
     26
                 handleOrganizationCreated(eventData);
     27
                 break;
     28


     29
             default:
     30
                 System.out.println("Unhandled event type: " + eventType);
     31
         }
     32
     }
     33


     34
     private void handleMemberAdded(Map user, Map organization) {
     35
         // Use case: Sync the user to your database, send a welcome email, set up defaults
     36
         System.out.println("New member: " + user.get("email") +
     37
             " in org: " + organization.get("display_name"));
     38


     39
         // Sync to your database
     40
         syncUserToDatabase(user, organization);
     41


     42
         // Send a welcome email. Profile fields are in user_profile.
     43
         Map profile = (Map) user.get("user_profile");
     44
         sendWelcomeEmail((String) user.get("email"), (String) profile.get("given_name"));
     45


     46
         // Set up the user's workspace or default settings
     47
         setupUserDefaults((String) user.get("id"), (String) organization.get("id"));
     48
     }
     ```

3. ### Verify with the three webhook headers

   [Section titled “Verify with the three webhook headers”](#verify-with-the-three-webhook-headers)

   Call the SDK verify method **before** you parse JSON. Pass the dashboard signing secret (`whsec_…`), the raw body, and these headers:

   | Header              | Role                                                                               |
   | ------------------- | ---------------------------------------------------------------------------------- |
   | `webhook-id`        | Message ID in the HMAC. Same value on every retry                                  |
   | `webhook-timestamp` | Unix seconds. The SDK rejects timestamps more than 5 minutes in the past or future |
   | `webhook-signature` | Space-separated `v1,` values                                               |

   | Language | Method                                                                    |
   | -------- | ------------------------------------------------------------------------- |
   | Node.js  | `scalekit.verifyWebhookPayload(secret, headers, payload)`                 |
   | Python   | `scalekit_client.verify_webhook_payload(secret, headers, payload)`        |
   | Go       | `scalekitClient.VerifyWebhookPayload(secret, headers, payload)`           |
   | Java     | `scalekitClient.webhook().verifyWebhookPayload(secret, headers, payload)` |

   Node, Python, and Java throw when the signature is invalid. Go returns `(false, error)`.

   There is no `webhooks.verifySignature` method and no `scalekit-signature` header.

   Verify before you trust the payload

   A forged POST can create or delete users in your app. Verify first. Sign the exact bytes Scalekit posted. `JSON.parse` / `request.get_json()` / `req.json()` change whitespace and fail the HMAC. Use `express.raw`, `request.get_data()`, or `io.ReadAll`.

   Verify before you queue

   If a queue or background worker processes your webhooks, verify the signature in the HTTP handler that receives the request. Then return a `2xx` response and put the verified event on your queue. Don’t verify in the worker: once an event waits in the queue for more than 5 minutes, the SDK rejects it with `Message timestamp too old`. The 5-minute window can’t be changed.

   Store the `webhook-id` with the queued event. Retries deliver the same event again with the same ID, so skip IDs your worker has already processed. The top-level `id` in the payload (`evt_…`) also works for this. It’s a different value from `webhook-id`. Don’t use `data.id`: it identifies the user or organization the event is about, not the event.

## Respond to webhook event

[Section titled “Respond to webhook event”](#respond-to-webhook-event)

Scalekit expects specific HTTP status codes in response to webhook deliveries. Return appropriate status codes to control retry behavior.

1. ### Return success responses

   [Section titled “Return success responses”](#return-success-responses)

   Return success status codes when webhooks are processed successfully.

   | Status Code               | Description                                  |
   | ------------------------- | -------------------------------------------- |
   | `200 OK`                  | Webhook processed successfully               |
   | `201 Created` Recommended | Webhook processed and resource created       |
   | `202 Accepted`            | Webhook accepted for asynchronous processing |

2. ### Handle error responses

   [Section titled “Handle error responses”](#handle-error-responses)

   Return error status codes to indicate processing failures.

   | Status Code                 | Description                          |
   | --------------------------- | ------------------------------------ |
   | `400 Bad Request`           | Invalid payload or malformed request |
   | `401 Unauthorized`          | Invalid webhook signature            |
   | `403 Forbidden`             | Webhook not authorized               |
   | `422 Unprocessable Entity`  | Valid request but cannot process     |
   | `500 Internal Server Error` | Server error during processing       |

   Retry schedule

   Scalekit retries every failed delivery with exponential backoff. Any status code outside `2xx` triggers a retry, including `4xx` codes. Each attempt waits up to 30 seconds for a response.

   | Attempt | Delay after the previous attempt |
   | ------- | -------------------------------- |
   | 1       | Immediately                      |
   | 2       | 5 seconds                        |
   | 3       | 5 minutes                        |
   | 4       | 30 minutes                       |
   | 5       | 2 hours                          |
   | 6       | 5 hours                          |
   | 7       | 10 hours                         |
   | 8       | 10 hours                         |

   After the eighth attempt fails, the delivery is marked as failed. That happens about 27 hours and 35 minutes after the first attempt. If an endpoint fails every delivery for 5 days, Scalekit disables it.

   Webhook failures are logged in your Scalekit dashboard for monitoring and debugging.

## Testing webhooks

[Section titled “Testing webhooks”](#testing-webhooks)

Test your webhook implementation locally before deploying to production.

Use **ngrok** to expose your local development server for webhook testing.

Set up local webhook testing

```bash
# Install ngrok
npm install -g ngrok


# Start your local server
npm run dev


# In another terminal, expose your local server
ngrok http 3000


# Use the ngrok URL in your Scalekit dashboard
# Example: https://abc123.ngrok.io/webhooks/scalekit
```

## Common webhook use cases

[Section titled “Common webhook use cases”](#common-webhook-use-cases)

Webhooks enable common integration patterns:

* **User lifecycle management**: Sync user data across systems, provision accounts in downstream services, and trigger onboarding workflows when users sign up or update their profiles
* **Organization and membership management**: Set up workspaces when organizations are created, update user access when they join or leave organizations, and provision organization-specific resources
* **Authentication monitoring**: Track login patterns, update last-seen timestamps, and trigger security alerts for suspicious activity

## Webhook event reference

[Section titled “Webhook event reference”](#webhook-event-reference)

You now have a complete webhook implementation that can reliably process authentication events from Scalekit. Consider these additional improvements:

[Organization events](/apis/#webhook/organizationcreated)

[User events](/apis/#webhook/usersignup)

[Directory events](/apis/#webhook/organizationdirectoryenabled)

[SSO connection events](/apis/#webhook/organizationssocreated)

[Role events](/apis/#webhook/rolecreated)

[Permission events](/apis/#webhook/permissiondeleted)

---
# DOCUMENT BOUNDARY
---

# Intercept authentication flows

> Run your own checks during sign-up, login, invitations and token issuance, and allow, deny, choose the organization or add claims

Interceptors let your app take part in Scalekit’s authentication flows. When a user signs up or logs in, when someone is invited, or when a service requests a machine-to-machine token, Scalekit calls an endpoint in your app and waits for its decision. Your endpoint can allow or deny the step and, at some trigger points, choose the user’s organization or add claims to their tokens.

A sales CRM, for example, uses interceptors to log a consultant straight in to the customer workspace they opened, and to add a new sales rep to their company’s existing workspace instead of creating a duplicate.

This page covers the [points you can intercept](#choose-a-trigger-point), how to [build](#build-the-endpoint) and [register](#register-and-test-the-interceptor) the endpoint, and what [each trigger point](#trigger-points) sends and accepts.

## Choose a trigger point

[Section titled “Choose a trigger point”](#choose-a-trigger-point)

A trigger point is a step in a Scalekit flow where your interceptor runs, and each interceptor you register listens on one trigger point. Trigger points labeled Authentication flow run while a user signs up or logs in. The names match the ones the dashboard shows.

| Trigger point                                                             | When it runs                                                                                             | Your response can                                                     |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| [Post-user authentication](#post-user-authentication) Authentication flow | After the user proves their identity, before Scalekit provisions them or shows the organization switcher | Deny the login, or choose the organization the user logs in to        |
| [Pre-signup](#pre-signup) Authentication flow                             | When a user signs up and would create a new organization                                                 | Deny the sign-up, or add the user to an existing organization instead |
| [Pre-session creation](#pre-session-creation) Authentication flow         | At the end of every login, before session tokens are issued                                              | Deny the login, or add custom claims to the access token              |
| [Pre-user invitation](#pre-user-invitation)                               | Before a user is added to an organization from the dashboard or the API                                  | Deny the invitation                                                   |
| [Pre-M2M token creation](#pre-m2m-token-creation)                         | Before a machine-to-machine access token is issued                                                       | Deny the token, or add custom claims to it                            |

The authentication flow trigger points run when users sign in on Scalekit’s hosted login page in an environment with Full Stack Auth, whatever the method: magic link or OTP, passkey, social login or enterprise SSO. They also run for MCP servers whose users sign in on that page. They do not run for Modular SSO integrations, or for MCP servers that send users to your own login service ([custom auth](/mcp/auth-methods/custom-auth/)), because Scalekit does not run its login steps there.

During one login they run in this order:

![During a login, Scalekit calls your endpoint at up to three points and waits on each answer](/d2/docs/authenticate/interceptors/auth-flow-interceptors-0.svg)

## Build the endpoint

[Section titled “Build the endpoint”](#build-the-endpoint)

Whichever trigger point you pick, your endpoint follows the same contract. Scalekit sends a JSON body and three headers: `interceptor-id`, `interceptor-timestamp` and `interceptor-signature`. Your endpoint verifies the signature, makes its decision and replies with HTTP 200 and a JSON body.

The request body has the same four top-level fields at every trigger point:

| Field                 | What it holds                                                                                                             |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `display_name`        | The name you gave the interceptor                                                                                         |
| `trigger_point`       | The trigger point, for example `POST_USER_AUTHENTICATION`                                                                 |
| `interceptor_context` | Who and where: environment, user ID and email, organization, connection, IP address, device and the authorization request |
| `data`                | The objects the decision is about: the user and their memberships, the organization, or the M2M token claims              |

Each trigger point below shows its full request.

### Verify the request

[Section titled “Verify the request”](#verify-the-request)

The signature covers the raw request body, so verify before you parse the JSON. Copy the signing secret from the **Interceptors** page in the dashboard and keep it in an environment variable.

* Node.js

  Express.js

  ```javascript
  1
  import express from 'express';
  2
  import { ScalekitClient } from '@scalekit-sdk/node';
  3


  4
  const app = express();
  5
  const scalekit = new ScalekitClient(
  6
    process.env.SCALEKIT_ENVIRONMENT_URL,
  7
    process.env.SCALEKIT_CLIENT_ID,
  8
    process.env.SCALEKIT_CLIENT_SECRET
  9
  );
  10


  11
  // Verify every interceptor route against the raw body, then hand handlers the parsed JSON
  12
  function verifyInterceptor(req, res, next) {
  13
    const rawBody = req.body.toString('utf8');
  14
    try {
  15
      scalekit.verifyInterceptorPayload(process.env.SCALEKIT_INTERCEPTOR_SECRET, req.headers, rawBody);
  16
    } catch (error) {
  17
      console.error('Interceptor verification failed:', error);
  18
      return res.json({ decision: 'DENY', error: { message: 'Unable to process request.' } });
  19
    }
  20
    req.body = JSON.parse(rawBody);
  21
    next();
  22
  }
  23


  24
  app.use('/auth/interceptors', express.raw({ type: 'application/json' }), verifyInterceptor);
  ```

* Python

  Flask

  ```python
  1
  import os
  2
  from flask import Flask, request
  3
  from scalekit import ScalekitClient
  4


  5
  app = Flask(__name__)
  6
  scalekit_client = ScalekitClient(
  7
      env_url=os.getenv('SCALEKIT_ENVIRONMENT_URL'),
  8
      client_id=os.getenv('SCALEKIT_CLIENT_ID'),
  9
      client_secret=os.getenv('SCALEKIT_CLIENT_SECRET'),
  10
  )
  11


  12
  def verified_body():
  13
      """Returns the parsed body, or None when the signature is invalid."""
  14
      headers = {
  15
          'interceptor-id': request.headers.get('interceptor-id'),
  16
          'interceptor-signature': request.headers.get('interceptor-signature'),
  17
          'interceptor-timestamp': request.headers.get('interceptor-timestamp'),
  18
      }
  19
      try:
  20
          if not scalekit_client.verify_interceptor_payload(
  21
              secret=os.getenv('SCALEKIT_INTERCEPTOR_SECRET'),
  22
              headers=headers,
  23
              payload=request.get_data(),
  24
          ):
  25
              return None
  26
      except Exception as error:
  27
          print(f'Interceptor verification failed: {error}')
  28
          return None
  29
      return request.get_json()
  30


  31
  DENY_UNVERIFIED = {'decision': 'DENY', 'error': {'message': 'Unable to process request.'}}
  ```

* Go

  Gin

  ```go
  1
  package main
  2


  3
  import (
  4
      "io"
  5
      "log"
  6
      "os"
  7


  8
      "github.com/gin-gonic/gin"
  9
      "github.com/scalekit-inc/scalekit-sdk-go/v2"
  10
  )
  11


  12
  var scalekitClient = scalekit.NewScalekitClient(
  13
      os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
  14
      os.Getenv("SCALEKIT_CLIENT_ID"),
  15
      os.Getenv("SCALEKIT_CLIENT_SECRET"),
  16
  )
  17


  18
  // verifiedBody returns the raw body once the signature checks out
  19
  func verifiedBody(c *gin.Context) ([]byte, bool) {
  20
      body, err := io.ReadAll(c.Request.Body)
  21
      if err != nil {
  22
          return nil, false
  23
      }
  24
      headers := map[string]string{
  25
          "interceptor-id":        c.GetHeader("interceptor-id"),
  26
          "interceptor-signature": c.GetHeader("interceptor-signature"),
  27
          "interceptor-timestamp": c.GetHeader("interceptor-timestamp"),
  28
      }
  29
      if _, err := scalekitClient.VerifyInterceptorPayload(os.Getenv("SCALEKIT_INTERCEPTOR_SECRET"), headers, body); err != nil {
  30
          log.Printf("Interceptor verification failed: %v", err)
  31
          return nil, false
  32
      }
  33
      return body, true
  34
  }
  ```

* Java

  Spring Boot

  ```java
  1
  import com.scalekit.webhooks.ScalekitInterceptor;
  2
  import java.nio.charset.StandardCharsets;
  3
  import java.util.Map;
  4


  5
  @RestController
  6
  @RequestMapping("/auth/interceptors")
  7
  public class InterceptorController {
  8


  9
      private final ScalekitInterceptor interceptor = new ScalekitInterceptor();
  10


  11
      // Returns true once the signature over the raw body checks out
  12
      private boolean isVerified(String body, Map headers) {
  13
          try {
  14
              return interceptor.verifyInterceptorPayload(
  15
                  System.getenv("SCALEKIT_INTERCEPTOR_SECRET"),
  16
                  headers,
  17
                  body.getBytes(StandardCharsets.UTF_8));
  18
          } catch (Exception error) {
  19
              System.err.println("Interceptor verification failed: " + error.getMessage());
  20
              return false;
  21
          }
  22
      }
  23
  }
  ```

### Send the decision

[Section titled “Send the decision”](#send-the-decision)

| Field           | Required | What it does                                                                                                                   |
| --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `decision`      | Yes      | `ALLOW` continues the flow. `DENY` stops it.                                                                                   |
| `error.message` | No       | With `DENY`, the message the user sees. Without it, Scalekit shows a [generic message](#when-your-endpoint-fails).             |
| `response`      | No       | With `ALLOW`, the change the trigger point allows: the organization, or the claims. Each trigger point below lists its fields. |

Reply with HTTP 200 even when you deny. Scalekit treats any other status, or no reply within the timeout, as a [failure](#when-your-endpoint-fails), not as a decision.

## Register and test the interceptor

[Section titled “Register and test the interceptor”](#register-and-test-the-interceptor)

Once the endpoint runs, register it in the dashboard and send it a test request.

1. In the Scalekit dashboard, go to **Interceptors** and add an interceptor. Enter a name, pick the trigger point, and enter your endpoint’s HTTPS URL.

   ![Interceptors settings in the Scalekit dashboard](/.netlify/images?url=_astro%2Fadd-interceptor-page.UBrvBruB.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

2. Set the timeout, from 1 to 30 seconds (5 by default), and the failure policy: **Fail open** continues the flow when your endpoint fails, **Fail closed** stops it.

3. Click **Create**, then turn on **Enable**.

4. Open the **Test** tab and click **Send request**. Scalekit sends a sample request for the trigger point to your endpoint and shows your response next to it.

   ![Interceptor test tab example](/.netlify/images?url=_astro%2Ftest-example.xdLJLh_5.png\&w=2970\&h=1643\&dpl=6ae471fc143c9a4c27550463)

5. Open the logs to see each request Scalekit sent and the response or error it got back. Requests from the Test tab appear there too.

   ![Interceptor logs in the dashboard](/.netlify/images?url=_astro%2Flogging.DSZdvTsn.png\&w=3024\&h=1705\&dpl=6ae471fc143c9a4c27550463)

Test without deploying

To try a trigger point before you build the endpoint, point the interceptor at a request bin such as [Beeceptor](https://beeceptor.com/) and set the response it returns.

## When your endpoint fails

[Section titled “When your endpoint fails”](#when-your-endpoint-fails)

Your endpoint fails when it returns a status other than 2xx, or does not reply within the timeout. The failure policy decides what happens next:

| Policy      | What Scalekit does                                                                                 |
| ----------- | -------------------------------------------------------------------------------------------------- |
| Fail open   | Continues as if you had returned `ALLOW` with no `response`. The Test tab still shows the failure. |
| Fail closed | Stops the flow and shows the generic message.                                                      |

The generic message is “The request could not be completed due to a policy restriction. Please contact support for assistance.” When the environment has a support email, the message names it: “Please contact  for assistance.” Scalekit also shows it when you deny without an `error.message`.

## Trigger points

[Section titled “Trigger points”](#trigger-points)

Each trigger point below covers when it runs, what Scalekit sends, what your endpoint can return, and an example.

### Post-user authentication

[Section titled “Post-user authentication”](#post-user-authentication)

Authentication flow

Post-user authentication runs once per login, after the user proves their identity and before Scalekit provisions them or shows the organization switcher. It runs for new users too: the request then has no `user_id` and no `data.user`.

Use it to block a login, or to choose the organization the user logs in to. The fields that drive that choice:

| Field                                    | What it holds                                                                                                                                                   |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `interceptor_context.auth_request`       | Parameters from the authorization URL: `client_id`, plus `organization_id`, `connection_id`, `domain` and `login_hint` when your app or the login page set them |
| `data.user.memberships`                  | Each organization the user belongs to, with its `status` and `roles`                                                                                            |
| `interceptor_context.connection_details` | The method the user signed in with                                                                                                                              |

Request body

```json
1
{
2
  "display_name": "Route users to their workspace",
3
  "trigger_point": "POST_USER_AUTHENTICATION",
4
  "interceptor_context": {
5
    "environment_id": "env_92561807201272162",
6
    "user_id": "usr_93418238346728951", // Only when the user already exists
7
    "user_email": "john.doe@example.com",
8
    "auth_request": {
9
      "client_id": "skc_92561807201272100",
10
      "organization_id": "org_93418204671239865", // Only when your app passed it
11
      "connection_id": "conn_92561808744978132",
12
      "login_hint": "john.doe@example.com",
13
      "state": "xsrPHl7k7ARgdhC6"
14
    },
9 collapsed lines
15
    "connection_details": [
16
      { "id": "conn_92561808744978132", "type": "PASSWORDLESS", "provider": "SCALEKIT" }
17
    ],
18
    "device_type": "Desktop",
19
    "ip_address": "203.0.113.24",
20
    "region": "US",
21
    "city": "San Francisco",
22
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
23
    "triggered_at": "2026-10-13T09:48:02.875Z"
24
  },
25
  "data": {
26
    "user": { // Only when the user already exists
27
      "id": "usr_93418238346728951",
28
      "email": "john.doe@example.com",
29
      "memberships": [
30
        { "organization_id": "org_93418204671239864", "organization_name": "Northwind", "status": "ACTIVE", "roles": ["admin"] },
31
        { "organization_id": "org_93418204671239865", "organization_name": "Globex", "status": "ACTIVE", "roles": ["member"] }
32
      ],
6 collapsed lines
33
      "name": "John Doe",
34
      "given_name": "John",
35
      "family_name": "Doe",
36
      "email_verified": true,
37
      "created_at": "2025-10-06T11:06:49.120Z",
38
      "updated_at": "2025-10-06T13:33:06.479Z"
39
    }
40
  }
41
}
```

#### Choose the organization

[Section titled “Choose the organization”](#choose-the-organization)

Return `ALLOW` with no `response` to continue as usual: a user with several organizations sees the organization switcher. To choose the organization yourself, send one of these two fields. Either one skips the switcher.

| Field                                  | What Scalekit does                                                                                                                                                                                      |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `select_organization`                  | Logs the user in to an organization they already belong to, including one whose invitation is pending or has expired.                                                                                   |
| `create_organization_membership`       | Adds the user to the organization, then logs them in. Creates the user first if they are new, uses an active or pending membership if one exists, and re-activates an inactive, deleted or expired one. |
| `organization_id`                      | Scalekit’s ID for the organization, such as `org_93418204671239865`. Send this or `external_organization_id` in either object. If you send both, Scalekit uses `organization_id`.                       |
| `external_organization_id`             | Your own ID for the organization, the external ID you set on it in Scalekit. Scalekit uses it only when `organization_id` is absent.                                                                    |
| `create_organization_membership.roles` | Optional role names for a new membership. An existing or re-activated membership keeps its roles, and names that match no role are ignored.                                                             |

Each response this trigger point accepts:

* Continue

  Response: continue as usual

  ```json
  {
    "decision": "ALLOW"
  }
  ```

* Select organization

  Response: log the user in to Globex

  ```json
  {
    "decision": "ALLOW",
    "response": {
      "select_organization": {
        "organization_id": "org_93418204671239865", // Scalekit's organization ID
        "external_organization_id": "your_external_org_id" // Or your own ID for the organization; used only when organization_id is absent
      }
    }
  }
  ```

* Create membership

  Response: add the user to Globex, then log them in

  ```json
  {
    "decision": "ALLOW",
    "response": {
      "create_organization_membership": {
        "organization_id": "org_93418204671239865", // Scalekit's organization ID
        "external_organization_id": "your_external_org_id", // Or your own ID for the organization; used only when organization_id is absent
        "roles": ["member"] // Optional: roles for a new membership; an existing one keeps its roles
      }
    }
  }
  ```

* Deny

  Response: block the login

  ```json
  {
    "decision": "DENY",
    "error": { "message": "Your account is suspended. Contact your administrator." }
  }
  ```

`create_organization_membership` skips the organization’s allowed email domains, so your endpoint alone decides who joins. If the chosen organization requires SSO, Scalekit sends the user to its identity provider before finishing the login.

#### A worked example

[Section titled “A worked example”](#a-worked-example)

John belongs to Northwind and Globex. He opens `globex.yourcrm.com`, and your app starts the login with Globex’s organization ID in the authorization URL (`organization_id=org_93418204671239865`).

1. John signs in with a magic link.
2. Scalekit calls your endpoint. `auth_request.organization_id` is Globex, and `data.user.memberships` has an active Globex membership.
3. Your endpoint returns `select_organization` with Globex.
4. Scalekit skips the switcher, runs Pre-session creation, and returns John to the CRM logged in to Globex.

If John had no active Globex membership, the endpoint below returns plain `ALLOW` and John picks from the switcher.

* Node.js

  Express.js

  ```javascript
  1
  app.post('/auth/interceptors/post-user-authentication', (req, res) => {
  2
    const { interceptor_context, data } = req.body;
  3
    const workspaceOrgId = interceptor_context.auth_request?.organization_id;
  4
    const memberships = data.user?.memberships ?? [];
  5


  6
    const isActiveMember = memberships.some(
  7
      (m) => m.organization_id === workspaceOrgId && m.status === 'ACTIVE'
  8
    );
  9
    if (!workspaceOrgId || !isActiveMember) {
  10
      return res.json({ decision: 'ALLOW' }); // The user picks an organization
  11
    }
  12


  13
    return res.json({
  14
      decision: 'ALLOW',
  15
      response: { select_organization: { organization_id: workspaceOrgId } }
  16
    });
  17
  });
  ```

* Python

  Flask

  ```python
  1
  @app.post('/auth/interceptors/post-user-authentication')
  2
  def post_user_authentication():
  3
      body = verified_body()
  4
      if body is None:
  5
          return DENY_UNVERIFIED
  6


  7
      workspace_org_id = (body['interceptor_context'].get('auth_request') or {}).get('organization_id')
  8
      memberships = (body['data'].get('user') or {}).get('memberships', [])
  9


  10
      is_active_member = any(
  11
          m['organization_id'] == workspace_org_id and m['status'] == 'ACTIVE' for m in memberships
  12
      )
  13
      if not workspace_org_id or not is_active_member:
  14
          return {'decision': 'ALLOW'}  # The user picks an organization
  15


  16
      return {
  17
          'decision': 'ALLOW',
  18
          'response': {'select_organization': {'organization_id': workspace_org_id}},
  19
      }
  ```

Don't grant access from auth_request alone

The user can edit the authorization URL, so `auth_request` is not proof of anything. `select_organization` is safe with it because Scalekit only logs users in to organizations they already belong to. Send `create_organization_membership` only when your own records say the user should join, or anyone could join an organization by changing the URL.

#### Errors the user sees

[Section titled “Errors the user sees”](#errors-the-user-sees)

The login stops with an error in these cases:

| Cause                                                                      | Error                                                                     |
| -------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `decision` is `DENY`                                                       | `user_auth_not_allowed`, with your `error.message` or the generic message |
| Both organization fields in one response, or one with neither ID           | `user_auth_failed`                                                        |
| `external_organization_id` matches no organization in the environment      | `invalid_organization_id`                                                 |
| `select_organization` names an organization the user doesn’t belong to     | `membership_not_found`                                                    |
| `select_organization` names an inactive or deleted membership              | `access_denied`                                                           |
| `create_organization_membership` targets an organization at its user limit | `access_denied`                                                           |

### Pre-signup

[Section titled “Pre-signup”](#pre-signup)

Authentication flow

Pre-signup runs when a user signs up and would create a new organization. It does not run when the user joins an existing organization, for example through Post-user authentication or an invitation.

Use it to block sign-ups, or to add the user to an existing organization instead. A first-time user arrives with `data` empty (`{}`). A user who already belongs to another organization arrives with `user_id` and `data.user`, as in this request:

Request body

```json
1
{
2
  "display_name": "Add new users to their company's workspace",
3
  "trigger_point": "PRE_SIGNUP",
4
  "interceptor_context": {
5
    "environment_id": "env_92561807201272162",
6
    "user_id": "usr_93418238346728951", // Only when the user already exists
7
    "user_email": "jane.smith@initech.com",
9 collapsed lines
8
    "auth_request": {
9
      "client_id": "skc_92561807201272100",
10
      "connection_id": "conn_81665025441299343",
11
      "login_hint": "jane.smith@initech.com",
12
      "state": "xsrPHl7k7ARgdhC6"
13
    },
14
    "connection_details": [
15
      { "id": "conn_92561808744978132", "type": "OAUTH", "provider": "GOOGLE" }
16
    ],
17
    "device_type": "Desktop",
18
    "ip_address": "203.0.113.24",
19
    "region": "US",
20
    "city": "San Francisco",
21
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
22
    "triggered_at": "2026-10-13T09:48:02.875Z"
23
  },
24
  "data": {
25
    "user": { // Only when the user already exists
26
      "id": "usr_93418238346728951",
27
      "email": "jane.smith@initech.com",
28
      "memberships": [
29
        { "organization_id": "org_93418204671239864", "organization_name": "Northwind", "status": "ACTIVE", "roles": ["member"] }
30
      ],
6 collapsed lines
31
      "name": "Jane Smith",
32
      "given_name": "Jane",
33
      "family_name": "Smith",
34
      "email_verified": true,
35
      "created_at": "2025-10-06T11:06:49.120Z",
36
      "updated_at": "2025-10-06T13:33:06.479Z"
37
    }
38
  }
39
}
```

Return `ALLOW` with no `response` to let the user create a new organization. To block the sign-up, return `DENY`. The user sees `signup_not_allowed` with your `error.message`. To add the user to an existing organization, return `create_organization_membership` with `organization_id` or `external_organization_id` (`organization_id` wins if you send both) and optional `roles`.

Each response this trigger point accepts:

* Continue

  Response: create a new organization

  ```json
  {
    "decision": "ALLOW"
  }
  ```

* Create membership

  Response: add the user to Initech

  ```json
  {
    "decision": "ALLOW",
    "response": {
      "create_organization_membership": {
        "organization_id": "org_93731871904672153", // Scalekit's organization ID
        "external_organization_id": "your_external_org_id", // Or your own ID for the organization; used only when organization_id is absent
        "roles": ["member"] // Optional: roles for the new membership
      }
    }
  }
  ```

* Deny

  Response: block the sign-up

  ```json
  {
    "decision": "DENY",
    "error": { "message": "Sign up with your work email address." }
  }
  ```

#### A worked example

[Section titled “A worked example”](#a-worked-example-1)

Jane joins Initech’s sales team and signs up with her `@initech.com` address. Initech already has a workspace, so your endpoint maps the email domain to Initech’s external ID and adds her there. An address from any other domain gets plain `ALLOW`, and the user creates a new workspace as usual.

* Node.js

  Express.js

  ```javascript
  1
  // Email domain → your external ID for the organization
  2
  const domainToOrg = {
  3
    'northwind.com': 'northwind',
  4
    'globex.com': 'globex',
  5
    'initech.com': 'initech'
  6
  };
  7


  8
  app.post('/auth/interceptors/pre-signup', (req, res) => {
  9
    const email = req.body.interceptor_context.user_email;
  10
    const externalOrgId = domainToOrg[email.split('@')[1]?.toLowerCase()];
  11


  12
    if (!externalOrgId) {
  13
      return res.json({ decision: 'ALLOW' }); // Creates a new organization
  14
    }
  15
    return res.json({
  16
      decision: 'ALLOW',
  17
      response: {
  18
        create_organization_membership: { external_organization_id: externalOrgId, roles: ['member'] }
  19
      }
  20
    });
  21
  });
  ```

* Python

  Flask

  ```python
  1
  # Email domain → your external ID for the organization
  2
  DOMAIN_TO_ORG = {
  3
      'northwind.com': 'northwind',
  4
      'globex.com': 'globex',
  5
      'initech.com': 'initech',
  6
  }
  7


  8
  @app.post('/auth/interceptors/pre-signup')
  9
  def pre_signup():
  10
      body = verified_body()
  11
      if body is None:
  12
          return DENY_UNVERIFIED
  13


  14
      email = body['interceptor_context']['user_email']
  15
      external_org_id = DOMAIN_TO_ORG.get(email.split('@')[-1].lower())
  16


  17
      if not external_org_id:
  18
          return {'decision': 'ALLOW'}  # Creates a new organization
  19
      return {
  20
          'decision': 'ALLOW',
  21
          'response': {
  22
              'create_organization_membership': {
  23
                  'external_organization_id': external_org_id,
  24
                  'roles': ['member'],
  25
              }
  26
          },
  27
      }
  ```

The request also carries `ip_address` and `region`, so the same endpoint can deny sign-ups from addresses or countries you block.

### Pre-session creation

[Section titled “Pre-session creation”](#pre-session-creation)

Authentication flow

Pre-session creation runs at the end of every login, before Scalekit issues the session tokens, however the user signed in. That includes an invitee’s first login from the [invitation email](/authenticate/manage-organizations/add-users-to-organization/) and a switch to another organization. By now Scalekit knows the organization, so the request carries `organization_id`, and `data.user.current_membership` when Scalekit finds the user’s membership in it. Check that the field is present before you read its roles.

Use it to deny a login, or to add claims to the access token. Scalekit puts the claims you return under `custom_claims`, saves them on the session, and keeps them on every refreshed access token. A change in your systems therefore shows up at the user’s next login. To compute claims each time a token is issued, refresh included, use [session token claims](/authenticate/fsa/session-token-claims/) instead.

Request body

```json
1
{
2
  "display_name": "Add plan claims",
3
  "trigger_point": "PRE_SESSION_CREATION",
4
  "interceptor_context": {
5
    "environment_id": "env_92561807204567213",
6
    "user_id": "usr_93418238346728951",
7
    "user_email": "john.doe@example.com",
8
    "organization_id": "org_93418204671239865", // The organization the user is logging in to
12 collapsed lines
9
    "auth_request": {
10
      "client_id": "skc_92561807201272100",
11
      "connection_id": "conn_92561808744978132",
12
      "organization_id": "org_93418204671239865",
13
      "state": "xsrPHl7k7ARgdhC6"
14
    },
15
    "connection_details": [
16
      { "id": "conn_92561808744978132", "type": "OAUTH", "provider": "GOOGLE" }
17
    ],
18
    "device_type": "Desktop",
19
    "ip_address": "203.0.113.24",
20
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
21
    "triggered_at": "2026-10-13T15:22:42.381Z"
22
  },
23
  "data": {
24
    "user": {
25
      "id": "usr_93418238346728951",
26
      "current_membership": { // When Scalekit finds the membership for organization_id
27
        "organization_id": "org_93418204671239865", "organization_name": "Globex", "status": "ACTIVE", "roles": ["member"] },
28
      "memberships": [
29
        { "organization_id": "org_93418204671239864", "organization_name": "Northwind", "status": "ACTIVE", "roles": ["admin"] },
30
        { "organization_id": "org_93418204671239865", "organization_name": "Globex", "status": "ACTIVE", "roles": ["member"] }
31
      ],
7 collapsed lines
32
      "email": "john.doe@example.com",
33
      "name": "John Doe",
34
      "given_name": "John",
35
      "family_name": "Doe",
36
      "email_verified": true,
37
      "created_at": "2025-10-06T11:06:49.120Z",
38
      "updated_at": "2025-10-06T13:33:06.479Z"
39
    }
40
  }
41
}
```

Return `ALLOW` with no `response` to issue the tokens unchanged. To deny the login, return `DENY`. The user sees `user_auth_not_allowed` with your `error.message`. To add claims, return them in `response.claims`.

Each response this trigger point accepts:

* Continue

  Response: issue the tokens unchanged

  ```json
  {
    "decision": "ALLOW"
  }
  ```

* Add claims

  Response: add plan claims

  ```json
  {
    "decision": "ALLOW",
    "response": {
      "claims": { "plan": "pro", "features": ["forecasting", "api_access"] }
    }
  }
  ```

* Deny

  Response: block the login

  ```json
  {
    "decision": "DENY",
    "error": { "message": "Your trial has ended. Contact sales to continue." }
  }
  ```

An endpoint that adds the user’s plan from your own records:

* Node.js

  Express.js

  ```javascript
  app.post('/auth/interceptors/pre-session-creation', async (req, res) => {
    const { user_id, organization_id } = req.body.interceptor_context;


    // Look up what your app knows about this user in this organization
    const account = await getAccount(user_id, organization_id);


    return res.json({
      decision: 'ALLOW',
      response: {
        claims: { plan: account.plan, features: account.features }
      }
    });
  });
  ```

* Python

  Flask

  ```python
  @app.post('/auth/interceptors/pre-session-creation')
  def pre_session_creation():
      body = verified_body()
      if body is None:
          return DENY_UNVERIFIED


      context = body['interceptor_context']
      # Look up what your app knows about this user in this organization
      account = get_account(context['user_id'], context['organization_id'])


      return {
          'decision': 'ALLOW',
          'response': {'claims': {'plan': account['plan'], 'features': account['features']}},
      }
  ```

The access token then carries them:

Decoded access token

```diff
{
  "sub": "usr_93418238346728951",
  "oid": "org_93418204671239865",
  "custom_claims": {
    "plan": "pro",
    "features": ["forecasting", "api_access"]
+  },
  "roles": ["member"],
  "sid": "ses_107201917586768386",
  "iss": "https://yourapp.scalekit.dev",
  "exp": 1767964824
}
```

Keep claims small, since the token travels with each request to your API. Custom claims never appear in the `scope` claim; for scopes your resource server checks, use [custom scopes and permissions](/authenticate/authz/implement-access-control/).

### Pre-user invitation

[Section titled “Pre-user invitation”](#pre-user-invitation)

Outside the login flow, Pre-user invitation runs before a user is added to an organization from the dashboard or the API ([Add users to organizations](/authenticate/manage-organizations/add-users-to-organization/)), whether the user is new or already belongs to another organization. Use it to enforce who may be invited, such as allowed email domains or seat limits you track yourself.

Request body

```json
1
{
2
  "display_name": "Allow company addresses only",
3
  "trigger_point": "PRE_USER_INVITATION",
4
  "interceptor_context": {
5
    "environment_id": "env_92561807201272162",
6
    "user_id": "usr_93418238346728951", // Only when the invitee already exists
7
    "user_email": "sarah.johnson@contractor.com", // The invitee
8
    "organization_id": "org_93731871904672153",
9
    "ip_address": "203.0.113.24", // The person sending the invitation
10
    "device_type": "Desktop",
11
    "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36...",
12
    "triggered_at": "2026-10-13T12:50:41.803Z"
13
  },
14
  "data": {
15
    "organization": {
16
      "id": "org_93731871904672153",
17
      "name": "Initech",
18
      "external_id": "initech" // Only when the organization has one
19
    }
20
  }
21
}
```

The response takes only `decision` and `error.message`. A `DENY` fails the invitation request with HTTP 400 and code `USER_INVITE_NOT_ALLOWED`, carrying your message. Under Fail closed, an endpoint failure returns HTTP 400 with `USER_INVITE_VALIDATION_FAILED` and the generic message.

Each response this trigger point accepts:

* Continue

  Response: send the invitation

  ```json
  {
    "decision": "ALLOW"
  }
  ```

* Deny

  Response: block the invitation

  ```json
  {
    "decision": "DENY",
    "error": { "message": "Invite people with an @initech.com address." }
  }
  ```

### Pre-M2M token creation

[Section titled “Pre-M2M token creation”](#pre-m2m-token-creation)

Pre-M2M token creation runs before Scalekit issues a machine-to-machine access token to an [M2M client](/authenticate/m2m/api-auth-quickstart/). `data.m2m_token_claims.claims` holds the claims Scalekit puts in the token.

Request body

```json
1
{
2
  "display_name": "Add sync service claims",
3
  "trigger_point": "PRE_M2M_TOKEN_CREATION",
4
  "interceptor_context": {
5
    "environment_id": "env_17002334043308132",
6
    "client_id": "m2morg_93710427703245914",
7
    "user_agent": "crm-sync-service/2.1.0",
8
    "device_type": "Unknown",
9
    "triggered_at": "2026-10-13T21:22:20.173Z"
10
  },
11
  "data": {
12
    "m2m_token_claims": {
13
      "client_id": "m2morg_93710427703245914",
14
      "claims": {
15
        "oid": "org_89669394174574792",
16
        "scope": "contacts:read deals:write",
17
        "scopes": ["contacts:read", "deals:write"],
18
        "custom_claims": { "service_name": "warehouse-sync" } // From the client's configuration
19
      }
20
    }
21
  }
22
}
```

`DENY` fails the token request. With `ALLOW`, Scalekit merges `response.claims` into the token’s `custom_claims`, and a key you return replaces the same key from the client’s configuration. You cannot change `scope`, `aud` or any other top-level claim this way: a `scope` you return lands at `custom_claims.scope`.

Each response this trigger point accepts:

* Continue

  Response: issue the token unchanged

  ```json
  {
    "decision": "ALLOW"
  }
  ```

* Add claims

  Response: add a rate limit claim

  ```json
  {
    "decision": "ALLOW",
    "response": {
      "claims": { "rate_limit": 1000 }
    }
  }
  ```

* Deny

  Response: block the token

  ```json
  {
    "decision": "DENY",
    "error": { "message": "This client is disabled." }
  }
  ```

---
# DOCUMENT BOUNDARY
---

# Add OAuth 2.0 to your APIs

> Secure your APIs in minutes with OAuth 2.0 client credentials, scoped access, and JWT validation using Scalekit

APIs let your customers, partners, and external systems interact with your application and its data. You need authentication to ensure only authorized clients can consume your APIs. Scalekit helps you add OAuth 2.0-based client-credentials authentication to your API endpoints.

If you are new to JWT-based API authentication, read the cookbook **[M2M JWT verification with JWKS and OAuth scopes](/cookbooks/m2m-jwks-and-oauth-scopes/)** for foundational context before following the steps below.

Here’s how it works:

![API client registration: your app creates an API client in Scalekit, the user saves its credentials, and your app validates its access tokens on each request](/d2/docs/authenticate/m2m/api-auth-quickstart-0.svg)

1. ## Installation

   [Section titled “Installation”](#installation)

   Scalekit becomes the authorization server for your APIs. Using Scalekit provides necessary methods to register and authenticate API clients.

   ```sh
   pip install scalekit-sdk-python
   ```

   Alternatively, you can use the [REST APIs directly](/apis/#tag/api-auth).

   Note

   Scalekit provides Node.js, Python, Go, and Java SDKs. [Contact us](/support/contact-us) if you need support for another language.

2. ## Enable API client registration for your customers

   [Section titled “Enable API client registration for your customers”](#enable-api-client-registration-for-your-customers)

   Allow your customers to register their applications as API clients. This process generates unique credentials that they can use to authenticate their application when interacting with your API.

   Scalekit will return a client ID and secret that you can show to your customers to integrate their application with your API.

   * An Organization ID identifies your customer, and multiple API clients can be registered for the same organization.
   * The `POST /organizations/{organization_id}/clients` endpoint creates a new API client for the organization. See [Scalekit API Authentication](/apis/#description/quickstart) to get the `` in case of HTTP requests.

   - cURL

     POST /organizations/{organization_id}/clients

     ```bash
     # For authentication details, see: http://docs.scalekit.com/apis#description/authentication
     curl -L '/api/v1/organizations//clients' \
     -H 'Content-Type: application/json' \
     -H 'Authorization: Bearer ' \
     -d '{
         "name": "GitHub Actions Deployment Service",  # A descriptive name for the API client
         "description": "Service account for GitHub Actions to deploy applications to production",  # A detailed explanation of the clients purpose and usage
         "custom_claims": [  # Key-value pairs that provide additional context about the client. Each claim must have a `key` and `value` field
             {
                 "key": "github_repository",
                 "value": "acmecorp/inventory-service"
             },
             {
                 "key": "environment",
                 "value": "production_us"
             }
         ],
         "scopes": [  # List of permissions the client needs (e.g., ["deploy:applications", "read:deployments"])
             "deploy:applications",
             "read:deployments"
         ],
         "audience": [  # List of API endpoints this client will access (e.g., ["deployment-api.acmecorp.com"])
             "deployment-api.acmecorp.com"
         ],
         "expiry": 3600  # Token expiration time in seconds. Defaults to 3600 (1 hour)
     }'
     ```

     Sample response

     Sample response

     ```json
     {
         "client": {
             "client_id": "m2morg_68315758685323697",
             "secrets": [
                 {
                     "id": "sks_68315758802764209",
                     "create_time": "2025-04-16T06:56:05.360Z",
                     "update_time": "2025-04-16T06:56:05.367190455Z",
                     "secret_suffix": "UZ0X",
                     "status": "ACTIVE",
                     "last_used_time": "2025-04-16T06:56:05.360Z"
                 }
             ],
             "name": "GitHub Actions Deployment Service",
             "description": "Service account for GitHub Actions to deploy applications to production",
             "organization_id": "org_59615193906282635",
             "create_time": "2025-04-16T06:56:05.290Z",
             "update_time": "2025-04-16T06:56:05.292145150Z",
             "scopes": [
                 "deploy:applications",
                 "read:deployments"
             ],
             "audience": [
                 "deployment-api.acmecorp.com"
             ],
             "custom_claims": [
                 {
                     "key": "github_repository",
                     "value": "acmecorp/inventory-service"
                 },
                 {
                     "key": "environment",
                     "value": "production_us"
                 }
             ]
         },
         "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoa..."
     }
     ```

   - Python

     ```python
     1
     from scalekit.v1.clients.clients_pb2 import OrganizationClient
     2


     3
     org_id = ""
     4


     5
     api_client = OrganizationClient(
     6
         name="GitHub Actions Deployment Service",  # A descriptive name for the API client
     7
         description="Service account for GitHub Actions to deploy applications to production",  # A detailed explanation of the client's purpose and usage
     8
         custom_claims=[  # Key-value pairs that provide additional context about the client. Each claim must have a `key` and `value` field
     9
             {
     10
                 "key": "github_repository",
     11
                 "value": "acmecorp/inventory-service"
     12
             },
     13
             {
     14
                 "key": "environment",
     15
                 "value": "production_us"
     16
             }
     17
         ],
     18
         scopes=["deploy:applications", "read:deployments"],  # List of permissions the client needs
     19
         audience=["deployment-api.acmecorp.com"],  # List of API endpoints this client will access
     20
         expiry=3600  # Token expiration time in seconds. Defaults to 3600 (1 hour)
     21
     )
     22


     23
     response = scalekit_client.m2m_client.create_organization_client(
     24
         organization_id=org_id,
     25
         m2m_client=api_client
     26
     )
     27


     28
     # Persist the generated credentials securely in your application
     29
     client_id = response.client.client_id
     30
     plain_secret = response.plain_secret
     ```

     Sample response

     Sample response

     ```json
     {
         "client": {
             "client_id": "m2morg_68315758685323697",
             "secrets": [
                 {
                     "id": "sks_68315758802764209",
                     "create_time": "2025-04-16T06:56:05.360Z",
                     "update_time": "2025-04-16T06:56:05.367190455Z",
                     "secret_suffix": "UZ0X",
                     "status": "ACTIVE",
                     "last_used_time": "2025-04-16T06:56:05.360Z"
                 }
             ],
             "name": "GitHub Actions Deployment Service",
             "description": "Service account for GitHub Actions to deploy applications to production",
             "organization_id": "org_59615193906282635",
             "create_time": "2025-04-16T06:56:05.290Z",
             "update_time": "2025-04-16T06:56:05.292145150Z",
             "scopes": [
                 "deploy:applications",
                 "read:deployments"
             ],
             "audience": [
                 "deployment-api.acmecorp.com"
             ],
             "custom_claims": [
                 {
                     "key": "github_repository",
                     "value": "acmecorp/inventory-service"
                 },
                 {
                     "key": "environment",
                     "value": "production_us"
                 }
             ]
         },
         "plain_secret": "test_ly8G57h0ErRJSObJI6dShkoaq6bigo11Dxcf.."
     }
     ```

   Tip

   Scalekit only returns the `plain_secret` once during client creation and does not store it. Instruct your API client developers to store the `plain_secret` securely.

3. ## API client requests Bearer access token for API authentication

   [Section titled “API client requests Bearer access token for API authentication”](#api-client-requests-bearer-access-token-for-api-authentication)

   API clients use the `client_id` and `client_secret` issued in the previous step to reach your Scalekit environment and get the access token. No action is needed by you in your API server. This section only demonstrates how API clients get the `access_token`.

   The client sends a POST request to the `/oauth/token` endpoint:

   * cURL

     POST /oauth/token

     ```sh
     curl -X POST \
       "https:///oauth/token" \
       -H "Content-Type: application/x-www-form-urlencoded" \
       -d "grant_type=client_credentials" \
       -d "client_id=" \
       -d "client_secret=" \
     ```

   * Python

     ```python
     1
     client_id = "API_CLIENT_ID"
     2
     client_secret = "API_CLIENT_SECRET"
     3


     4
     token_response = scalekit_client.generate_client_token(
     5
         client_id=client_id,
     6
         client_secret=client_secret
     7
     )
     ```

   Upon successful authentication, your Scalekit environment issues a JWT access token to the API client.

   Access token response

   ```json
   {
     "access_token":"",
     "token_type":"Bearer",
     "expires_in":86399,
     // Same scopes that were granted during client registration
     "scope":"deploy:applications read:deployments"
   }
   ```

   The client includes this access token in the `Authorization` header of subsequent requests to your API server. Your API server validates these tokens before granting access to resources.

4. ## Validate and authenticate API client’s access tokens

   [Section titled “Validate and authenticate API client’s access tokens”](#validate-and-authenticate-api-clients-access-tokens)

   Your API server must validate the incoming JWT access token to ensure the request originates from a trusted API client and that the token is legitimate.

   Validate the token in two steps:

   1. **Retrieve the public key:** Fetch the appropriate public key from your Scalekit environment’s [JSON Web Key Set (JWKS)](/cookbooks/m2m-jwks-and-oauth-scopes/#jwks-and-scalekit-keys) at `https:///keys`. Use the `kid` (Key ID) from the JWT header to identify the correct key. Cache the key according to standard JWKS practices.

      * Node.js

        ```js
        import jwksClient from 'jwks-rsa';


        const client = jwksClient({
          jwksUri: `${process.env.SCALEKIT_ENVIRONMENT_URL}/keys`,
          cache: true
        });


        async function getPublicKey(header: any): Promise {
          return new Promise((resolve, reject) => {
            client.getSigningKey(header.kid, (err, key) => {
              if (err) reject(err);
              else resolve(key.getPublicKey());
            });
          });
        }
        ```

      * Python

        ```py
        # This is automatically handled by Scalekit SDK
        ```

   2. **Verify the token signature:** Use the retrieved public key and a JWT library to verify the token’s signature and claims (like issuer, audience, and expiration).

      * Node.js

        ```js
        import jwt from 'jsonwebtoken';


        async function verifyToken(token: string, publicKey: string) {
          try {
            const decoded = jwt.decode(token, { complete: true });
            const verified = jwt.verify(token, publicKey, {
              algorithms: ['RS256'],
              complete: true
            });
            return verified.payload;
          } catch (error) {
            throw new Error('Token verification failed');
          }
        }
        ```

      * Python

        ```py
        # Token from the incoming API request's authorization header
        token = token_response[""]


        claims = scalekit_client.validate_access_token_and_get_claims(
            token=token
        )
        ```

   Upon successful token verification, your API server gains confidence in the request’s legitimacy and can proceed to process the request, leveraging the authorization scopes embedded within the token.

5. ## Register API client’s scopes Optional

   [Section titled “Register API client’s scopes ”](#register-api-clients-scopes-)

   [OAuth scopes](/cookbooks/m2m-jwks-and-oauth-scopes/#oauth-scopes-for-machine-clients) are embedded in the access token and validated server-side using the Scalekit SDK. This ensures that API clients only access resources they’re authorized for, adding an extra layer of security.

   For example, you might create an API client for a customer’s deployment service with scopes like `deploy:applications` and `read:deployments`.

   * cURL

     Register an API client with specific scopes

     ```bash
     curl -L 'https:///api/v1/organizations//clients' \
     -H 'Content-Type: application/json' \
     -H 'Authorization: Bearer ' \
     -d '{
         "name": "GitHub Actions Deployment Service",
         "description": "Service account for GitHub Actions to deploy applications to production",
         "scopes": [
             "deploy:applications",
             "read:deployments"
         ],
         "expiry": 3600
     }'
     ```

     Sample response

     Sample response

     ```json
     {
         "client": {
             "client_id": "m2morg_68315758685323697",
             "secrets": [
                 {
                     "id": "sks_68315758802764209",
                     "create_time": "2025-04-16T06:56:05.360Z",
                     "update_time": "2025-04-16T06:56:05.367190455Z",
                     "secret_suffix": "UZ0X",
                     "status": "ACTIVE",
                     "last_used_time": "2025-04-16T06:56:05.360Z"
                 }
             ],
             "name": "GitHub Actions Deployment Service",
             "description": "Service account for GitHub Actions to deploy applications to production",
             "organization_id": "org_59615193906282635",
             "create_time": "2025-04-16T06:56:05.290Z",
             "update_time": "2025-04-16T06:56:05.292145150Z",
             "scopes": [
                 "deploy:applications",
                 "read:deployments"
             ]
         },
          "plain_secret": ""
     }
     ```

   * Node.js

     Register an API client with specific scopes

     ```javascript
     1
     // Use case: Your customer requests API access for their deployment automation.
     2
     // You register an API client app with the appropriate scopes.
     3
     import { ScalekitClient } from '@scalekit-sdk/node';
     4


     5
     // Initialize Scalekit client (see installation guide for setup)
     6
     const scalekit = new ScalekitClient(
     7
       process.env.SCALEKIT_ENVIRONMENT_URL,
     8
       process.env.SCALEKIT_CLIENT_ID,
     9
       process.env.SCALEKIT_CLIENT_SECRET
     10
     );
     11


     12
     async function createAPIClient() {
     13
       try {
     14
         // Define API client details with scopes your customer's app needs
     15
         const clientDetails = {
     16
           name: 'GitHub Actions Deployment Service',
     17
           description: 'Service account for GitHub Actions to deploy applications to production',
     18
           scopes: ['deploy:applications', 'read:deployments'],
     19
           expiry: 3600, // Token expiry in seconds
     20
         };
     21


     22
         // API call to register the client
     23
         const response = await scalekit.m2m.createClient({
     24
           organizationId: process.env.SCALEKIT_ORGANIZATION_ID,
     25
           client: clientDetails,
     26
         });
     27


     28
         // Response contains client details and the plain_secret (only returned once)
     29
         const clientId = response.client.client_id;
     30
         const plainSecret = response.plain_secret;
     31


     32
         // Provide these credentials to your customer securely
     33
         console.log('Created API client:', clientId);
     34
       } catch (error) {
     35
         console.error('Error creating API client:', error);
     36
       }
     37
     }
     38


     39
     createAPIClient();
     ```

   * Python

     Register an API client with specific scopes

     ```python
     1
     # Use case: Your customer requests API access for their deployment automation.
     2
     # You register an API client app with the appropriate scopes.
     3
     import os
     4
     from scalekit import ScalekitClient
     5


     6
     # Initialize Scalekit client (see installation guide for setup)
     7
     scalekit_client = ScalekitClient(
     8
         env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
     9
         client_id=os.getenv("SCALEKIT_CLIENT_ID"),
     10
         client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
     11
     )
     12


     13
     try:
     14
         # Define API client details with scopes your customer's app needs
     15
         from scalekit.v1.clients.clients_pb2 import OrganizationClient
     16


     17
         client_details = OrganizationClient(
     18
             name="GitHub Actions Deployment Service",
     19
             description="Service account for GitHub Actions to deploy applications to production",
     20
             scopes=["deploy:applications", "read:deployments"],
     21
             expiry=3600  # Token expiry in seconds
     22
         )
     23


     24
         # API call to register the client
     25
         response = scalekit_client.m2m_client.create_organization_client(
     26
             organization_id=os.getenv("SCALEKIT_ORGANIZATION_ID"),
     27
             m2m_client=client_details
     28
         )
     29


     30
         # Response contains client details and the plain_secret (only returned once)
     31
         client_id = response.client.client_id
     32
         plain_secret = response.plain_secret
     33


     34
         # Provide these credentials to your customer securely
     35
         print("Created API client:", client_id)
     36


     37
     except Exception as e:
     38
         print("Error creating API client:", e)
     ```

   The API returns a JSON object with two key parts:

   * `client.client_id` - The client identifier
   * `plain_secret` - The client secret (only returned once, never stored by Scalekit)

   Provide both values to your customer securely. Your customer will use these credentials in their application to authenticate with your API. The `plain_secret` is never shown again after creation.

   Additional parameters

   You can also include `custom_claims` (key-value metadata) and `audience` (target API endpoints) when registering API clients. See the [API keys guide](/authenticate/m2m/api-keys) for examples.

6. ## Verify API client’s scopes

   [Section titled “Verify API client’s scopes”](#verify-api-clients-scopes)

   When your API server receives a request from an API client app, you must validate the scopes present in the access token provided in the `Authorization` header. The access token is a JSON Web Token (JWT).

   First, let’s look at the claims inside a decoded JWT payload. Scalekit encodes the granted scopes in the `scopes` field.

   Example decoded access token

   ```json
   {
     "client_id": "m2morg_69038819013296423",
     "exp": 1745305340,
     "iat": 1745218940,
     "iss": "",
     "jti": "tkn_69041163914445100",
     "nbf": 1745218940,
     "oid": "org_59615193906282635",
     "scopes": [
       "deploy:applications",
       "read:deployments"
     ],
     "sub": "m2morg_69038819013296423"
   }
   ```

   Scope Naming Conventions

   Structure your scopes using the `resource:action` pattern, for example `deployments:read` or `applications:create`. This makes permissions clear and manageable for your customers.

   Your API server should inspect the `scopes` array in the token payload to authorize the requested operation. Here’s how you validate the token and check for a specific scope in your API server.

   * Node.js

     Example Express.js middleware for scope validation

     ```javascript
     27 collapsed lines
     1
     // Security: ALWAYS validate the access token on your server before trusting its claims.
     2
     // This prevents token forgery and ensures the token has not expired.
     3
     import { ScalekitClient } from '@scalekit-sdk/node';
     4
     import jwt from 'jsonwebtoken';
     5
     import jwksClient from 'jwks-rsa';
     6


     7
     const scalekit = new ScalekitClient(
     8
       process.env.SCALEKIT_ENVIRONMENT_URL,
     9
       process.env.SCALEKIT_CLIENT_ID,
     10
       process.env.SCALEKIT_CLIENT_SECRET
     11
     );
     12


     13
     // Setup JWKS client for token verification
     14
     const client = jwksClient({
     15
       jwksUri: `${process.env.SCALEKIT_ENVIRONMENT_URL}/keys`,
     16
       cache: true
     17
     });
     18


     19
     async function getPublicKey(header) {
     20
       return new Promise((resolve, reject) => {
     21
         client.getSigningKey(header.kid, (err, key) => {
     22
           if (err) reject(err);
     23
           else resolve(key.getPublicKey());
     24
         });
     25
       });
     26
     }
     27


     28
     async function checkPermissions(req, res, next) {
     29
       const authHeader = req.headers.authorization;
     30
       if (!authHeader || !authHeader.startsWith('Bearer ')) {
     31
         return res.status(401).send('Unauthorized: Missing token');
     32
       }
     33
       const token = authHeader.split(' ')[1];
     34


     35
       try {
     36
         // Decode to get the header with kid
     37
         const decoded = jwt.decode(token, { complete: true });
     38
         const publicKey = await getPublicKey(decoded.header);
     39


     40
         // Verify the token signature and claims
     41
         const verified = jwt.verify(token, publicKey, {
     42
           algorithms: ['RS256'],
     43
           complete: true
     44
         });
     45


     46
         const decodedToken = verified.payload;
     47


     48
         // Check if the API client app has the required scope
     49
         const requiredScope = 'deploy:applications';
     50
         if (decodedToken.scopes && decodedToken.scopes.includes(requiredScope)) {
     51
           // API client app has the required scope, proceed with the request
     52
           next();
     53
         } else {
     54
           // API client app does not have the required scope
     55
           res.status(403).send('Forbidden: Insufficient permissions');
     56
         }
     57
       } catch (error) {
     58
         // Token is invalid or expired
     59
         res.status(401).send('Unauthorized: Invalid token');
     60
       }
     61
     }
     ```

   * Python

     Example Flask decorator for scope validation

     ```python
     14 collapsed lines
     1
     # Security: ALWAYS validate the access token on your server before trusting its claims.
     2
     # This prevents token forgery and ensures the token has not expired.
     3
     import os
     4
     import functools
     5
     from scalekit import ScalekitClient
     6
     from flask import request, jsonify
     7


     8
     # Initialize Scalekit client
     9
     scalekit_client = ScalekitClient(
     10
         env_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
     11
         client_id=os.getenv("SCALEKIT_CLIENT_ID"),
     12
         client_secret=os.getenv("SCALEKIT_CLIENT_SECRET")
     13
     )
     14


     15
     def check_permissions(required_scope):
     16
         def decorator(f):
     17
             @functools.wraps(f)
     18
             def decorated_function(*args, **kwargs):
     19
                 auth_header = request.headers.get('Authorization')
     20
                 if not auth_header or not auth_header.startswith('Bearer '):
     21
                     return jsonify({"error": "Unauthorized: Missing token"}), 401
     22


     23
                 token = auth_header.split(' ')[1]
     24


     25
                 try:
     26
                     # Validate the token using the Scalekit SDK
     27
                     claims = scalekit_client.validate_access_token_and_get_claims(token=token)
     28


     29
                     # Check if the API client app has the required scope
     30
                     if required_scope in claims.get('scopes', []):
     31
                         # API client app has the required scope
     32
                         return f(*args, **kwargs)
     33
                     else:
     34
                         # API client app does not have the required scope
     35
                         return jsonify({"error": "Forbidden: Insufficient permissions"}), 403
     36
                 except Exception as e:
     37
                     # Token is invalid or expired
     38
                     return jsonify({"error": "Unauthorized: Invalid token"}), 401
     39
             return decorated_function
     40
         return decorator
     41


     42
     # Example usage in a Flask route
     43
     # @app.route('/deploy', methods=['POST'])
     44
     # @check_permissions('deploy:applications')
     45
     # def deploy_application():
     46
     #     return jsonify({"message": "Deployment successful"})
     ```

---
# DOCUMENT BOUNDARY
---

# API keys

> Issue long-lived, revocable API keys scoped to organizations and users for programmatic access to your APIs

When your customers integrate with your APIs — whether for CI/CD pipelines, partner integrations, or internal tooling — they need a straightforward way to authenticate. Scalekit API keys give you long-lived, revocable bearer credentials for organization-level or user-level access to your APIs.

In this guide, you’ll learn how to create, validate, list, and revoke API keys using the Scalekit.

![API key flow: your app creates an API key in Scalekit for the user, the customer's app sends it on each request, and your app validates it to get the organization](/d2/docs/authenticate/m2m/api-keys-0.svg)

Tip

The plain-text API key is returned **only at creation time**. Scalekit does not store the key and cannot retrieve it later. Instruct your users to copy and store the key securely before closing the creation dialog.

**Organization vs user-scoped keys**: The `userId` parameter is optional. If omitted, the key is organization-scoped and grants access to all resources in that workspace. If included, the key is user-scoped and your API uses the returned user context to filter data to only that user’s resources.

1. ## Install the SDK

   [Section titled “Install the SDK”](#install-the-sdk)

   * Node.js

     ```bash
     npm install @scalekit-sdk/node
     ```

   * Python

     ```sh
     pip install scalekit-sdk-python
     ```

   * Go

     ```sh
     go get -u github.com/scalekit-inc/scalekit-sdk-go
     ```

   * Java

     ```groovy
     /* Gradle users - add the following to your dependencies in build file */
     implementation "com.scalekit:scalekit-sdk-java:2.1.3"
     ```

     ```xml
     
     
         com.scalekit
         scalekit-sdk-java
         2.1.3
     
     ```

   Initialize the Scalekit client with your environment credentials:

   * Node.js

     Express.js

     ```javascript
     2 collapsed lines
     import { ScalekitClient } from '@scalekit-sdk/node';


     const scalekit = new ScalekitClient(
       process.env.SCALEKIT_ENVIRONMENT_URL,
       process.env.SCALEKIT_CLIENT_ID,
       process.env.SCALEKIT_CLIENT_SECRET
     );
     ```

   * Python

     Flask

     ```python
     2 collapsed lines
     import os
     from scalekit import ScalekitClient


     scalekit_client = ScalekitClient(
         env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
         client_id=os.environ["SCALEKIT_CLIENT_ID"],
         client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
     )
     ```

   * Go

     Gin

     ```go
     2 collapsed lines
     import scalekit "github.com/scalekit-inc/scalekit-sdk-go/v2"


     scalekitClient := scalekit.NewScalekitClient(
       os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
       os.Getenv("SCALEKIT_CLIENT_ID"),
       os.Getenv("SCALEKIT_CLIENT_SECRET"),
     )
     ```

   * Java

     Spring Boot

     ```java
     2 collapsed lines
     import com.scalekit.ScalekitClient;


     ScalekitClient scalekitClient = new ScalekitClient(
         System.getenv("SCALEKIT_ENVIRONMENT_URL"),
         System.getenv("SCALEKIT_CLIENT_ID"),
         System.getenv("SCALEKIT_CLIENT_SECRET")
     );
     ```

2. ## Create a token

   [Section titled “Create a token”](#create-a-token)

   To get started, create an API key scoped to an organization. You can optionally scope it to a specific user and attach custom metadata.

   ### Organization-scoped API key

   [Section titled “Organization-scoped API key”](#organization-scoped-api-key)

   **When to use**: Organization-scoped keys are for customers who need full access to all resources within their workspace or account. When they authenticate with the key, Scalekit validates it and confirms the organization context — your API then exposes all resources they own.

   **Example scenario**: You’re building a CRM like HubSpot. Your customer integrates with your API using an organization-scoped key. When they request contacts, tasks, or deals, the key validates successfully for their organization, and your API returns all resources in that workspace.

   This is the most common pattern for service-to-service integrations where the API key represents access on behalf of an entire organization.

   * Node.js

     ```javascript
     try {
       const response = await scalekit.token.createToken(organizationId, {
         description: 'CI/CD pipeline token',
       });


       // Store securely — this value cannot be retrieved again after creation
       const opaqueToken = response.token;
       // Stable identifier for management operations (format: apit_xxxxx)
       const tokenId = response.tokenId;
     } catch (error) {
       console.error('Failed to create token:', error.message);
     }
     ```

   * Python

     ```python
     try:
         response = scalekit_client.tokens.create_token(
             organization_id=organization_id,
             description="CI/CD pipeline token",
         )


         opaque_token = response.token  # store this securely
         token_id = response.token_id   # format: apit_xxxxx
     except Exception as e:
         print(f"Failed to create token: {e}")
     ```

   * Go

     ```go
     response, err := scalekitClient.Token().CreateToken(
       ctx, organizationId, scalekit.CreateTokenOptions{
         Description: "CI/CD pipeline token",
       },
     )
     if err != nil {
       log.Printf("Failed to create token: %v", err)
       return
     }


     // Store securely — this value cannot be retrieved again after creation
     opaqueToken := response.Token
     // Stable identifier for management operations (format: apit_xxxxx)
     tokenId := response.TokenId
     ```

   * Java

     ```java
     import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse;


     try {
         CreateTokenResponse response = scalekitClient.tokens().create(organizationId);


         // Store securely — this value cannot be retrieved again after creation
         String opaqueToken = response.getToken();
         // Stable identifier for management operations (format: apit_xxxxx)
         String tokenId = response.getTokenId();
     } catch (Exception e) {
         System.err.println("Failed to create token: " + e.getMessage());
     }
     ```

   ### User-scoped API key

   [Section titled “User-scoped API key”](#user-scoped-api-key)

   **When to use**: User-scoped keys enable fine-grained data filtering based on who owns the key. Your API validates the key, receives the user context, and then exposes only data relevant to that user — enabling role-based filtering without additional database lookups.

   **Example scenario**: Your CRM has a `/tasks` endpoint. One customer gives their team member a user-scoped API key. When that person calls `/tasks`, the key validates for their organization *and* user, and your API returns only tasks assigned to them — not all tasks in the workspace. Another team member with a different key sees only their own tasks.

   User-scoped keys enable personal access tokens, per-user audit trails, and user-level rate limiting. You can also attach custom claims as key-value metadata.

   * Node.js

     ```javascript
     try {
       const userToken = await scalekit.token.createToken(organizationId, {
         userId: 'usr_12345',
         customClaims: {
           team: 'engineering',
           environment: 'production',
         },
         description: 'Deployment service token',
       });


       const opaqueToken = userToken.token;
       const tokenId = userToken.tokenId;
     } catch (error) {
       console.error('Failed to create token:', error.message);
     }
     ```

   * Python

     ```python
     try:
         user_token = scalekit_client.tokens.create_token(
             organization_id=organization_id,
             user_id="usr_12345",
             custom_claims={
                 "team": "engineering",
                 "environment": "production",
             },
             description="Deployment service token",
         )


         opaque_token = user_token.token
         token_id = user_token.token_id
     except Exception as e:
         print(f"Failed to create token: {e}")
     ```

   * Go

     ```go
     1
     userToken, err := scalekitClient.Token().CreateToken(
     2
       ctx, organizationId, scalekit.CreateTokenOptions{
     3
         UserId:      "usr_12345",
     4
         CustomClaims: map[string]string{
     5
           "team":        "engineering",
     6
           "environment": "production",
     7
         },
     8
         Description: "Deployment service token",
     9
       },
     10
     )
     11
     if err != nil {
     12
       log.Printf("Failed to create user token: %v", err)
     13
       return
     14
     }
     15


     16
     opaqueToken := userToken.Token
     17
     tokenId := userToken.TokenId
     ```

   * Java

     ```java
     1
     import java.util.Map;
     2
     import com.scalekit.grpc.scalekit.v1.tokens.CreateTokenResponse;
     3


     4
     try {
     5
         Map customClaims = Map.of(
     6
             "team", "engineering",
     7
             "environment", "production"
     8
         );
     9


     10
         CreateTokenResponse userToken = scalekitClient.tokens().create(
     11
             organizationId, "usr_12345", customClaims, null, "Deployment service token"
     12
         );
     13


     14
         String opaqueToken = userToken.getToken();
     15
         String tokenId = userToken.getTokenId();
     16
     } catch (Exception e) {
     17
         System.err.println("Failed to create token: " + e.getMessage());
     18
     }
     ```

   The response contains three fields:

   | Field        | Description                                                                              |
   | ------------ | ---------------------------------------------------------------------------------------- |
   | `token`      | The API key string. **Returned only at creation.**                                       |
   | `token_id`   | An identifier (format: `apit_xxxxx`) for referencing the token in management operations. |
   | `token_info` | Metadata including organization, user, custom claims, and timestamps.                    |

3. ## Validate a token

   [Section titled “Validate a token”](#validate-a-token)

   When your API server receives a request with an API key, you’ll want to verify it’s legitimate before processing the request. Pass the key to Scalekit — it validates the key server-side and returns the associated organization, user, and metadata context.

   * Node.js

     ```javascript
     import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node';


     try {
       const result = await scalekit.token.validateToken(opaqueToken);


       const orgId = result.tokenInfo?.organizationId;
       const userId = result.tokenInfo?.userId;
       const claims = result.tokenInfo?.customClaims;
     } catch (error) {
       if (error instanceof ScalekitValidateTokenFailureException) {
         // Token is invalid, expired, or revoked
         console.error('Token validation failed:', error.message);
       }
     }
     ```

   * Python

     ```python
     from scalekit import ScalekitValidateTokenFailureException


     try:
         result = scalekit_client.tokens.validate_token(token=opaque_token)


         org_id = result.token_info.organization_id
         user_id = result.token_info.user_id
         claims = result.token_info.custom_claims
     except ScalekitValidateTokenFailureException:
         # Token is invalid, expired, or revoked
         print("Token validation failed")
     ```

   * Go

     ```go
     result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken)
     if errors.Is(err, scalekit.ErrTokenValidationFailed) {
       // Token is invalid, expired, or revoked
       log.Printf("Token validation failed: %v", err)
       return
     }


     orgId := result.TokenInfo.OrganizationId
     userId := result.TokenInfo.GetUserId()   // *string — nil for org-scoped tokens
     claims := result.TokenInfo.CustomClaims
     ```

   * Java

     ```java
     import java.util.Map;
     import com.scalekit.exceptions.TokenInvalidException;
     import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse;


     try {
         ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken);


         String orgId = result.getTokenInfo().getOrganizationId();
         String userId = result.getTokenInfo().getUserId();
         Map claims = result.getTokenInfo().getCustomClaimsMap();
     } catch (TokenInvalidException e) {
         // Token is invalid, expired, or revoked
         System.err.println("Token validation failed: " + e.getMessage());
     }
     ```

   If the API key is invalid, expired, or has been revoked, validation fails with a specific error that you can catch and handle in your code. This makes it easy to reject unauthorized requests in your API middleware.

   ### Access roles and organization details

   [Section titled “Access roles and organization details”](#access-roles-and-organization-details)

   Beyond the basic organization and user information, the validation response also includes any roles assigned to the user and external identifiers you’ve configured for the organization. These are useful for making authorization decisions without additional database lookups.

   * Node.js

     ```javascript
     try {
       const result = await scalekit.token.validateToken(opaqueToken);


       // Roles assigned to the user
       const roles = result.tokenInfo?.roles;


       // External identifiers for mapping to your system
       const externalOrgId = result.tokenInfo?.organizationExternalId;
       const externalUserId = result.tokenInfo?.userExternalId;
     } catch (error) {
       if (error instanceof ScalekitValidateTokenFailureException) {
         console.error('Token validation failed:', error.message);
       }
     }
     ```

   * Python

     ```python
     try:
         result = scalekit_client.tokens.validate_token(token=opaque_token)


         # Roles assigned to the user
         roles = result.token_info.roles


         # External identifiers for mapping to your system
         external_org_id = result.token_info.organization_external_id
         external_user_id = result.token_info.user_external_id
     except ScalekitValidateTokenFailureException:
         print("Token validation failed")
     ```

   * Go

     ```go
     result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken)
     if errors.Is(err, scalekit.ErrTokenValidationFailed) {
       log.Printf("Token validation failed: %v", err)
       return
     }


     // Roles assigned to the user
     roles := result.TokenInfo.Roles


     // External identifiers for mapping to your system
     externalOrgId := result.TokenInfo.OrganizationExternalId
     externalUserId := result.TokenInfo.GetUserExternalId()  // *string — nil if no external ID
     ```

   * Java

     ```java
     1
     import java.util.List;
     2
     import com.scalekit.exceptions.TokenInvalidException;
     3
     import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse;
     4


     5
     try {
     6
         ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken);
     7


     8
         // Roles assigned to the user
     9
         List roles = result.getTokenInfo().getRolesList();
     10


     11
         // External identifiers for mapping to your system
     12
         String externalOrgId = result.getTokenInfo().getOrganizationExternalId();
     13
         String externalUserId = result.getTokenInfo().getUserExternalId();
     14
     } catch (TokenInvalidException e) {
     15
         System.err.println("Token validation failed: " + e.getMessage());
     16
     }
     ```

   Note

   Roles are available when you use [Full Stack Authentication](/authenticate/fsa/quickstart/) with [role-based access control](/authenticate/authz/overview/). Assign roles to users through the Scalekit dashboard or API.

   ### Access custom metadata

   [Section titled “Access custom metadata”](#access-custom-metadata)

   If you attached custom claims when creating the API key, they come back in every validation response. This is a convenient way to make fine-grained authorization decisions — like restricting access by team or environment — without hitting your database.

   * Node.js

     ```javascript
     try {
       const result = await scalekit.token.validateToken(opaqueToken);


       const team = result.tokenInfo?.customClaims?.team;
       const environment = result.tokenInfo?.customClaims?.environment;


       // Use metadata for authorization
       if (environment !== 'production') {
         return res.status(403).json({ error: 'Production access required' });
       }
     } catch (error) {
       if (error instanceof ScalekitValidateTokenFailureException) {
         console.error('Token validation failed:', error.message);
       }
     }
     ```

   * Python

     ```python
     try:
         result = scalekit_client.tokens.validate_token(token=opaque_token)


         team = result.token_info.custom_claims.get("team")
         environment = result.token_info.custom_claims.get("environment")


         # Use metadata for authorization
         if environment != "production":
             return jsonify({"error": "Production access required"}), 403
     except ScalekitValidateTokenFailureException:
         print("Token validation failed")
     ```

   * Go

     ```go
     result, err := scalekitClient.Token().ValidateToken(ctx, opaqueToken)
     if errors.Is(err, scalekit.ErrTokenValidationFailed) {
       log.Printf("Token validation failed: %v", err)
       return
     }


     team := result.TokenInfo.CustomClaims["team"]
     environment := result.TokenInfo.CustomClaims["environment"]


     // Use metadata for authorization
     if environment != "production" {
       c.JSON(403, gin.H{"error": "Production access required"})
       return
     }
     ```

   * Java

     ```java
     1
     import java.util.Map;
     2
     import com.scalekit.exceptions.TokenInvalidException;
     3
     import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse;
     4


     5
     try {
     6
         ValidateTokenResponse result = scalekitClient.tokens().validate(opaqueToken);
     7


     8
         String team = result.getTokenInfo().getCustomClaimsMap().get("team");
     9
         String environment = result.getTokenInfo().getCustomClaimsMap().get("environment");
     10


     11
         // Use metadata for authorization
     12
         if (!"production".equals(environment)) {
     13
             return ResponseEntity.status(403).body(Map.of("error", "Production access required"));
     14
         }
     15
     } catch (TokenInvalidException e) {
     16
         System.err.println("Token validation failed: " + e.getMessage());
     17
     }
     ```

4. ## List tokens

   [Section titled “List tokens”](#list-tokens)

   You can retrieve all active API keys for an organization at any time. The response supports pagination for large result sets, and you can filter by user to find keys scoped to a specific person.

   * Node.js

     ```javascript
     1
     try {
     2
       // List tokens for an organization
     3
       const response = await scalekit.token.listTokens(organizationId, {
     4
         pageSize: 10,
     5
       });
     6


     7
       for (const token of response.tokens) {
     8
         console.log(token.tokenId, token.description);
     9
       }
     10


     11
       // Paginate through results
     12
       if (response.nextPageToken) {
     13
         const nextPage = await scalekit.token.listTokens(organizationId, {
     14
           pageSize: 10,
     15
           pageToken: response.nextPageToken,
     16
         });
     17
       }
     18


     19
       // Filter tokens by user
     20
       const userTokens = await scalekit.token.listTokens(organizationId, {
     21
         userId: 'usr_12345',
     22
       });
     23
     } catch (error) {
     24
       console.error('Failed to list tokens:', error.message);
     25
     }
     ```

   * Python

     ```python
     1
     try:
     2
         # List tokens for an organization
     3
         response = scalekit_client.tokens.list_tokens(
     4
             organization_id=organization_id,
     5
             page_size=10,
     6
         )
     7


     8
         for token in response.tokens:
     9
             print(token.token_id, token.description)
     10


     11
         # Paginate through results
     12
         if response.next_page_token:
     13
             next_page = scalekit_client.tokens.list_tokens(
     14
                 organization_id=organization_id,
     15
                 page_size=10,
     16
                 page_token=response.next_page_token,
     17
             )
     18


     19
         # Filter tokens by user
     20
         user_tokens = scalekit_client.tokens.list_tokens(
     21
             organization_id=organization_id,
     22
             user_id="usr_12345",
     23
         )
     24
     except Exception as e:
     25
         print(f"Failed to list tokens: {e}")
     ```

   * Go

     ```go
     1
     // List tokens for an organization
     2
     response, err := scalekitClient.Token().ListTokens(
     3
       ctx, organizationId, scalekit.ListTokensOptions{
     4
         PageSize: 10,
     5
       },
     6
     )
     7
     if err != nil {
     8
       log.Printf("Failed to list tokens: %v", err)
     9
       return
     10
     }
     11


     12
     for _, token := range response.Tokens {
     13
       fmt.Println(token.TokenId, token.GetDescription())
     14
     }
     15


     16
     // Paginate through results
     17
     if response.NextPageToken != "" {
     18
       nextPage, err := scalekitClient.Token().ListTokens(
     19
         ctx, organizationId, scalekit.ListTokensOptions{
     20
           PageSize:  10,
     21
           PageToken: response.NextPageToken,
     22
         },
     23
       )
     24
       if err != nil {
     25
         log.Printf("Failed to fetch next page: %v", err)
     26
         return
     27
       }
     28
       _ = nextPage // process nextPage.Tokens
     29
     }
     30


     31
     // Filter tokens by user
     32
     userTokens, err := scalekitClient.Token().ListTokens(
     33
       ctx, organizationId, scalekit.ListTokensOptions{
     34
         UserId: "usr_12345",
     35
       },
     36
     )
     37
     if err != nil {
     38
       log.Printf("Failed to list user tokens: %v", err)
     39
       return
     40
     }
     41
     _ = userTokens // process userTokens.Tokens
     ```

   * Java

     ```java
     1
     import com.scalekit.grpc.scalekit.v1.tokens.ListTokensResponse;
     2
     import com.scalekit.grpc.scalekit.v1.tokens.Token;
     3


     4
     try {
     5
         ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null);
     6
         for (Token token : response.getTokensList()) {
     7
             System.out.println(token.getTokenId() + " " + token.getDescription());
     8
         }
     9
     } catch (Exception e) {
     10
         System.err.println("Failed to list tokens: " + e.getMessage());
     11
     }
     12


     13
     try {
     14
         ListTokensResponse response = scalekitClient.tokens().list(organizationId, 10, null);
     15
         if (!response.getNextPageToken().isEmpty()) {
     16
             ListTokensResponse nextPage = scalekitClient.tokens().list(
     17
                 organizationId, 10, response.getNextPageToken()
     18
             );
     19
         }
     20
     } catch (Exception e) {
     21
         System.err.println("Failed to paginate tokens: " + e.getMessage());
     22
     }
     23


     24
     try {
     25
         ListTokensResponse userTokens = scalekitClient.tokens().list(
     26
             organizationId, "usr_12345", 10, null
     27
         );
     28
     } catch (Exception e) {
     29
         System.err.println("Failed to list user tokens: " + e.getMessage());
     30
     }
     ```

   The response includes `totalCount` for the total number of matching tokens and `nextPageToken` / `prevPageToken` cursors for navigating pages.

5. ## Invalidate a token

   [Section titled “Invalidate a token”](#invalidate-a-token)

   When you need to revoke an API key — for example, when an employee leaves or you suspect credentials have been compromised — you can invalidate it through Scalekit. Revocation takes effect instantly: the very next validation request for that key will fail.

   This operation is **idempotent**, so calling invalidate on an already-revoked key succeeds without error.

   * Node.js

     ```javascript
     try {
       // Invalidate by API key string
       await scalekit.token.invalidateToken(opaqueToken);


       // Or invalidate by token_id (useful when you store tokenId for lifecycle management)
       await scalekit.token.invalidateToken(tokenId);
     } catch (error) {
       console.error('Failed to invalidate token:', error.message);
     }
     ```

   * Python

     ```python
     try:
         # Invalidate by API key string
         scalekit_client.tokens.invalidate_token(token=opaque_token)


         # Or invalidate by token_id (useful when you store token_id for lifecycle management)
         scalekit_client.tokens.invalidate_token(token=token_id)
     except Exception as e:
         print(f"Failed to invalidate token: {e}")
     ```

   * Go

     ```go
     // Invalidate by API key string
     if err := scalekitClient.Token().InvalidateToken(ctx, opaqueToken); err != nil {
       log.Printf("Failed to invalidate token: %v", err)
     }


     // Or invalidate by token_id (useful when you store tokenId for lifecycle management)
     if err := scalekitClient.Token().InvalidateToken(ctx, tokenId); err != nil {
       log.Printf("Failed to invalidate token: %v", err)
     }
     ```

   * Java

     ```java
     try {
         // Invalidate by API key string
         scalekitClient.tokens().invalidate(opaqueToken);


         // Or invalidate by token_id (useful when you store tokenId for lifecycle management)
         scalekitClient.tokens().invalidate(tokenId);
     } catch (Exception e) {
         System.err.println("Failed to invalidate token: " + e.getMessage());
     }
     ```

6. ## Protect your API endpoints

   [Section titled “Protect your API endpoints”](#protect-your-api-endpoints)

   Now let’s put it all together. The most common pattern is to add API key validation as middleware in your API server. Extract the Bearer token from the `Authorization` header, validate it through Scalekit, and use the returned context for authorization decisions.

   * Node.js

     Express.js

     ```javascript
     1
     import { ScalekitValidateTokenFailureException } from '@scalekit-sdk/node';
     2


     3
     async function authenticateToken(req, res, next) {
     4
       const authHeader = req.headers['authorization'];
     5
       const token = authHeader && authHeader.split(' ')[1];
     6


     7
       if (!token) {
     8
         // Reject requests without credentials to prevent unauthorized access
     9
         return res.status(401).json({ error: 'Missing authorization token' });
     10
       }
     11


     12
       try {
     13
         // Server-side validation — Scalekit checks token status in real time
     14
         const result = await scalekit.token.validateToken(token);
     15
         // Attach token context to the request for downstream handlers
     16
         req.tokenInfo = result.tokenInfo;
     17
         next();
     18
       } catch (error) {
     19
         if (error instanceof ScalekitValidateTokenFailureException) {
     20
           // Revoked, expired, or malformed tokens are rejected immediately
     21
           return res.status(401).json({ error: 'Invalid or expired token' });
     22
         }
     23
         throw error;
     24
       }
     25
     }
     26


     27
     // Apply to protected routes
     28
     app.get('/api/resources', authenticateToken, (req, res) => {
     29
       const orgId = req.tokenInfo.organizationId;
     30
       // Serve resources scoped to this organization
     31
     });
     ```

   * Python

     Flask

     ```python
     1
     from functools import wraps
     2
     from flask import request, jsonify, g
     3
     from scalekit import ScalekitValidateTokenFailureException
     4


     5
     def authenticate_token(f):
     6
         @wraps(f)
     7
         def decorated(*args, **kwargs):
     8
             auth_header = request.headers.get("Authorization", "")
     9
             if not auth_header.startswith("Bearer "):
     10
                 # Reject requests without credentials to prevent unauthorized access
     11
                 return jsonify({"error": "Missing authorization token"}), 401
     12


     13
             token = auth_header.split(" ")[1]
     14


     15
             try:
     16
                 # Server-side validation — Scalekit checks token status in real time
     17
                 result = scalekit_client.tokens.validate_token(token=token)
     18
                 # Attach token context for downstream handlers
     19
                 g.token_info = result.token_info
     20
             except ScalekitValidateTokenFailureException:
     21
                 # Revoked, expired, or malformed tokens are rejected immediately
     22
                 return jsonify({"error": "Invalid or expired token"}), 401
     23


     24
             return f(*args, **kwargs)
     25
         return decorated
     26


     27
     # Apply to protected routes
     28
     @app.route("/api/resources")
     29
     @authenticate_token
     30
     def get_resources():
     31
         org_id = g.token_info.organization_id
     32
         # Serve resources scoped to this organization
     ```

   * Go

     Gin

     ```go
     1
     func AuthenticateToken(scalekitClient scalekit.Scalekit) gin.HandlerFunc {
     2
       return func(c *gin.Context) {
     3
         authHeader := c.GetHeader("Authorization")
     4
         if !strings.HasPrefix(authHeader, "Bearer ") {
     5
           // Reject requests without credentials to prevent unauthorized access
     6
           c.JSON(401, gin.H{"error": "Missing authorization token"})
     7
           c.Abort()
     8
           return
     9
         }
     10


     11
         token := strings.TrimPrefix(authHeader, "Bearer ")
     12


     13
         // Server-side validation — Scalekit checks token status in real time
     14
         result, err := scalekitClient.Token().ValidateToken(c.Request.Context(), token)
     15
         if err != nil {
     16
           if errors.Is(err, scalekit.ErrTokenValidationFailed) {
     17
             // Revoked, expired, or malformed tokens are rejected immediately
     18
             c.JSON(401, gin.H{"error": "Invalid or expired token"})
     19
           } else {
     20
             // Surface transport or unexpected errors as 500
     21
             c.JSON(500, gin.H{"error": "Internal server error"})
     22
           }
     23
           c.Abort()
     24
           return
     25
         }
     26


     27
         // Attach token context for downstream handlers
     28
         c.Set("tokenInfo", result.TokenInfo)
     29
         c.Next()
     30
       }
     31
     }
     32


     33
     // Apply to protected routes
     34
     r.GET("/api/resources", AuthenticateToken(scalekitClient), func(c *gin.Context) {
     35
       tokenInfo := c.MustGet("tokenInfo").(*scalekit.TokenInfo)
     36
       orgId := tokenInfo.OrganizationId
     37
       // Serve resources scoped to this organization
     38
     })
     ```

   * Java

     Spring Boot

     ```java
     1
     import com.scalekit.exceptions.TokenInvalidException;
     2
     import com.scalekit.grpc.scalekit.v1.tokens.Token;
     3
     import com.scalekit.grpc.scalekit.v1.tokens.ValidateTokenResponse;
     4


     5
     @Component
     6
     public class TokenAuthFilter extends OncePerRequestFilter {
     7
         private final ScalekitClient scalekitClient;
     8


     9
         public TokenAuthFilter(ScalekitClient scalekitClient) {
     10
             this.scalekitClient = scalekitClient;
     11
         }
     12


     13
         @Override
     14
         protected void doFilterInternal(
     15
             HttpServletRequest request,
     16
             HttpServletResponse response,
     17
             FilterChain filterChain
     18
         ) throws ServletException, IOException {
     19
             String authHeader = request.getHeader("Authorization");
     20
             if (authHeader == null || !authHeader.startsWith("Bearer ")) {
     21
                 // Reject requests without credentials to prevent unauthorized access
     22
                 response.sendError(401, "Missing authorization token");
     23
                 return;
     24
             }
     25


     26
             String token = authHeader.substring(7);
     27


     28
             try {
     29
                 // Server-side validation — Scalekit checks token status in real time
     30
                 ValidateTokenResponse result = scalekitClient.tokens().validate(token);
     31
                 // Attach token context for downstream handlers
     32
                 request.setAttribute("tokenInfo", result.getTokenInfo());
     33
                 filterChain.doFilter(request, response);
     34
             } catch (TokenInvalidException e) {
     35
                 // Revoked, expired, or malformed tokens are rejected immediately
     36
                 response.sendError(401, "Invalid or expired token");
     37
             }
     38
         }
     39
     }
     40


     41
     // Access in your controller
     42
     @GetMapping("/api/resources")
     43
     public ResponseEntity getResources(HttpServletRequest request) {
     44
         Token tokenInfo = (Token) request.getAttribute("tokenInfo");
     45
         String orgId = tokenInfo.getOrganizationId();
     46
         // Serve resources scoped to this organization
     47
     }
     ```

   ### Using validation context for data filtering

   [Section titled “Using validation context for data filtering”](#using-validation-context-for-data-filtering)

   After validation succeeds, your middleware has access to the organization and (optionally) user context. Use this context to filter the data your endpoint returns — no additional database queries needed.

   **For organization-scoped keys**: Extract the organization ID from the validation response. Your endpoint then returns resources belonging to that organization. If a customer authenticates with an organization-scoped key, they get access to all their workspace data.

   **For user-scoped keys**: Extract both organization ID and user ID. Filter your query to return only resources belonging to that user within the organization. If a team member authenticates with a user-scoped key, they see only their assigned tasks, their owned projects, or their allocated resources — depending on your application logic.

   The validation response is your source of truth. Trust the organization and user context it provides, and use it to build your authorization queries without additional lookups.

   Here are a few tips to help you get the most out of API keys in production:

   * **Store API keys securely**: Treat API keys like passwords. Store them in encrypted secrets managers or environment variables. Never log keys, commit them to version control, or expose them in client-side code.
   * **Set expiry for time-limited access**: Use the `expiry` parameter for keys that should automatically become invalid after a set period. This limits the blast radius if a key is compromised.
   * **Use custom claims for context**: Attach metadata like `team`, `environment`, or `service` as custom claims. Your API middleware can use these claims for fine-grained authorization without additional database lookups.
   * **Rotate keys safely**: To rotate an API key, create a new key, update the consuming service to use the new key, verify the new key works, then invalidate the old key. This avoids downtime during rotation.

   You now have everything you need to issue, validate, and manage API keys in your application.

---
# DOCUMENT BOUNDARY
---

# Add users to organizations

> Ways in which users join or get added to organizations

The journey of a user into your application begins with how they join an organization. A smooth onboarding experience sets the tone for their entire interaction with your product, while administrators need flexible options to manage their organization members.

Scalekit supports a variety of ways for users to join organizations. This guide covers methods ranging from manual additions in the dashboard to fully automated provisioning.

## Enable user invitations through your app

[Section titled “Enable user invitations through your app”](#enable-user-invitations-through-your-app)

Scalekit lets you add user invitation features to your app, allowing users to invite others to join their organization.

1. #### Begin the invite flow

   [Section titled “Begin the invite flow”](#begin-the-invite-flow)

   When a user clicks the invite button in your application, retrieve the `organization_id` from their ID token or the application’s context. Then, call the Scalekit SDK with the invitee’s email address to send the invitation.

   * Node.js

     Express.js invitation API

     ```javascript
     1
     // POST /api/organizations/:orgId/invite
     2
     app.post('/api/organizations/:orgId/invite', async (req, res) => {
     3
       const { orgId } = req.params
     4
       const { email } = req.body
     5


     6
       try {
     7
         // Create user and add to organization with invitation
     8
         const { user } = await scalekit.user.createUserAndMembership(orgId, {
     9
           email,
     10
           sendInvitationEmail: true, // Scalekit sends the invitation email
     11
         })
     12


     13
         res.json({
     14
           message: 'Invitation sent successfully',
     15
           userId: user.id,
     16
           email: user.email
     17
         })
     18
       } catch (error) {
     19
         res.status(400).json({ error: error.message })
     20
       }
     21
     })
     ```

   * Python

     Django invitation API

     ```python
     1
     # Python - Django invitation API
     2
     @api_view(['POST'])
     3
     def invite_user_to_organization(request, org_id):
     4
         email = request.data.get('email')
     5


     6
         try:
     7
             # Create user and add to organization with invitation
     8
             user_response = scalekit_client.user.create_user_and_membership(org_id, {
     9
                 'email': email,
     10
                 'send_invitation_email': True,  # Scalekit sends the invitation email
     11
             })
     12


     13
             return JsonResponse({
     14
                 'message': 'Invitation sent successfully',
     15
                 'user_id': user_response['user']['id'],
     16
                 'email': user_response['user']['email']
     17
             })
     18
         except Exception as error:
     19
             return JsonResponse({'error': str(error)}, status=400)
     ```

   * Go

     Gin invitation API

     ```go
     1
     // Go - Gin invitation API
     2
     func inviteUserToOrganization(c *gin.Context) {
     3
         orgID := c.Param("orgId")
     4


     5
         var req struct {
     6
             Email string `json:"email"`
     7
         }
     8


     9
         if err := c.ShouldBindJSON(&req); err != nil {
     10
             c.JSON(400, gin.H{"error": err.Error()})
     11
             return
     12
         }
     13


     14
         // Create user and add to organization with invitation
     15
         userResp, err := scalekitClient.User.CreateUserAndMembership(ctx, orgID, scalekit.CreateUserAndMembershipRequest{
     16
             Email: req.Email,
     17
             SendInvitationEmail: scalekit.Bool(true), // Scalekit sends the invitation email
     18
         })
     19


     20
         if err != nil {
     21
             c.JSON(400, gin.H{"error": err.Error()})
     22
             return
     23
         }
     24


     25
         c.JSON(200, gin.H{
     26
             "message": "Invitation sent successfully",
     27
             "user_id": userResp.User.Id,
     28
             "email":   userResp.User.Email,
     29
         })
     30
     }
     ```

   * Java

     Spring Boot invitation API

     ```java
     1
     // Java - Spring Boot invitation API
     2
     @PostMapping("/api/organizations/{orgId}/invite")
     3
     public ResponseEntity> inviteUserToOrganization(
     4
         @PathVariable String orgId,
     5
         @RequestBody InviteRequest request,
     6
         HttpSession session
     7
     ) {
     8
         try {
     9
             // Create user and add to organization with invitation
     10
             CreateUser createUser = CreateUser.newBuilder()
     11
                 .setEmail(request.email())
     12
                 .setSendInvitationEmail(true) // Scalekit sends the invitation email
     13
                 .build();
     14


     15
             CreateUserAndMembershipResponse response = scalekitClient.users()
     16
                 .createUserAndMembership(orgId, createUser);
     17


     18
             return ResponseEntity.ok(Map.of(
     19
                 "message", "Invitation sent successfully",
     20
                 "user_id", response.getUser().getId(),
     21
                 "email", response.getUser().getEmail()
     22
             ));
     23
         } catch (Exception error) {
     24
             return ResponseEntity.badRequest().body(
     25
                 Map.of("error", error.getMessage())
     26
             );
     27
         }
     28
     }
     ```

   This sends a email invitation to invitee to join the organization.

2. #### Set up initiate login endpoint

   [Section titled “Set up initiate login endpoint”](#set-up-initiate-login-endpoint)

   After the invitee clicks the invitation link they receive via email, Scalekit will handle verifying their identity in the background through the unique link embedded.

   Once verified, Scalekit automatically tries to log the invitee into your application by redirecting them to your app’s [configured initiate login endpoint](/guides/dashboard/intitate-login-endpoint/).

   Let’s go ahead and implement this endpoint.

   * Node.js

     routes/auth.js

     ```javascript
     // Handle indirect auth entry points
     app.get('/login', (req, res) => {
       const redirectUri = 'http://localhost:3000/auth/callback';
       const options = {
         scopes: ['openid', 'profile', 'email', 'offline_access']
       };


       const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options);
       res.redirect(authorizationUrl);
     });
     ```

   * Python

     routes/auth.py

     ```python
     from flask import redirect
     from scalekit import AuthorizationUrlOptions


     # Handle indirect auth entry points
     @app.route('/login')
     def login():
         redirect_uri = 'http://localhost:3000/auth/callback'
         options = AuthorizationUrlOptions()
         options.scopes = ['openid', 'profile', 'email', 'offline_access']
         options.state = session['oauth_state']


         authorization_url = scalekit_client.get_authorization_url(redirect_uri, options)
         return redirect(authorization_url)
     ```

   * Go

     routes/auth.go

     ```go
     // Handle indirect auth entry points
     r.GET("/login", func(c *gin.Context) {
       redirectUri := "http://localhost:3000/auth/callback"
       options := scalekitClient.AuthorizationUrlOptions{
         Scopes: []string{"openid", "profile", "email", "offline_access"}
       }


       authorizationUrl, _ := scalekitClient.GetAuthorizationUrl(redirectUri, options)
       c.Redirect(http.StatusFound, authorizationUrl.String())
     })
     ```

   * Java

     AuthController.java

     ```java
     import org.springframework.web.bind.annotation.GetMapping;
     import org.springframework.web.bind.annotation.RestController;
     import java.net.URL;


     // Handle indirect auth entry points
     @GetMapping("/login")
     public String login() {
         String redirectUri = "http://localhost:3000/auth/callback";
         AuthorizationUrlOptions options = new AuthorizationUrlOptions();
         options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access"));


         URL authorizationUrl = scalekitClient.authentication().getAuthorizationUrl(redirectUri, options);
         return "redirect:" + authorizationUrl.toString();
     }
     ```

   This redirection ensures that the invitee is logged into your application after they accept the invitation. User won’t see a login page along the way since the identity is already verified through the unique link embedded in the invitation email.

   If you use a `PRE_SESSION_CREATION` interceptor, it also runs on this first login, so the invitee’s access token includes your custom claims. See [Pre-session creation](/authenticate/interceptors/auth-flow-interceptors/#pre-session-creation).

The user will get an invitation email from Scalekit to accept the invitation.

## Enable Just-In-Time (JIT) provisioning

[Section titled “Enable Just-In-Time (JIT) provisioning”](#enable-just-in-time-jit-provisioning)

Organization administrators, especially at enterprises, prefer to have users verify their identity through their preferred identity provider (such as Okta, Microsoft Entra ID, etc.). This is particularly useful for enterprises with many users who need to ensure that only organization members can access the application.

Scalekit will provision the user accounts in your app automatically when they sign in through SSO for the first time and map the user to the same organization. [Learn more](/authenticate/manage-users-orgs/jit-provisioning/)

## Enable SCIM provisioning

[Section titled “Enable SCIM provisioning”](#enable-scim-provisioning)

Enterprises often rely on user directory providers (such as Okta, Microsoft Entra ID, etc.) to handle user management. This enables their organization administrators to control and manage access for organization members efficiently.

Scalekit supports SCIM provisioning, allowing your app to connect with these user directory providers so that user accounts are automatically created or removed in your app when users join or leave the organization. This automation is especially valuable for enterprise customers who want to ensure their licenses or seats are allocated efficiently, with organization admins managing access based on user groups. [Learn more](/authenticate/manage-users-orgs/scim-provisioning/)

## Add users through dashboard

[Section titled “Add users through dashboard”](#add-users-through-dashboard)

For administrative or support purposes, the Scalekit dashboard allows you to add new members directly to a customer’s organization

1. In the Scalekit dashboard, navigate to **Dashboard > Organizations**.

2. Select the organization you want to add a user to.

3. Go to the **Users** tab and click Invite User.

4. Fill out the invitation form:

   * Email Address: The user’s email
   * Role: Assign a role from the dropdown (e.g., Admin, Member, or a custom organization role)
   * Personal Information (Optional): Add the user’s first name, last name, and display name

5. Click **Send Invitation**

The user will receive an email with a link to accept the invitation and join your organization. Once they accept, their status will update in the Users tab.

Users in multiple organizations

Users belonging to multiple organizations will see an organization selection interface in subsequent login flows, allowing them to choose their desired organization. To choose the organization for them, for example from the site they started on, use a [`POST_USER_AUTHENTICATION` interceptor](/authenticate/interceptors/auth-flow-interceptors/#post-user-authentication).

---
# DOCUMENT BOUNDARY
---

# Organization session policy

> Override application-level session timeouts for specific organizations with custom absolute and idle session policies

By default, all organizations inherit the session policy configured at the application level — covering absolute session duration and idle timeout. When an enterprise customer requires stricter or different session controls than your application defaults, you can set a custom session policy on a per-organization basis.

Scalekit always enforces the **stricter of the two** (application vs. organization) at session creation time, so organization policies can only tighten — not relax — your application-level defaults.

## How it works

[Section titled “How it works”](#how-it-works)

Each organization can either inherit the application session policy or define its own. The two settings you can customize per organization are:

| Setting                      | Behavior                                                                                                                                |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| **Absolute session timeout** | Maximum session lifetime regardless of activity. Scalekit applies `min(app value, org value)`.                                          |
| **Idle session timeout**     | Inactivity period after which the session expires. Enabled if either the app or org has it on; duration is `min(app value, org value)`. |

**Access token lifetime** is not configurable at the org level. It remains an application-level setting only.

Validation rules

Custom policy values must satisfy this chain:

```plaintext
org absolute session timeout > org idle session timeout > app access token expiry
```

## Set up custom session policy for an organization

[Section titled “Set up custom session policy for an organization”](#set-up-custom-session-policy-for-an-organization)

### Prerequisites

[Section titled “Prerequisites”](#prerequisites)

Enable the **Session Policy** feature for an organization before configuring a custom policy. Navigate to **Dashboard > Organizations > \[Organization] > Overview > Edit** and turn on **Session Policy** feature. You can also use the [organization settings API](/apis/#tag/organizations/PATCH/api/v1/organizations/%7Bid%7D/settings).

![Enable session policy feature for organization.](/.netlify/images?url=_astro%2F2026-05-21-16-43-01.CaYNvMBM.png\&w=2920\&h=1570\&dpl=6ae471fc143c9a4c27550463)

### Configure via dashboard

[Section titled “Configure via dashboard”](#configure-via-dashboard)

Once the **Session Policy** feature is enabled for the organization, you can configure a custom policy for the organization via the Scalekit dashboard.

1. Go to **Dashboard > Organizations** and open the organization.
2. Click the **Session Policy** tab.
3. Select **Custom** to apply org-specific settings, or **Application** to revert to defaults.
4. Set the **Absolute session timeout** and **Idle session timeout** for the organization.
5. Click **Save**.

![Edit session policy for the organization.](/.netlify/images?url=_astro%2Forganization-session-policy.CyEzWmK_.png\&w=2930\&h=1584\&dpl=6ae471fc143c9a4c27550463)

### Let org admins self-serve via Hosted Widgets

[Section titled “Let org admins self-serve via Hosted Widgets”](#let-org-admins-self-serve-via-hosted-widgets)

You can let your customers manage their own session policy through [Hosted Widgets](/authenticate/manage-users-orgs/hosted-widgets/) — an embeddable self-service portal that lets your customers manage organization and user-level settings. When the `Session Policy` feature is enabled for an organization, the Session Policy widget becomes available in the Hosted Widget portal.

### Configure via API/SDK

[Section titled “Configure via API/SDK”](#configure-via-apisdk)

1. **Get the current session policy**

   Retrieve the active session policy for an organization to display it in your settings UI or audit the current configuration.

   * Node.js

     Get session policy

     ```javascript
     try {
       const policy = await scalekit.organization.getOrganizationSessionPolicy('org_12345');


       // policySource: 1 = APPLICATION (inheriting defaults), 2 = CUSTOM (org-specific values active)
       console.log('Policy source:', policy.policySource);
       console.log('Absolute timeout (minutes):', policy.absoluteSessionTimeout);
       console.log('Idle timeout enabled:', policy.idleSessionTimeoutEnabled);
     } catch (error) {
       console.error('Failed to get session policy:', error.message);
     }
     ```

   * Python

     Get session policy

     ```python
     from scalekit.v1.organizations.organizations_pb2 import SessionPolicyType


     try:
         response, _ = scalekit_client.organization.get_organization_session_policy('org_12345')
         policy = response.policy


         if policy.policy_source == SessionPolicyType.CUSTOM:
             print('Absolute timeout (minutes):', policy.absolute_session_timeout.value)
             print('Idle timeout enabled:', policy.idle_session_timeout_enabled.value)
     except Exception as e:
         print('Failed to get session policy:', e)
     ```

   * Go

     Get session policy

     ```go
     policy, err := scalekitClient.Organization().GetOrganizationSessionPolicy(ctx, "org_12345")
     if err != nil {
         log.Fatal(err)
     }


     if policy.PolicySource == scalekit.SessionPolicySourceCustom {
         fmt.Println("Absolute timeout (minutes):", policy.AbsoluteSessionTimeout.GetValue())
         fmt.Println("Idle timeout enabled:", policy.IdleSessionTimeoutEnabled.GetValue())
     }
     ```

   * Java

     Get session policy

     ```java
     import com.scalekit.grpc.scalekit.v1.organizations.OrganizationSessionPolicySettings;
     import com.scalekit.grpc.scalekit.v1.organizations.SessionPolicyType;


     try {
         OrganizationSessionPolicySettings policy =
             scalekitClient.organizations().getOrganizationSessionPolicy("org_12345");


         if (policy.getPolicySource() == SessionPolicyType.CUSTOM) {
             System.out.println("Absolute timeout (minutes): " + policy.getAbsoluteSessionTimeout().getValue());
             System.out.println("Idle timeout enabled: " + policy.getIdleSessionTimeoutEnabled().getValue());
         }
     } catch (Exception e) {
         System.err.println("Failed to get session policy: " + e.getMessage());
     }
     ```

2. **Set a custom session policy**

   Apply a custom policy when an organization requires different session durations than your application defaults.

   * Node.js

     Set custom session policy

     ```javascript
     try {
       const updated = await scalekit.organization.updateOrganizationSessionPolicy('org_12345', {
         policySource: 'CUSTOM',
         absoluteSessionTimeout: 480,
         absoluteSessionTimeoutUnit: 'MINUTES',
         idleSessionTimeoutEnabled: true,
         idleSessionTimeout: 60,
         idleSessionTimeoutUnit: 'MINUTES',
       });


       console.log('Policy updated:', updated.policySource);
     } catch (error) {
       console.error('Failed to update session policy:', error.message);
     }
     ```

   * Python

     Set custom session policy

     ```python
     1
     from scalekit.v1.organizations.organizations_pb2 import SessionPolicyType
     2
     from scalekit.v1.commons.commons_pb2 import TimeUnit
     3


     4
     try:
     5
         response, _ = scalekit_client.organization.update_organization_session_policy(
     6
             organization_id='org_12345',
     7
             policy_source=SessionPolicyType.CUSTOM,
     8
             absolute_session_timeout=480,
     9
             absolute_session_timeout_unit=TimeUnit.MINUTES,
     10
             idle_session_timeout_enabled=True,
     11
             idle_session_timeout=60,
     12
             idle_session_timeout_unit=TimeUnit.MINUTES,
     13
         )
     14


     15
         print('Policy updated:', response.policy.policy_source)
     16
     except Exception as e:
     17
         print('Failed to update session policy:', e)
     ```

   * Go

     Set custom session policy

     ```go
     1
     timeout := int32(480)
     2
     idleTimeout := int32(60)
     3
     idleEnabled := true
     4


     5
     updated, err := scalekitClient.Organization().UpdateOrganizationSessionPolicy(ctx, "org_12345", scalekit.OrganizationSessionPolicy{
     6
         PolicySource:               scalekit.SessionPolicySourceCustom,
     7
         AbsoluteSessionTimeout:     &timeout,
     8
         AbsoluteSessionTimeoutUnit: scalekit.TimeUnitMinutes,
     9
         IdleSessionTimeoutEnabled:  &idleEnabled,
     10
         IdleSessionTimeout:         &idleTimeout,
     11
         IdleSessionTimeoutUnit:     scalekit.TimeUnitMinutes,
     12
     })
     13
     if err != nil {
     14
         log.Fatal(err)
     15
     }
     16


     17
     fmt.Println("Policy updated:", updated.PolicySource)
     ```

   * Java

     Set custom session policy

     ```java
     1
     import com.google.protobuf.Int32Value;
     2
     import com.google.protobuf.BoolValue;
     3
     import com.scalekit.grpc.scalekit.v1.commons.TimeUnit;
     4
     import com.scalekit.grpc.scalekit.v1.organizations.OrganizationSessionPolicySettings;
     5
     import com.scalekit.grpc.scalekit.v1.organizations.SessionPolicyType;
     6


     7
     try {
     8
         OrganizationSessionPolicySettings policy = OrganizationSessionPolicySettings.newBuilder()
     9
             .setPolicySource(SessionPolicyType.CUSTOM)
     10
             .setAbsoluteSessionTimeout(Int32Value.of(480))
     11
             .setAbsoluteSessionTimeoutUnit(TimeUnit.MINUTES)
     12
             .setIdleSessionTimeoutEnabled(BoolValue.of(true))
     13
             .setIdleSessionTimeout(Int32Value.of(60))
     14
             .setIdleSessionTimeoutUnit(TimeUnit.MINUTES)
     15
             .build();
     16


     17
         OrganizationSessionPolicySettings updated =
     18
             scalekitClient.organizations().updateOrganizationSessionPolicy("org_12345", policy);
     19


     20
         System.out.println("Policy updated: " + updated.getPolicySource());
     21
     } catch (Exception e) {
     22
         System.err.println("Failed to update session policy: " + e.getMessage());
     23
     }
     ```

3. **Revert to application defaults**

   Remove the custom policy and restore the organization to the application-level session settings.

   * Node.js

     Revert to application defaults

     ```javascript
     try {
       await scalekit.organization.updateOrganizationSessionPolicy('org_12345', {
         policySource: 'APPLICATION',
       });
     } catch (error) {
       console.error('Failed to revert session policy:', error.message);
     }
     ```

   * Python

     Revert to application defaults

     ```python
     from scalekit.v1.organizations.organizations_pb2 import SessionPolicyType


     try:
         scalekit_client.organization.update_organization_session_policy(
             organization_id='org_12345',
             policy_source=SessionPolicyType.APPLICATION,
         )
     except Exception as e:
         print('Failed to revert session policy:', e)
     ```

   * Go

     Revert to application defaults

     ```go
     _, err := scalekitClient.Organization().UpdateOrganizationSessionPolicy(ctx, "org_12345", scalekit.OrganizationSessionPolicy{
         PolicySource: scalekit.SessionPolicySourceApplication,
     })
     if err != nil {
         log.Fatal(err)
     }
     ```

   * Java

     Revert to application defaults

     ```java
     import com.scalekit.grpc.scalekit.v1.organizations.OrganizationSessionPolicySettings;
     import com.scalekit.grpc.scalekit.v1.organizations.SessionPolicyType;


     try {
         OrganizationSessionPolicySettings policy = OrganizationSessionPolicySettings.newBuilder()
             .setPolicySource(SessionPolicyType.APPLICATION)
             .build();


         scalekitClient.organizations().updateOrganizationSessionPolicy("org_12345", policy);
     } catch (Exception e) {
         System.err.println("Failed to revert session policy: " + e.getMessage());
     }
     ```

---
# DOCUMENT BOUNDARY
---

# Remove users from organizations

> Remove users from organizations through dashboard management and API while maintaining security and compliance

As your application grows and teams evolve, your administrators will need to manage user access when employees leave, change roles, or when administrators need to revoke access for security reasons. Proper user removal ensures that access control remains accurate, licenses are managed efficiently, and security is maintained across your organization.

When a user is removed from an organization, they immediately lose access to that organization’s resources. The user’s account remains in Scalekit, but their membership status changes, and they can no longer access organization-specific data or features.

* User loses access to ONE specific organization
* User account remains in Scalekit
* User can still access OTHER organizations they belong to
* Reversible - user can be re-added later

- Node.js

  Remove users from organizations

  ```javascript
  // Use case: Remove user during offboarding workflow triggered by HR system
  await scalekit.user.deleteMembership({
    organizationId: 'org_12345',
    userId: 'usr_67890'
  })
  ```

- Python

  Remove users from organizations

  ```python
  # Use case: Remove user during offboarding workflow triggered by HR system
  scalekit_client.users.delete_membership(
      organization_id="org_12345",
      user_id="usr_67890"
  )
  ```

- Go

  Remove users from organizations

  ```go
  // Use case: Remove user during offboarding workflow triggered by HR system
  err := scalekitClient.User().DeleteMembership(ctx, "org_123", "user_456", false)
  if err != nil {
      log.Printf("Failed to remove user: %v", err)
      return err
  }
  ```

- Java

  Remove users from organizations

  ```java
  // Use case: Remove user during offboarding workflow triggered by HR system
  try {
      scalekitClient.user().deleteMembership("org_123", "user_456");
  } catch (Exception e) {
      log.error("Failed to remove user: " + e.getMessage());
      throw e;
  }
  ```

The membership is removed, effectively dropping the user’s access to the specified organization.

```diff
1
{
2
    "user": {
6 collapsed lines
3
        "id": "usr_96194455173857548",
4
        "environment_id": "env_58345499215790610",
5
        "create_time": "2025-10-25T14:46:03.300Z",
6
        "update_time": "2025-10-31T11:33:31.639425Z",
7
        "email": "saifshine7+locksmith@gmail.com",
8
        "external_id": "hitman",
9
        "memberships": [
10
            {
11
                "organization_id": "org_96194455157080332",
12
                "membership_status": "ACTIVE",
13
                "roles": [
14
                    {
15
                        "id": "role_69229687729029148",
16
                        "name": "admin",
17
                        "display_name": "Admin"
18
                    }
19
                ],
20
                "name": "",
21
                "metadata": {},
22
                "display_name": ""
23
            },
24
-            {
25
                "organization_id": "org_67609586521080405",
26
                "membership_status": "PENDING_INVITE",
27
                "roles": [
28
-                    {
29
                        "id": "role_69229700009951260",
30
                        "name": "member",
31
                        "display_name": "Member"
32
-                    }
33
-                ],
34
                "name": "Megasoft Inc",
35
                "metadata": {},
36
                "display_name": "Megasoft Inc",
37
                "created_at": "2025-10-31T12:38:42.270Z",
38
                "expires_at": "2025-11-15T12:38:42.231316Z"
39
-            }
40
        ],
41
        "user_profile": {
9 collapsed lines
42
            "id": "usp_96194455173923084",
43
            "first_name": "Saif",
44
            "last_name": "Shines",
45
            "name": "",
46
            "locale": "",
47
            "email_verified": true,
48
            "phone_number": "80384873",
49
            "metadata": {},
50
            "custom_attributes": {}
51
        },
52
        "metadata": {}
53
    }
54
}
```

User removal from an organization involves several important considerations and behaviors.

* When a user is removed from an organization and has no other organizational memberships, Scalekit will automatically delete their user account.
* Your application is responsible for handling the transfer or deletion of the user’s resources when they are removed from an organization.
* Scalekit immediately terminates the user’s active session upon removal from an organization.
* Removing a user from one organization does not impact their memberships in other organizations.
* When a user is removed from an organization, that organization will be automatically removed from the user’s organization switcher options.

## Automate user removal with directory sync

[Section titled “Automate user removal with directory sync”](#automate-user-removal-with-directory-sync)

When organizations use enterprise directory providers with [SCIM provisioning](/guides/user-management/scim-provisioning/), users are automatically removed from Scalekit organizations when they’re deprovisioned in the source directory. This ensures consistent access control across all systems without requiring manual intervention.

When a user is removed from your enterprise directory provider (such as Okta, Azure AD, or JumpCloud):

1. The directory provider sends a SCIM DELETE request to Scalekit
2. Scalekit automatically removes the user’s membership from the organization by marking the `memberships.membership_status` as `INACTIVE`
3. The user immediately loses access to organization resources
4. Your application receives webhook notifications about the membership change

This automation is particularly valuable for enterprise customers who manage large numbers of users and need to ensure that license allocation and access control remain synchronized with their directory provider.

Early access

De-provisioning via SCIM is currently in limited release. Interested in activating this feature for your Scalekit environment? [Reach out to our team](/support/contact-us) to request early access.

## Remove users in the Scalekit dashboard

[Section titled “Remove users in the Scalekit dashboard”](#remove-users-in-the-scalekit-dashboard)

Use the Scalekit dashboard when administrators need to manually remove users for compliance, security, support or administrative purposes. This approach provides direct control and visibility into the removal process, making it ideal for situations requiring manual oversight.

1. Sign in to the Scalekit dashboard and navigate to **Dashboard** > **Organizations**. Select the organization from which you want to remove users.

2. Click on the **Users** tab to view all organization members. Locate the user you want to remove from the user list. You can use the search functionality to quickly find specific users by name or email.

3. Click the **Actions** menu (three dots) next to the user’s name and select **Remove from organization**. A confirmation dialog will appear to prevent accidental removals.

4. Review the confirmation dialog to ensure you’re removing the correct user. Click **Remove user** to confirm. The user will immediately lose access to the organization and its resources.

---
# DOCUMENT BOUNDARY
---

# Create organizations

> Ways the organizations are created in Scalekit

An Organization enables shared data access and enforces consistent authentication methods, session policies, and access control policies for all its members. Scalekit supports two main approaches to organization creation:

1. **Sign up creates organizations automatically**: When users successfully authenticate with your app, Scalekit automatically creates an organization for them.
2. **User creates organizations themselves**: When your application provides users with the option to create new organizations themselves. For instance, Jira enables users to create their own workspaces.

## Sign up creates organizations automatically

[Section titled “Sign up creates organizations automatically”](#sign-up-creates-organizations-automatically)

Existing [Scalekit integration](/authenticate/fsa/quickstart/) to authenticate users and handle the login flow automatically generates an organization for each user. The organization ID associated with the user will be included in both the ID token and access token.

* Decoded ID token

  ID token decoded

  ```json
  1
  {
  2
    "at_hash": "ec_jU2ZKpFelCKLTRWiRsg", // Access token hash for validation
  12 collapsed lines
  3
    "aud": [
  4
      "skc_58327482062864390" // Audience (your client ID)
  5
    ],
  6
    "azp": "skc_58327482062864390", // Authorized party (your client ID)
  7
    "c_hash": "6wMreK9kWQQY6O5R0CiiYg", // Authorization code hash
  8
    "client_id": "skc_58327482062864390", // Your application's client ID
  9
    "email": "john.doe@example.com", // User's email address
  10
    "email_verified": true, // Whether the user's email is verified
  11
    "exp": 1742975822, // Expiration time (Unix timestamp)
  12
    "family_name": "Doe", // User's last name
  13
    "given_name": "John", // User's first name
  14
    "iat": 1742974022, // Issued at time (Unix timestamp)
  15
    "iss": "https://scalekit-z44iroqaaada-dev.scalekit.cloud", // Issuer (Scalekit environment URL)
  16
    "name": "John Doe", // User's full name
  17
    "oid": "org_59615193906282635", // Organization ID
  18
    "sid": "ses_65274187031249433", // Session ID
  19
    "sub": "usr_63261014140912135" // Subject (user's unique ID)
  20
  }
  ```

* Decoded access token

  Decoded access token

  ```json
  1
  {
  2
      "aud": [
  3
        "prd_skc_7848964512134X699" // Audience (API or resource server)
  4
      ],
  5
      "client_id": "prd_skc_7848964512134X699", // Your application's client ID
  6
      "oid": "org_89678001X21929734", // Organization ID
  7
      "exp": 1758265247, // Expiration time (Unix timestamp)
  8
      "iat": 1758264947, // Issued at time (Unix timestamp)
  10 collapsed lines
  9
      "iss": "https://login.devramp.ai", // Issuer (Scalekit environment URL)
  10
      "jti": "tkn_90928731115292X63", // JWT ID (unique token identifier)
  11
      "nbf": 1758264947, // Not before time (Unix timestamp)
  12
      "permissions": [ // Scopes or permissions granted
  13
        "workspace_data:write",
  14
        "workspace_data:read"
  15
      ],
  16
      "roles": [ // User roles within the organization
  17
        "admin"
  18
      ],
  19
      "sid": "ses_90928729571723X24", // Session ID
  20
      "sub": "usr_8967800122X995270", // Subject (user's unique ID)
  21
  }
  ```

## Allow users to create organizations API

[Section titled “Allow users to create organizations ”](#allow-users-to-create-organizations--)

Applications often provide options for users to create their own organizations.

For example, show an option for users such “Create new workspace” within their app. Use the Scalekit SDK to power such options:

* Node.js

  Create and manage organizations

  ```javascript
  const { organization } = await scalekit.organization.createOrganization(
    'Orion Analytics'
  );


  // Use case: Sync organization profile to downstream systems
  const { organization: fetched } = await scalekit.organization.getOrganization(organization.id);
  ```

* Python

  Create and manage organizations

  ```python
  from scalekit.v1.organizations.organizations_pb2 import CreateOrganization


  response = scalekit_client.organization.create_organization(
      CreateOrganization(
          display_name="Orion Analytics",
      )
  )


  # Use case: Sync organization profile to downstream systems
  fetched = scalekit_client.organization.get_organization(response[0].organization.id)
  ```

* Go

  Create and manage organizations

  ```go
  created, err := scalekitClient.Organization().CreateOrganization(
      ctx,
      "Orion Analytics",
      scalekit.CreateOrganizationOptions{},
  )
  if err != nil {
      log.Fatalf("create organization: %v", err)
  }


  // Use case: Sync organization profile to downstream systems
  fetched, err := scalekitClient.Organization().GetOrganization(ctx, created.Organization.Id)
  if err != nil {
      log.Fatalf("get organization: %v", err)
  }
  ```

* Java

  Create and manage organizations

  ```java
  // Use case: Provision a workspace after a sales-assisted onboarding
  CreateOrganization createOrganization = CreateOrganization.newBuilder()
      .setDisplayName("Orion Analytics")
      .build();


  Organization organization = scalekitClient.organizations().create(createOrganization);


  // Use case: Sync organization profile to downstream systems
  Organization fetched = scalekitClient.organizations().getById(organization.getId());
  ```

### Set organization properties at creation

[Section titled “Set organization properties at creation”](#set-organization-properties-at-creation)

Pass additional properties when you create an organization to give it a slug, map it to your own system, or brand its hosted pages. Every property except the display name is optional.

| Property       | Description                                                                                                                                                  |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `display_name` | Human-readable name shown in the dashboard and hosted UI. Required.                                                                                          |
| `slug`         | DNS-safe identifier such as `orion` or `auth.orion-analytics.com`. Expands `{{org_slug}}` in [template redirect URLs](/guides/dashboard/org-redirect-urls/). |
| `external_id`  | Your own identifier for the organization, so you can look it up by your system’s ID.                                                                         |
| `logo_url`     | Publicly accessible logo URL shown on [branded login pages and portals](/fsa/guides/organization-branding/).                                                 |

* Node.js

  Create an organization with properties

  ```javascript
  const { organization } = await scalekit.organization.createOrganization(
    'Orion Analytics',
    {
      slug: 'orion',
      externalId: 'customer_12345',
      logoUrl: 'https://cdn.orion-analytics.com/logo.png',
    }
  );
  ```

* Python

  Create an organization with properties

  ```python
  from scalekit.v1.organizations.organizations_pb2 import CreateOrganization


  response = scalekit_client.organization.create_organization(
      CreateOrganization(
          display_name="Orion Analytics",
          slug="orion",
          external_id="customer_12345",
          logo_url="https://cdn.orion-analytics.com/logo.png",
      )
  )
  ```

* Go

  Create an organization with properties

  ```go
  1
  slug := "orion"
  2
  externalID := "customer_12345"
  3
  logoURL := "https://cdn.orion-analytics.com/logo.png"
  4


  5
  created, err := scalekitClient.Organization().CreateOrganization(
  6
      ctx,
  7
      "Orion Analytics",
  8
      scalekit.CreateOrganizationOptions{
  9
          Slug:       &slug,
  10
          ExternalId: &externalID,
  11
          LogoUrl:    &logoURL,
  12
      },
  13
  )
  14
  if err != nil {
  15
      log.Fatalf("create organization: %v", err)
  16
  }
  ```

* Java

  Create an organization with properties

  ```java
  CreateOrganization createOrganization = CreateOrganization.newBuilder()
      .setDisplayName("Orion Analytics")
      .setSlug("orion")
      .setExternalId("customer_12345")
      .setLogoUrl("https://cdn.orion-analytics.com/logo.png")
      .build();


  Organization organization = scalekitClient.organizations().create(createOrganization);
  ```

Set a slug for friendly redirect URLs

A `slug` gives each organization a DNS-safe identifier that expands `{{org_slug}}` in [template redirect URLs](/guides/dashboard/org-redirect-urls/). You can also set or change it later with `updateOrganization`.

Next, let’s look at how users can be added to organizations.

---
# DOCUMENT BOUNDARY
---

# Customize user profiles

> Tailor user profiles to your business needs by creating and managing user profile attributes in Scalekit

User profiles in Scalekit provide essential identity information through standard attributes like email, name, and phone number. However, when your application requires business-specific data such as employee IDs, department codes, or access levels, you need more flexibility. T

This guide shows how to extend user profiles with custom attributes that can be created through the dashboard, managed programmatically via API, and synchronized with enterprise identity providers.

#### Standard user profile attributes

[Section titled “Standard user profile attributes”](#standard-user-profile-attributes)

Let’s start by looking at the existing standard attributes in a `user_profile` from the Scalekit’s [Get User API](https://docs.scalekit.com/apis/#tag/users/GET/api/v1/users/%7Bid%7D) response.

```json
{
  "id": "usp_96194455173923084",     // Unique user identifier
  "first_name": "John",              // User's given name
  "last_name": "Doe",                // User's family name
  "name": "John Doe",                // Full name for UI display
  "locale": "en-US",                 // User's language and region preference
  "email_verified": true,            // Whether the email address has been confirmed
  "phone_number": "+14155552671",    // Contact phone number
2 collapsed lines
  "metadata": { }, // Additional, non-structured user data
  "custom_attributes": {} // Business-specific user data
}
```

These attributes are also listed in your Scalekit dashboard. Navigate to **Dashboard** > **User Attributes** to see them. Let’s see how we can create a custom attribute.

## Create custom attributes

[Section titled “Create custom attributes”](#create-custom-attributes)

To add a custom attribute

1. Navigate to **Dashboard** > **User Attributes** and click **Add Attribute**.

2. Configure the new attribute fields:

   * **Display name** - Human-readable label shown in the dashboard (e.g., “Employee Number”)
   * **Attribute key** - Internal field name for API and SDK access (e.g., `employee_id`)

3. The new attribute can be used to attach the new information about the user to their user profile.

```diff
{
  "id": "usp_96194455173923084",     // Unique user identifier
  "first_name": "John",              // User's given name
  "last_name": "Doe",                // User's family name
  "name": "John Doe",                // Full name for UI display
  "locale": "en-US",                 // User's language and region preference
  "email_verified": true,            // Whether the email address has been confirmed
  "phone_number": "+14155552671",    // Contact phone number
  "metadata": { }, // Additional, non-structured user data
  "custom_attributes": {
    "pin_number": "123456"
  }
}
```

Custom attributes are user profile extensions that can be precisely configured to meet your application’s unique needs. For example, as a logistics platform, you might define custom attributes to capture critical operational details like delivery ZIP codes, service zones, or fleet vehicle specifications that apply all your users.

## Map profile attributes to identity providers

[Section titled “Map profile attributes to identity providers”](#map-profile-attributes-to-identity-providers)

When users authenticate through Single Sign-On (SSO) or join an organization, Scalekit can retrieve and transfer user profile information from the identity provider directly to your application via the ID token during the [login completion](/authenticate/fsa/complete-login/) process.

![Custom user attribute flow: a zip_code attribute passes from the customer's identity provider through Scalekit to your app](/d2/docs/authenticate/manage-users-orgs/custom-user-attributes-0.svg)

Administrators can configure attribute mapping from their identity provider by selecting specific user profile attributes. This mapping supports both standard and custom attributes seamlessly.

Note

Scalekit supports attribute mapping from directory providers to user profile attributes through SCIM Provisioning. Contact our sales team to learn more about enabling this advanced synchronization feature.

## Modify user profile attributes API

[Section titled “Modify user profile attributes ”](#modify-user-profile-attributes-)

If your application provides a user interface for users to view and modify their profile details directly within the app, the Scalekit API enables seamless profile attribute updates.

* cURL

  ```sh
  curl -L -X PATCH '/api/v1/users/' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ...2QA' \
  -d '{
    "user_profile": {
      "custom_attributes": {
        "zip_code": "90210"
      }
    }
  }'
  ```

* Node.js

  Update user profile with custom attributes

  ```javascript
  // Use case: Update user profile with a custom zip code attribute
  await scalekit.user.updateUser("", {
    userProfile: {
      customAttributes: {
        zip_code: "11120",
      },
      firstName: "John",
      lastName: "Doe",
      locale: "en-US",
      name: "John Michael Doe",
      phoneNumber: "+14155552671"
    }
  });
  ```

* Python

  Update user profile with custom attributes

  ```python
  # Use case: Update user profile with a custom zip code attribute
  scalekit.user.update_user(
      "",
      user_profile={
          "custom_attributes": {
              "zip_code": "11120"
          },
          "first_name": "John",
          "last_name": "Doe",
          "locale": "en-US",
          "name": "John Michael Doe",
          "phone_number": "+14155552671"
      }
  )
  ```

* Go

  Update user profile with custom attributes

  ```go
  // Use case: Update user profile with a custom zip code attribute
  updateUser := &usersv1.UpdateUser{
      UserProfile: &usersv1.UpdateUserProfile{
          CustomAttributes: map[string]string{
              "zip_code": "11120",
          },
          FirstName:   "John",
          LastName:    "Doe",
          Locale:      "en-US",
          Name:        "John Michael Doe",
          PhoneNumber: "+14155552671",
      },
  }


  updatedUser, err := scalekitClient.User().UpdateUser(ctx, "", updateUser)
  ```

* Java

  Update user profile with custom attributes

  ```java
  1
  // Use case: Update user profile with a custom zip code attribute
  2
  UpdateUser updateUser = UpdateUser.newBuilder()
  3
      .setUserProfile(
  4
          UpdateUserProfile.newBuilder()
  5
              .putCustomAttributes("zip_code", "11120")
  6
              .setFirstName("John")
  7
              .setLastName("Doe")
  8
              .setLocale("en-US")
  9
              .setName("John Michael Doe")
  10
              .setPhoneNumber("+14155552671")
  11
              .build())
  12
      .build();
  13


  14
  UpdateUserRequest updateReq = UpdateUserRequest.newBuilder()
  15
      .setUser(updateUser)
  16
      .build();
  17


  18
  User updatedUser = scalekitClient.users().updateUser("", updateReq);
  ```

## Link your system identifiers & metadata

[Section titled “Link your system identifiers & metadata”](#link-your-system-identifiers--metadata)

Beyond user profile attributes, you can link your systems with Scalekit to easily map, identify and store more context about organizations and users.

This may be helpful when:

* You are migrating from an existing system and need to keep your existing identifiers
* You are integrating with multiple platforms and need to maintain data consistency
* You need to simplify integration by avoiding complex ID mapping between your systems and Scalekit

## Organization external IDs for system integration

[Section titled “Organization external IDs for system integration”](#organization-external-ids-for-system-integration)

External IDs let you identify organizations using your own identifiers instead of Scalekit’s generated IDs. This is essential when migrating from existing systems or integrating with multiple platforms.

1. #### Set external IDs during organization creation

   [Section titled “Set external IDs during organization creation”](#set-external-ids-during-organization-creation)

   Include your system’s identifier when creating organizations to maintain consistent references across your infrastructure.

   * Node.js

     Create organization with external ID

     ```javascript
     // During user signup or organization creation
     const { organization } = await scalekit.organization.createOrganization('Acme Corporation', {
       externalId: 'CUST-12345-ACME' // Your customer ID in your database
     });


     console.log('Organization created:', organization.id);
     console.log('Your ID:', organization.externalId);
     ```

   * Python

     Create organization with external ID

     ```python
     from scalekit.v1.organizations.organizations_pb2 import CreateOrganization


     # During user signup or organization creation
     organization = CreateOrganization(
         display_name='Acme Corporation',
         external_id='CUST-12345-ACME'  # Your customer ID in your database
     )
     response = scalekit_client.organization.create_organization(organization=organization)
     created_organization = response[0].organization


     print(f'Organization created: {created_organization.id}')
     print(f'Your ID: {created_organization.external_id}')
     ```

   * Go

     Create organization with external ID

     ```go
     // During user signup or organization creation
     resp, err := scalekitClient.Organization().CreateOrganization(ctx, "Acme Corporation", scalekit.CreateOrganizationOptions{
         ExternalId: "CUST-12345-ACME", // Your customer ID in your database
     })
     if err != nil {
         log.Fatal(err)
     }


     org := resp.GetOrganization()
     fmt.Printf("Organization created: %s\n", org.GetId())
     fmt.Printf("Your ID: %s\n", org.GetExternalId())
     ```

   * Java

     Create organization with external ID

     ```java
     // During user signup or organization creation
     CreateOrganization createOrg = CreateOrganization.newBuilder()
         .setDisplayName("Acme Corporation")
         .setExternalId("CUST-12345-ACME") // Your customer ID in your database
         .build();


     Organization organization = scalekitClient.organizations().create(createOrg);


     System.out.println("Organization created: " + organization.getId());
     System.out.println("Your ID: " + organization.getExternalId());
     ```

2. ### Find organizations using your IDs

   [Section titled “Find organizations using your IDs”](#find-organizations-using-your-ids)

   Use external IDs to quickly locate organizations when processing webhooks, handling customer support requests, or syncing data between systems.

   * Node.js

     Find organization by external ID

     ```javascript
     // When processing a webhook or customer update
     const customerId = 'CUST-12345-ACME'; // From your webhook payload


     const { organization } = await scalekit.organization.getOrganizationByExternalId(customerId);


     if (organization) {
       console.log('Found organization:', organization.displayName);
       // Process organization updates, sync data, etc.
     }
     ```

   * Python

     Find organization by external ID

     ```python
     # When processing a webhook or customer update
     customer_id = 'CUST-12345-ACME'  # From your webhook payload


     response = scalekit_client.organization.get_organization_by_external_id(customer_id)
     organization = response[0].organization


     if organization:
         print(f'Found organization: {organization.display_name}')
         # Process organization updates, sync data, etc.
     ```

   * Go

     Find organization by external ID

     ```go
     // When processing a webhook or customer update
     customerId := "CUST-12345-ACME" // From your webhook payload


     resp, err := scalekitClient.Organization().GetOrganizationByExternalId(ctx, customerId)
     if err != nil {
         log.Printf("Error finding organization: %v", err)
         return
     }


     org := resp.GetOrganization()
     if org != nil {
         fmt.Printf("Found organization: %s\n", org.GetDisplayName())
         // Process organization updates, sync data, etc.
     }
     ```

   * Java

     Find organization by external ID

     ```java
     // When processing a webhook or customer update
     String customerId = "CUST-12345-ACME"; // From your webhook payload


     Organization organization = scalekitClient.organizations().getByExternalId(customerId);


     if (organization != null) {
         System.out.println("Found organization: " + organization.getDisplayName());
         // Process organization updates, sync data, etc.
     }
     ```

3. ### Update external IDs when needed

   [Section titled “Update external IDs when needed”](#update-external-ids-when-needed)

   If your customer IDs change or you need to migrate identifier formats, you can update external IDs for existing organizations.

   * Node.js

     Update external ID

     ```javascript
     const { organization: updatedOrg } = await scalekit.organization.updateOrganization(organizationId, {
       externalId: 'NEW-CUST-12345-ACME'
     });


     console.log('External ID updated:', updatedOrg.externalId);
     ```

   * Python

     Update external ID

     ```python
     from scalekit.v1.organizations.organizations_pb2 import UpdateOrganization


     update_organization = UpdateOrganization(external_id='NEW-CUST-12345-ACME')
     response = scalekit_client.organization.update_organization(
         organization_id=organization_id, organization=update_organization
     )
     updated_org = response[0].organization


     print(f'External ID updated: {updated_org.external_id}')
     ```

   * Go

     Update external ID

     ```go
     newExternalId := "NEW-CUST-12345-ACME"
     resp, err := scalekitClient.Organization().UpdateOrganization(ctx, organizationId, &organizationsv1.UpdateOrganization{
         ExternalId: &newExternalId,
     })
     if err != nil {
         log.Fatal(err)
     }


     updatedOrg := resp.GetOrganization()
     fmt.Printf("External ID updated: %s\n", updatedOrg.GetExternalId())
     ```

   * Java

     Update external ID

     ```java
     UpdateOrganization updateOrg = UpdateOrganization.newBuilder()
         .setExternalId("NEW-CUST-12345-ACME")
         .build();


     Organization updatedOrg = scalekitClient.organizations().updateById(organizationId, updateOrg);


     System.out.println("External ID updated: " + updatedOrg.getExternalId());
     ```

## User external IDs and metadata

[Section titled “User external IDs and metadata”](#user-external-ids-and-metadata)

Just as organizations need external identifiers, users often require integration with existing systems. User external IDs and metadata work similarly to organization identifiers, enabling you to link Scalekit users with your CRM, HR systems, and other business applications.

### When to use user external IDs and metadata

[Section titled “When to use user external IDs and metadata”](#when-to-use-user-external-ids-and-metadata)

**External IDs** link Scalekit users to your existing systems:

* Reference users in your database, CRM, or billing system
* Maintain consistent user identification across multiple platforms
* Enable easy data synchronization and lookups

**Metadata** stores additional user attributes:

* Organizational information (department, location, role level)
* Business context (territory, quota, access permissions)
* Integration data (external system IDs, custom properties)

### Set user external IDs and metadata during user creation

[Section titled “Set user external IDs and metadata during user creation”](#set-user-external-ids-and-metadata-during-user-creation)

* Node.js

  Create user with external ID and metadata

  ```diff
  1
  // Use case: Create user during system migration or bulk import with existing system references
  2
  const { user } = await scalekit.user.createUserAndMembership("", {
  3
    email: "john.doe@company.com",
  4
    externalId: "SALESFORCE-003921",
  5
    metadata: {
  6
      department: "Sales",
  7
      employeeId: "EMP-002",
  8
      territory: "West Coast",
  9
      quota: 150000,
  10
      crmAccountId: "ACC-789",
  11
      hubspotContactId: "12345",
  12
  +  },
  13
    userProfile: {
  14
      firstName: "John",
  15
      lastName: "Doe",
  16
    },
  17
    sendInvitationEmail: true,
  18
  });
  ```

* Python

  Create user with external ID and metadata

  ```diff
  1
  # Use case: Create user during system migration or bulk import with existing system references
  2
  user_response = scalekit.user.create_user_and_membership(
  3
      "",
  4
      +email="john.doe@company.com",
  5
      +external_id="SALESFORCE-003921",
  6
      +metadata={
  7
          "department": "Sales",
  8
          "employee_id": "EMP-002",
  9
          "territory": "West Coast",
  10
          "quota": 150000,
  11
          "crm_account_id": "ACC-789",
  12
          "hubspot_contact_id": "12345"
  13
      },
  14
      user_profile={
  15
          "first_name": "John",
  16
          "last_name": "Doe"
  17
      },
  18
      send_invitation_email=True
  19
  )
  ```

* Go

  Create user with external ID and metadata

  ```diff
  1
  // Use case: Create user during system migration or bulk import with existing system references
  2
  newUser := &usersv1.CreateUser{
  3
      Email: "john.doe@company.com",
  4
      +ExternalId: "SALESFORCE-003921",
  5
      +Metadata: map[string]string{
  6
          "department":          "Sales",
  7
          "employee_id":         "EMP-002",
  8
          "territory":           "West Coast",
  9
          "quota":               "150000",
  10
          "crm_account_id":      "ACC-789",
  11
          "hubspot_contact_id":  "12345",
  12
  +    },
  13
      UserProfile: &usersv1.CreateUserProfile{
  14
          FirstName: "John",
  15
          LastName:  "Doe",
  16
      },
  17
  }
  18
  userResp, err := scalekitClient.User().CreateUserAndMembership(
  19
      ctx,
  20
      "",
  21
      newUser,
  22
      true, // sendInvitationEmail
  23
  )
  ```

* Java

  Create user with external ID and metadata

  ```diff
  1
  // Use case: Create user during system migration or bulk import with existing system references
  2
  CreateUser createUser = CreateUser.newBuilder()
  3
      .setEmail("john.doe@company.com")
  4
  +    .setExternalId("SALESFORCE-003921")
  5
  +    .putMetadata("department", "Sales")
  6
  +    .putMetadata("employee_id", "EMP-002")
  7
  +    .putMetadata("territory", "West Coast")
  8
  +    .putMetadata("quota", "150000")
  9
  +    .putMetadata("crm_account_id", "ACC-789")
  10
  +    .putMetadata("hubspot_contact_id", "12345")
  11
  +    .setUserProfile(
  12
          +CreateUserProfile.newBuilder()
  13
              .setFirstName("John")
  14
              .setLastName("Doe")
  15
              .build())
  16
      .build();
  17


  18
  CreateUserAndMembershipRequest createUserReq = CreateUserAndMembershipRequest.newBuilder()
  19
      .setUser(createUser)
  20
      .setSendInvitationEmail(true)
  21
      .build();
  22


  23
  CreateUserAndMembershipResponse userResp = scalekitClient.users()
  24
      .createUserAndMembership("", createUserReq);
  ```

### Update user external IDs and metadata for existing users

[Section titled “Update user external IDs and metadata for existing users”](#update-user-external-ids-and-metadata-for-existing-users)

* Node.js

  Update user external ID and metadata

  ```diff
  // Use case: Link user with external systems (CRM, HR) and track custom attributes in a single call
  const updatedUser = await scalekit.user.updateUser("", {
    externalId: "SALESFORCE-003921",
    metadata: {
      department: "Sales",
      employeeId: "EMP-002",
      territory: "West Coast",
      quota: 150000,
      crmAccountId: "ACC-789",
      hubspotContactId: "12345",
  +  },
  });
  ```

* Python

  Update user external ID and metadata

  ```diff
  # Use case: Link user with external systems (CRM, HR) and track custom attributes in a single call
  updated_user = scalekit.user.update_user(
      "",
      +external_id="SALESFORCE-003921",
      +metadata={
          "department": "Sales",
          "employee_id": "EMP-002",
          "territory": "West Coast",
          "quota": 150000,
          "crm_account_id": "ACC-789",
          "hubspot_contact_id": "12345"
      }
  )
  ```

* Go

  Update user external ID and metadata

  ```go
  1
  // Use case: Link user with external systems (CRM, HR) and track custom attributes in a single call
  2
  updateUser := &usersv1.UpdateUser{
  3
      ExternalId: "SALESFORCE-003921",
  4
      Metadata: map[string]string{
  5
          "department":          "Sales",
  6
          "employee_id":         "EMP-002",
  7
          "territory":           "West Coast",
  8
          "quota":               "150000",
  9
          "crm_account_id":      "ACC-789",
  10
          "hubspot_contact_id":  "12345",
  11
      },
  12
  }
  13
  updatedUser, err := scalekitClient.User().UpdateUser(
  14
      ctx,
  15
      "",
  16
      updateUser,
  17
  )
  ```

* Java

  Update user external ID and metadata

  ```java
  1
  // Use case: Link user with external systems (CRM, HR) and track custom attributes in a single call
  2
  UpdateUser updateUser = UpdateUser.newBuilder()
  3
      .setExternalId("SALESFORCE-003921")
  4
      .putMetadata("department", "Sales")
  5
      .putMetadata("employee_id", "EMP-002")
  6
      .putMetadata("territory", "West Coast")
  7
      .putMetadata("quota", "150000")
  8
      .putMetadata("crm_account_id", "ACC-789")
  9
      .putMetadata("hubspot_contact_id", "12345")
  10
      .build();
  11


  12
  UpdateUserRequest updateReq = UpdateUserRequest.newBuilder()
  13
      .setUser(updateUser)
  14
      .build();
  15


  16
  User updatedUser = scalekitClient.users().updateUser("", updateReq);
  ```

### Find users by external ID

[Section titled “Find users by external ID”](#find-users-by-external-id)

* Node.js

  Find user by external ID

  ```javascript
  // Use case: Look up Scalekit user when you have your system's user ID
  const user = await scalekit.user.getUserByExternalId("", "SALESFORCE-003921");
  console.log(`Found user: ${user.email} with ID: ${user.id}`);
  ```

* Python

  Find user by external ID

  ```python
  # Use case: Look up Scalekit user when you have your system's user ID
  user = scalekit.user.get_user_by_external_id("", "SALESFORCE-003921")
  print(f"Found user: {user['email']} with ID: {user['id']}")
  ```

* Go

  Find user by external ID

  ```go
  // Use case: Look up Scalekit user when you have your system's user ID
  user, err := scalekitClient.User().GetUserByExternalId(
      ctx,
      "",
      "SALESFORCE-003921",
  )
  if err != nil {
      log.Printf("User not found: %v", err)
  } else {
      fmt.Printf("Found user: %s with ID: %s\n", user.Email, user.Id)
  }
  ```

* Java

  Find user by external ID

  ```java
  // Use case: Look up Scalekit user when you have your system's user ID
  try {
      GetUserByExternalIdResponse response = scalekitClient.users()
          .getUserByExternalId("", "SALESFORCE-003921");


      User user = response.getUser();
      System.out.printf("Found user: %s with ID: %s%n", user.getEmail(), user.getId());
  } catch (Exception e) {
      System.err.println("User not found: " + e.getMessage());
  }
  ```

This integration approach maintains consistent user identity across your system architecture while letting you choose the source of truth for authentication and authorization. Both user and organization external IDs work together to provide complete system integration capabilities.

---
# DOCUMENT BOUNDARY
---

# Delete users and organizations

> Trigger deletions and let Scalekit handle sessions, memberships, and cleanup automatically

Properly deleting users and organizations is essential for security and regulatory compliance. Whether a user departs or an entire organization must be removed, it’s important to have reliable deletion processes in place. This guide shows you how to implement deletion for both users and organizations.

Provide a feature for administrators to permanently delete a user account. This is useful for handling user account closures, GDPR deletion requests, or cleaning up test accounts.

Note

Before permanent deletion, confirm this is the intended action. If you only need to revoke a user’s access to an organization while preserving their account, [remove the user from the organization](/authenticate/manage-organizations/remove-users-from-organization/) instead.

1. ## Delete a user

   [Section titled “Delete a user”](#delete-a-user)

   Call the `deleteUser` method with the user’s ID:

   * Node.js

     Delete a user permanently

     ```javascript
     // Use case: User account closure, GDPR deletion requests, or cleaning up test accounts
     await scalekit.user.deleteUser("usr_123");
     ```

   * Python

     Delete a user permanently

     ```python
     # Use case: User account closure, GDPR deletion requests, or cleaning up test accounts
     scalekit_client.users.delete_user(
         user_id="usr_123"
     )
     ```

   * Go

     Delete a user permanently

     ```go
     // Use case: User account closure, GDPR deletion requests, or cleaning up test accounts
     if err := scalekitClient.User().DeleteUser(ctx, "usr_123"); err != nil {
         panic(err)
     }
     ```

   * Java

     Delete a user permanently

     ```java
     // Use case: User account closure, GDPR deletion requests, or cleaning up test accounts
     scalekitClient.users().deleteUser("usr_123");
     ```

   When you delete a user, Scalekit performs the following actions:

   * Terminates all of the user’s active sessions.
   * Removes all of the user’s organization memberships.
   * Permanently deletes the user account.

2. ## Delete an organization

   [Section titled “Delete an organization”](#delete-an-organization)

   Provide a feature for users to delete organizations they own. This is useful for company closures, account restructuring, or removing test organizations.

   Call the `deleteOrganization` method with the organization’s ID:

   * Node.js

     Delete an organization permanently

     ```javascript
     // Use case: Company closure, account restructuring, or removing test organizations
     await scalekit.organization.deleteOrganization(organizationId);
     ```

   * Python

     Delete an organization permanently

     ```python
     # Use case: Company closure, account restructuring, or removing test organizations
     scalekit_client.organization.delete_organization(organization_id)
     ```

   * Go

     Delete an organization permanently

     ```go
     // Use case: Company closure, account restructuring, or removing test organizations
     err := scalekitClient.Organization().DeleteOrganization(
       ctx,
       organizationId
     )
     if err != nil {
         panic(err)
     }
     ```

   * Java

     Delete an organization permanently

     ```java
     // Use case: Company closure, account restructuring, or removing test organizations
     scalekitClient.organizations().deleteById(organizationId);
     ```

   When you delete an organization, Scalekit performs the following actions:

   * Terminates active sessions for all organization members.
   * Removes all user memberships from the organization.
   * Permanently removes all organization data and settings.
   * **Cascading deletion**: If a user is a member of only this organization, their account is also permanently deleted.
   * Users who are members of other organizations retain their accounts and access.

Permanent deletion cannot be undone

* Ensure you have appropriate backups and audit trails in your system before deleting a user.
* If your organization has data retention policies, consider implementing a soft delete. Schedule the permanent deletion for a future date (e.g., 30-60 days) to allow for data backup and user notifications.

---
# DOCUMENT BOUNDARY
---

# Configure email domain rules

> Set up allowed domains for organization auto-join and configure restrictions for generic and disposable email sign-ups

Email domain rules control how users join your application in two ways: by restricting who can sign up and by enabling automatic organization membership for trusted domains. These rules help maintain data quality, prevent abuse, and streamline onboarding for enterprise customers.

Sign-up restrictions block registrations and invitations from generic email providers (like Gmail or Outlook) and disposable email services, ensuring your user base consists of verified business contacts. Allowed email domains enable users with matching email addresses to automatically join organizations through the organization switcher, reducing manual invitation overhead.

Together, these features give you fine-grained control over user addition, blocking unwanted sign-ups while facilitating seamless access for legitimate users from trusted domains.

## Set up sign-up restrictions

[Section titled “Set up sign-up restrictions”](#set-up-sign-up-restrictions)

Sign-up restrictions help you maintain data quality and prevent abuse by controlling who can create accounts in your application. This is particularly important for B2B applications where you need to ensure users have legitimate business email addresses rather than personal or temporary accounts.

These restrictions automatically block registrations and invitations from two types of email addresses:

* **Generic email domains** - Public email providers like `@gmail.com`, `@outlook.com`, or `@yahoo.com` that anyone can use
* **Disposable email addresses** - Temporary email services often used for spam, trial abuse, or avoiding accountability

When enabled, these restrictions apply to both direct signups and organization invitations, ensuring consistent policy enforcement across your application. This prevents users from creating multiple trial accounts, maintains clean analytics, and ensures your user base consists of verified business contacts.

The following diagram illustrates how sign-up restrictions work:

![Sign-up restrictions: Scalekit blocks sign-ups and invitations from restricted email domains](/d2/docs/authenticate/manage-users-orgs/email-domain-rules-0.svg)

### How restrictions affect invitations

[Section titled “How restrictions affect invitations”](#how-restrictions-affect-invitations)

* Any user with a disposable email domain cannot sign up to create a new organization and cannot be invited to any existing organization.
* Any user with a public email domain cannot sign up to create a new organization and cannot be invited to any existing organization.

### Set sign-up restrictions

[Section titled “Set sign-up restrictions”](#set-sign-up-restrictions)

1. ### Navigate to sign-up restrictions settings

   [Section titled “Navigate to sign-up restrictions settings”](#navigate-to-sign-up-restrictions-settings)

   Go to **Dashboard > Authentication > General** and locate the sign-up restrictions section.

2. ### Configure restriction options

   [Section titled “Configure restriction options”](#configure-restriction-options)

   Toggle the following options based on what suits your application:

   * **Block disposable email domains**: Prevents temporary/disposable email addresses from signing up or being invited
   * **Block public email domains**: Prevents generic email providers like Gmail, Outlook, Yahoo from creating organizations

   ![Authentication General tab showing Sign-up Settings with toggles to block sign-up for public email domains and disposable email domains](/.netlify/images?url=_astro%2Fui.D6G2x64L.png\&w=2858\&h=1611\&dpl=6ae471fc143c9a4c27550463)

   Choosing the right restrictions

   Enable disposable email blocking for all production applications to prevent abuse. Only enable public email blocking if you’re building a B2B application that requires verified business identities.

3. ### Save your settings

   [Section titled “Save your settings”](#save-your-settings)

   Click **Save** to apply the restrictions. Changes take effect immediately for all new signups and invitations.

   Note

   Existing users with restricted email domains remain unaffected. You can return to this section anytime to update your restrictions.

## Configure allowed email domains

[Section titled “Configure allowed email domains”](#configure-allowed-email-domains)

Allowed email domains lets organization admins define trusted domains for their organization. When a user signs in or signs up with a matching email domain, Scalekit suggests the user to join that organization in the **organization switcher** so the user can join the organization with one click. This feature is authentication-method agnostic: regardless of whether a user authenticates via SSO, social login, or passwordless authentication, organization options are suggested based on their email domain.

![Allowed email domains: a user whose domain matches is suggested their organization, otherwise they join or create one](/d2/docs/authenticate/manage-users-orgs/email-domain-rules-1.svg)

When a user signs up or signs in, Scalekit will automatically:

1. **Match email domains** - Check if the user’s email domain matches configured allowed domains for any organization.
2. **Suggest organization options** - Show the user available organizations they can join through an organization switcher.
3. **Enable user choice** - Allow users to decide which of the suggested organizations they want to join.
4. **Create organization membership** - Automatically add the user to their selected organization.

Security consideration

Disposable and public email domains are blocked and cannot be added to the allow-list (e.g., `gmail.com`, `outlook.com`). We maintain a blocklist to enforce this.

### Manage allowed email domains in Scalekit Dashboard

[Section titled “Manage allowed email domains in Scalekit Dashboard”](#manage-allowed-email-domains-in-scalekit-dashboard)

Allowed email domains can be configured for an organization through the Scalekit Dashboard.

![Organization settings page with the Allowed email domains section listing acmecorp.com and an Add domain button](/.netlify/images?url=_astro%2Fdashboard.Cf5i9h8I.png\&w=2938\&h=1588\&dpl=6ae471fc143c9a4c27550463)

1. Navigate to **Organizations** and **select an organization**.
2. Navigate to **Overview** > **User Management** > **Allowed email domains**.
3. Add or edit allowed email domains for automatic suggestions/provisioning.

### Manage allowed email domains API

[Section titled “Manage allowed email domains ”](#manage-allowed-email-domains-)

Configure allowed email domains for an organization programmatically through the Scalekit API. Before proceeding, complete the steps in the [installation guide](/authenticate/set-up-scalekit/).

* cURL

  Register, list, get, and delete allowed email domains

  ```sh
  # 1. Register an allowed email domain
  # Use case: Restrict user registration to specific company domains for B2B applications
  curl 'https:///api/v1/organizations/{organization_id}/domains' \
    --request POST \
    --header 'Content-Type: application/json' \
    --data '{
    "domain": "customerdomain.com",
    "domain_type": "ALLOWED_EMAIL_DOMAIN"
  }'


  # 2. List all registered allowed email domains
  # Use case: Display domain restrictions in admin dashboard or verify current settings
  curl 'https:///api/v1/organizations/{organization_id}/domains'


  # 3. Get details of a specific domain
  # Use case: Verify domain configuration or retrieve domain metadata
  curl 'https:///api/v1/organizations/{organization_id}/domains/{domain_id}'


  # 4. Delete an allowed email domain
  # Use case: Remove domain restrictions or clean up unused configurations
  curl 'https:///api/v1/organizations/{organization_id}/domains/{domain_id}' \
    --request DELETE
  ```

* Nodejs

  Register, list, get, and delete allowed email domains

  ```js
  1
  // 1. Register an allowed email domain
  2
  // Use case: Restrict user registration to specific company domains for B2B applications
  3
  const newDomain = await scalekit.createDomain("org-123", "customerdomain.com", {
  4
    domainType: "ALLOWED_EMAIL_DOMAIN",
  5
  });
  6


  7
  // 2. List all registered allowed email domains
  8
  // Use case: Display domain restrictions in admin dashboard or verify current settings
  9
  const domains = await client.domain.listDomains(organizationId);
  10


  11
  // 3. Get details of a specific domain
  12
  // Use case: Verify domain configuration or retrieve domain metadata
  13
  const domain = await client.domain.getDomain(organizationId, domainId);
  14


  15
  // 4. Delete an allowed email domain
  16
  // Use case: Remove domain restrictions or clean up unused configurations
  17
  // Caution: Deletion is permanent and may affect user access
  18
  await client.domain.deleteDomain(organizationId, domainId);
  ```

---
# DOCUMENT BOUNDARY
---

# Hosted UI widgets

> Redirect users to a branded self-serve portal for organization settings, member management, SSO, SCIM, domains, and user profiles.

Redirect signed-in users from your application to Hosted UI widgets at `/ui/` so their workspace administrators can self-manage organizations, members, single sign-on (SSO), SCIM provisioning, domains, and profiles.

For example, add a link in your app’s settings or account area. Scalekit will use the active session and show users their workspace automatically.

Hosted UI widgets URL

```sh
/ui/  # https://your-app-env.scalekit.com/ui/
```

## When to use Hosted UI widgets

[Section titled “When to use Hosted UI widgets”](#when-to-use-hosted-ui-widgets)

Use Hosted UI widgets when you want customers to self-manage their organizations and accounts without you building custom UIs.

* **Advantages**: Enterprise customers can self-serve member management, SSO setup, SCIM provisioning, domain verification, session policy, and their own profiles. Permissions and organization features are handled automatically. The portal stays up to date with new Scalekit capabilities.
* **Trade-offs**: Customers leave your application UI and work in Scalekit-hosted pages. Branding follows your shared application customization settings. You send users to `/ui/`; they use portal navigation to open individual modules.

If you need a shareable link or iframe embed for enterprise IT administrators to configure SSO, SCIM, and organization domain verification, use the [Admin portal](/authenticate/manage-users-orgs/admin-portal/) instead. The Admin portal supports only those three modules. Hosted UI widgets are the better fit for signed-in users who open self-serve settings from inside your product.

![Hosted UI widgets: your B2B app redirects to Scalekit-hosted user widgets (profile, security) and organization widgets (settings, members, SSO, SCIM, domains, session policy)](/d2/docs/authenticate/manage-users-orgs/hosted-widgets-0.svg)

## Organization widgets

[Section titled “Organization widgets”](#organization-widgets)

Organization widgets let your customers manage their organization’s settings, members, and configurations. These widgets are access-controlled using Scalekit permissions and the features enabled for the organization. A widget appears only if the user has the required permissions and the organization has the corresponding feature enabled.

### Manage organization settings

[Section titled “Manage organization settings”](#manage-organization-settings)

Your customers can view and manage their organization profile, including allowed email domains. Navigate to **Organization settings** to update organization details.

![Organization settings widget showing the organization name field and a list of allowed email domains](/.netlify/images?url=_astro%2Forg_settings.XshZN6sS.png\&w=2936\&h=1592\&dpl=6ae471fc143c9a4c27550463)

### Manage organization members

[Section titled “Manage organization members”](#manage-organization-members)

Your customers can view organization members, invite new members, manage roles, and remove members from the organization. The **Member management** widget provides a complete view of their team.

![Member management widget listing organization users with their roles and active status, plus an Invite User button](/.netlify/images?url=_astro%2Forg_member.pe4fgTMu.png\&w=2936\&h=1592\&dpl=6ae471fc143c9a4c27550463)

### Configure SSO for the organization

[Section titled “Configure SSO for the organization”](#configure-sso-for-the-organization)

Your customers can set up and manage Single Sign-On for their organization. The widget includes a setup guide tailored to their identity provider, making it easy to connect their SSO connection.

Feature must be enabled

The SSO widget appears only if SSO is enabled for the organization. You can enable SSO in the Scalekit dashboard or using the [SDK](/authenticate/auth-methods/enterprise-sso/#enable-sso-for-the-organization).

![Single Sign-On widget prompting the user to choose an identity provider such as Okta, OneLogin, Google Workspace, or Entra ID](/.netlify/images?url=_astro%2Forg_sso.IHoRc3E6.png\&w=2936\&h=1592\&dpl=6ae471fc143c9a4c27550463)

### Configure SCIM for the organization

[Section titled “Configure SCIM for the organization”](#configure-scim-for-the-organization)

Your customers can set up and manage SCIM provisioning for their organization. The widget includes a setup guide tailored to their identity provider to automate user and group provisioning.

Feature must be enabled

The SCIM widget appears only if SCIM is enabled for the organization. You can enable SCIM in the Scalekit dashboard or using the [SDK](/guides/user-management/scim-provisioning/#enable-scim-provisioning-for-the-organization).

![SCIM provisioning widget prompting the user to select a directory provider such as Okta, OneLogin, Google Workspace, or Entra ID](/.netlify/images?url=_astro%2Forg_scim.CBDzga3B.png\&w=2936\&h=1592\&dpl=6ae471fc143c9a4c27550463)

### Verify organization domains

[Section titled “Verify organization domains”](#verify-organization-domains)

Your customers can add and verify the domains they own, enabling Home Realm Discovery and SCIM provisioning for their organization. [Learn more about organization domains](/authenticate/manage-users-orgs/organization-domains/).

After entering a domain, the widget displays the DNS TXT record to publish. Scalekit verifies ownership in the background and marks the domain as verified once the record propagates.

Feature must be enabled

The domain verification widget appears only if **Domain Verification** is enabled for the organization. You can enable Domain Verification in the Scalekit dashboard or via the [organization settings API](/apis/#tag/organizations/PATCH/api/v1/organizations/%7Bid%7D/settings).

![Domain verification via Hosted UI widgets](/.netlify/images?url=_astro%2F2026-05-26-19-06-40.DKn_DLNP.png\&w=2932\&h=1598\&dpl=6ae471fc143c9a4c27550463)

### Manage session policy

[Section titled “Manage session policy”](#manage-session-policy)

Your customers can view and configure their organization’s session policy, setting custom absolute and idle session timeouts that override your application defaults. Scalekit always enforces the stricter of the two.

Feature must be enabled

The session policy widget appears only if **Session Policy** is enabled for the organization. You can enable it in the Scalekit dashboard or using the [organization settings API](/apis/#tag/organizations/PATCH/api/v1/organizations/%7Bid%7D/settings).

![Session policy widget with Application and Custom options selected, showing absolute and idle session timeout settings](/.netlify/images?url=_astro%2F2026-05-21-17-12-14.OQW71QKW.png\&w=2920\&h=1570\&dpl=6ae471fc143c9a4c27550463)

## User widgets

[Section titled “User widgets”](#user-widgets)

User widgets let your customers manage their personal profile and security settings. These widgets are accessible to all authenticated users and are not controlled by organization features or Scalekit permissions.

### Manage profile

[Section titled “Manage profile”](#manage-profile)

Your customers can view and manage their personal profile information, including their name, email, and other account details.

![My Account widget showing editable first name and last name fields and a verified email address](/.netlify/images?url=_astro%2Fuser_profile.DF85cQEC.png\&w=2936\&h=1592\&dpl=6ae471fc143c9a4c27550463)

### Manage security

[Section titled “Manage security”](#manage-security)

Your customers can register and manage passkeys, view active sessions, and revoke sessions. The **User security** widget helps them maintain account security.

![User security widget showing a registered passkey and a list of active sessions with options to revoke them](/.netlify/images?url=_astro%2Fuser_security.B5SWg3po.png\&w=2936\&h=1592\&dpl=6ae471fc143c9a4c27550463)

## Access management

[Section titled “Access management”](#access-management)

Hosted UI widgets enforce access using **Scalekit permissions**. You can map these permissions to any application roles assigned to the end user. When a user accesses Hosted UI widgets, Scalekit checks their permissions and shows the available widgets.

| Permission                     | Purpose                                                |
| ------------------------------ | ------------------------------------------------------ |
| `sk_org_settings_read`         | View organization profile and settings                 |
| `sk_org_settings_manage`       | View and modify organization profile and settings      |
| `sk_org_users_read`            | View users in an organization                          |
| `sk_org_users_invite`          | Invite new users to an organization                    |
| `sk_org_users_delete`          | Remove users from an organization                      |
| `sk_org_users_role_change`     | Change roles of users in an organization               |
| `sk_org_sso_read`              | View SSO configuration for an organization             |
| `sk_org_sso_manage`            | View and modify SSO configuration for an organization  |
| `sk_org_scim_read`             | View SCIM configuration for an organization            |
| `sk_org_scim_manage`           | View and modify SCIM configuration for an organization |
| `sk_org_session_policy_read`   | View session policy for an organization                |
| `sk_org_session_policy_manage` | View and manage session policy for an organization     |

Default Admin and Member roles

Scalekit creates **Admin** and **Member** roles for every environment by default. Scalekit permissions are mapped to these two roles by default. The Admin role has all Scalekit permissions and can access all Hosted UI widgets enabled for the organization. The Member role has limited access to organization widgets and can only view organization settings and organization members. Both roles have access to user widgets.

You can customize the permission mapping for these roles or create a [custom role](/authenticate/authz/create-roles-permissions/) and assign Scalekit permissions to control access to Hosted UI widgets.

## Branding and customization

[Section titled “Branding and customization”](#branding-and-customization)

Hosted UI widgets use your application [branding](/fsa/guides/login-page-branding/) so the portal looks like an extension of your app. The same customization settings apply across hosted experiences, including the login page and [Admin portal](/authenticate/manage-users-orgs/admin-portal/). You can also apply [organization branding](/fsa/guides/organization-branding/) where that feature is enabled.

You can change the Hosted UI widgets URL to match your application URL by setting up a [custom domain](/guides/custom-domain/).

## Common scenarios

[Section titled “Common scenarios”](#common-scenarios)

What is the shared session between my app and Hosted UI widgets?

It is the same Scalekit session issued to your application for that user. After your [authentication flow](/authenticate/fsa/quickstart/), the browser holds Scalekit cookies for your environment or custom domain. Hosted UI widgets read those same cookies, so users do not sign in again when you redirect them to `/ui/`.

What happens if a user does not have a session?

Scalekit redirects the user to that Scalekit environment’s login page for their account. Send users to `/ui/` only after they complete your [authentication flow](/authenticate/fsa/quickstart/) so a Scalekit session cookie is available.

What happens when a user logs out from Hosted UI widgets?

Scalekit ends the Scalekit session and redirects the user to that Scalekit environment’s login page for their account (the hosted login screen on your environment or [custom domain](/guides/custom-domain/)). Because Hosted UI widgets and your app share that session via Scalekit cookies, the user must sign in again before returning to `/ui/` or to app routes that rely on the same browser session.

If you keep a separate application session in your own backend, clear it when Scalekit logs the user out. Use [back-channel logout](/guides/dashboard/redirects/#back-channel-logout-url) or the [user logout webhook](/apis/#webhook/userlogout) so your backend stays in sync. See [manage sessions](/authenticate/fsa/manage-session/) for guidance.

How does `/ui/` choose the organization for multi-org users?

The portal uses the user’s **current active organization** from their Scalekit session. Set or switch the active organization in your application before redirecting to `/ui/`. You do not pass an organization ID as a query parameter on the portal URL.

Do Hosted UI widgets require webhooks to load data?

No. Scalekit hosts the pages and loads the data they need. Use webhooks for your application workflows, for example logout propagation or provisioning events, not to supply data to Hosted UI widgets.

Can I embed Hosted UI widgets in an iframe?

No. Use the [Admin portal](/authenticate/manage-users-orgs/admin-portal/) for iframe embeds and shareable links. The Admin portal supports only the SSO, SCIM, and organization domain verification modules. For every other module, such as member management, session policy, and user profile, use Hosted UI widgets, which are full-page experiences only; redirect users to `/ui/`.

---
# DOCUMENT BOUNDARY
---

# Provision user accounts Just-In-Time (JIT)

> Turn first-time SSO logins into instant, secure access

Organizations where the SSO connection is set up, the enterprise users maybe yet to sign up on your application before they can access your application. Scalekit can automatically provision the user accounts as they sign in through SSO for the first time and creates a membership with an organization instantly. Your app will receive the user’s profile and organization membership details. This is called Just-in-time (JIT) provisioning.

This eliminates the need for manual invitations and allows users to access your application immediately after authenticating with their identity provider. JIT is enabled by default once you [integrated](/authenticate/fsa/quickstart/) and enabled [the SSO connection](/authenticate/auth-methods/enterprise-sso/).

Review the JIT provisioning sequence

![Just-in-time provisioning: a user authenticates through SSO, the IdP sends attributes, and Scalekit creates or updates the user before granting access](/d2/docs/authenticate/manage-users-orgs/jit-provisioning-0.svg)

## Manage JIT provisioning

[Section titled “Manage JIT provisioning”](#manage-jit-provisioning)

Manage JIT provisioning settings for each organization through the Scalekit Dashboard. Register organization domains to enable automatic user creation, and configure whether Scalekit should sync user attributes every time users sign in through SSO.

1. ### Register organization owned domains

   [Section titled “Register organization owned domains”](#register-organization-owned-domains)

   Register email domains for your organization to enable JIT provisioning. JIT provisioning only works for users whose email domain matches one of the organization’s registered [Organization domains](/authenticate/manage-users-orgs/organization-domains/). This ensures that only verified members of the organization can be automatically provisioned.

   **Contractors and external users** with non-matching domains (for eg, `joe@ext.yourapp.com`) cannot be automatically provisioned. These users must be [manually invited](/fsa/guides/user-invitations/) to join the organization. This ensures that unauthorized users cannot obtain access automatically.

2. ### Toggle JIT provisioning on or off

   [Section titled “Toggle JIT provisioning on or off”](#toggle-jit-provisioning-on-or-off)

   **JIT provisioning is enabled by default** once you [integrated](/authenticate/fsa/quickstart/) and enabled [the SSO connection](/authenticate/auth-methods/enterprise-sso/). You can toggle JIT provisioning on or off from the Scalekit Dashboard.

   Go to **Organizations** and select the target organization > **Single Sign On** → **Settings** → **Just-in-time provisioning** section.

   ![Single Sign On Settings tab for an organization with the Just-in-time provisioning toggle enabled](/.netlify/images?url=_astro%2Fjit-provisioning.CWBROiBA.png\&w=2934\&h=1588\&dpl=6ae471fc143c9a4c27550463)

3. ### Keep the user profile in sync with the identity provider

   [Section titled “Keep the user profile in sync with the identity provider”](#keep-the-user-profile-in-sync-with-the-identity-provider)

   Enable **Sync user attributes during login** to keep user profiles updated. When enabled, Scalekit updates the user’s profile using attributes from the identity provider each time they authenticate.

   This keeps the user’s profile in Scalekit aligned with the external Identity Provider.

   ![Single Sign On Settings tab highlighting the Sync user attributes during login toggle below the Just-in-time provisioning setting](/.netlify/images?url=_astro%2Fsync-user-profile.DW9qgfGm.png\&w=2932\&h=1580\&dpl=6ae471fc143c9a4c27550463)

4. ### Using self-service Admin Portal for organization admins

   [Section titled “Using self-service Admin Portal for organization admins”](#using-self-service-admin-portal-for-organization-admins)

   Your customers (organization admins) can manage JIT provisioning settings through the Admin Portal, including registering organization-owned domains, toggling JIT provisioning on or off, and keeping user profiles in sync with the identity provider.

   [Generate and share Admin Portal](/authenticate/manage-users-orgs/admin-portal/) with your customers to set up SSO for their organization. Your end customer can manage the JIT configuration in **Admin portal** > **Single Sign On** > **Settings** > **Just-in-time provisioning** section.

## Common JIT provisioning scenarios

[Section titled “Common JIT provisioning scenarios”](#common-jit-provisioning-scenarios)

Why isn’t a user automatically provisioned during SSO login?

JIT provisioning only works for users whose email domain matches one of the organization’s registered [Organization domains](/authenticate/manage-users-orgs/organization-domains/). If a user’s email domain doesn’t match, they won’t be automatically provisioned.

**Solution**: Register the user’s domain in [Organization domains](/authenticate/manage-users-orgs/organization-domains/) or [manually invite](/fsa/guides/user-invitations/) the user to join the organization.

Why are user roles not assigned correctly during JIT provisioning?

During JIT provisioning, users are assigned the organization’s default member role. If roles are not being assigned as expected, the default role may be missing or misconfigured for the organization.

**Solution**: Review SSO connection settings for default role assignments in **Dashboard > Organizations > \[Organization] > Default role for member**.

---
# DOCUMENT BOUNDARY
---

# Merge user identities

> Scalekit automatically merges user identities from different authentication methods, ensuring a single user profile and preventing duplicate accounts

Users can sign into your application using different authentication methods. A user might authenticate with a passwordless method today and LinkedIn OAuth tomorrow. Scalekit automatically merges these identities into a single user profile. This prevents duplicate accounts and ensures a unified experience.

Identity linking is how Scalekit safely deduplicates authentication methods across identity providers. Scalekit uses the **email address** as the unique identifier and access to the email inbox as the source of truth.

![Identity merging: Google and Microsoft sign-ins with the same email attach as credentials to one user](/d2/docs/authenticate/manage-users-orgs/merge-identities-0.svg)

When users prove access to their email inbox through any authentication method, Scalekit treats this as an identity. Scalekit automatically links multiple identities together using the user’s email address as the source of truth. All authentication methods for the same email address are associated with a single User object.

## Domain verification

[Section titled “Domain verification”](#domain-verification)

When an organization administrator verifies a domain for their organization through [allowed email domains](/authenticate/manage-users-orgs/email-domain-rules/), they prove they have access to create email inboxes. A **verified domain implies the ability to verify all users with that email domain**.

When a domain is verified and an SSO connection is configured, users who sign in through an organization’s identity provider are automatically considered email verified if the domain matches. This reduces friction for your end users while maintaining security.

Users who sign in through SSO with an email address that is not a verified domain are not considered verified. These users must go through the email verification process.

Configure allowed email domains

Learn how to set up allowed email domains for automatic organization membership and domain verification in the [email domain rules guide](/authenticate/manage-users-orgs/email-domain-rules/).

## Merge SSO identities

[Section titled “Merge SSO identities”](#merge-sso-identities)

Users can have multiple authentication methods. Users can also have multiple SSO credentials. This happens when a user works with multiple organizations that each require SSO authentication for all members. There is still only one User object. Users choose which organization’s SSO identity provider to use when authenticating.

![SSO identity merging: SSO credentials from two organizations attach to a single user profile](/d2/docs/authenticate/manage-users-orgs/merge-identities-1.svg)

When users sign in through an SSO identity provider for the first time, Scalekit checks if their email domain is verified. If verified, Scalekit automatically links the SSO credential to the user’s existing account.

Email verification safety still applies. When a user signs in for the first time through an SSO identity provider where the user’s email address is not a verified domain, Scalekit asks the user to verify their email before linking the SSO credential to their account.

Multiple organizations

Users can belong to multiple organizations, each with their own SSO configuration. Scalekit maintains a single user profile while allowing users to authenticate through different organization identity providers.

---
# DOCUMENT BOUNDARY
---

# Organization domains

> Verify ownership of customer domains so Scalekit can route Home Realm Discovery (SSO) and SCIM provisioning to the right organization.

An **organization domain** is a domain your customer owns — such as `acmecorp.com`. Proving domain ownership unlocks two key features:

* **[Home Realm Discovery (SSO)](/authenticate/auth-methods/enterprise-sso/#identify-and-enforce-sso-for-organization-users)**: When a user signs in with an email address matching a registered domain, Scalekit automatically routes them to that organization’s SSO identity provider.
* **[SCIM Provisioning](/authenticate/manage-users-orgs/scim-provisioning/)**: Scalekit only processes SCIM user lifecycle events (create, update, deactivate) for users whose email domain matches a registered organization domain. This ensures only the organization that owns a domain can manage those users.

## Add and verify an organization domain

[Section titled “Add and verify an organization domain”](#add-and-verify-an-organization-domain)

### Via Admin portal No-code

[Section titled “Via Admin portal ”](#via-admin-portal-)

The Admin portal gives the admin of a customer organization a self-serve way to add and verify their domain — without involving your engineering team. This is the recommended approach for production onboarding.

1. ### Enable domain verification for the organization

   [Section titled “Enable domain verification for the organization”](#enable-domain-verification-for-the-organization)

   Turn on the `Domain Verification` feature for the organization. You can do this in the Scalekit Dashboard (navigate to **Organizations** → select the organization → **Overview** → toggle **Domain Verification**), or via the API:

   * cURL

     Enable domain_verification feature

     ```sh
     curl --request PATCH \
       'https:///api/v1/organizations//settings' \
       --header 'Authorization: Bearer ' \
       --header 'Content-Type: application/json' \
       --data '{
         "features": [{ "name": "domain_verification", "enabled": true }]
       }'
     ```

   * Node.js

     Enable domain_verification feature

     ```ts
     await scalekit.organization.updateOrganizationSettings(organizationId, {
       features: [{ name: 'domain_verification', enabled: true }],
     });
     ```

   * Python

     Enable domain_verification feature

     ```python
     scalekit_client.organization.update_organization_settings(
         organization_id,
         [{"name": "domain_verification", "enabled": True}],
     )
     ```

   * Go

     Enable domain_verification feature

     ```go
     err = scalekitClient.Organization.UpdateOrganizationSettings(
         ctx,
         organizationID,
         scalekit.OrganizationSettings{
             Features: []scalekit.OrganizationSettingsFeature{
                 {Name: "domain_verification", Enabled: true},
             },
         },
     )
     ```

   * Java

     Enable domain_verification feature

     ```java
     scalekitClient.organizations().updateOrganizationSettings(
         organizationId,
         List.of(new Feature().setName("domain_verification").setEnabled(true))
     );
     ```

2. ### Generate an Admin portal link and embed in your app

   [Section titled “Generate an Admin portal link and embed in your app”](#generate-an-admin-portal-link-and-embed-in-your-app)

   Generate an admin portal link and embed in your application:

   * cURL

     Generate portal link for domain verification

     ```sh
     curl 'https:///api/v1/organizations//portal_links' \
       --header 'Authorization: Bearer '
     ```

   * Node.js

     Generate portal link for domain verification

     ```ts
     const link = await scalekit.organization.generatePortalLink(organizationId);
     // Redirect the org admin to link.location
     ```

   * Python

     Generate portal link for domain verification

     ```python
     link = scalekit_client.organization.generate_portal_link(organization_id)
     # Redirect the org admin to link.location
     ```

   * Go

     Generate portal link for domain verification

     ```go
     link, err := scalekitClient.Organization().GeneratePortalLink(ctx, organizationID)
     // Redirect the org admin to link.Link.Location
     ```

   * Java

     Generate portal link for domain verification

     ```java
     Link link = scalekitClient.organizations().generatePortalLink(
         organizationId,
         Arrays.asList(Feature.domain_verification)
     );
     // Redirect the org admin to link.getLocation()
     ```

3. ### Organization admin adds the domain and publishes the DNS record

   [Section titled “Organization admin adds the domain and publishes the DNS record”](#organization-admin-adds-the-domain-and-publishes-the-dns-record)

   The admin opens the portal link, clicks **Add domain**, and enters their domain name. The portal displays the TXT record to add and verify domain ownership. The admin adds this record in their DNS provider (e.g., Cloudflare, Route 53, GoDaddy). Propagation typically takes a few minutes but can take up to 48 hours.

   ![Domain verification via admin portal](/.netlify/images?url=_astro%2F2026-05-26-19-01-37.C8CAmZIi.png\&w=2196\&h=1596\&dpl=6ae471fc143c9a4c27550463)

4. ### Scalekit verifies the DNS record

   [Section titled “Scalekit verifies the DNS record”](#scalekit-verifies-the-dns-record)

   Scalekit polls the domain’s DNS automatically. Once the TXT record is detected, the domain status changes to **Verified** and SSO routing and SCIM become active.

### Via Hosted UI widgets No-code

[Section titled “Via Hosted UI widgets ”](#via-hosted-ui-widgets-)

Turn on the `Domain Verification` feature for the organization. Then redirect end users to [Hosted UI widgets](/authenticate/manage-users-orgs/hosted-widgets/) to manage their organization’s domains. The domain verification flow inside the widget is identical to the Admin portal. For a shareable link or iframe embed aimed at enterprise IT, use the [Admin portal](/authenticate/manage-users-orgs/admin-portal/).

![Domain verification via Hosted UI widgets](/.netlify/images?url=_astro%2F2026-05-26-19-06-40.DKn_DLNP.png\&w=2932\&h=1598\&dpl=6ae471fc143c9a4c27550463)

### Via Scalekit Dashboard No-code

[Section titled “Via Scalekit Dashboard ”](#via-scalekit-dashboard-)

Your team can add organization domains directly from the Scalekit Dashboard, useful for initial setup or when you have already verified ownership through other means.

1. Go to **Dashboard > Organizations** and select the target organization.
2. Navigate to **Overview** > **Organization Domains**.
3. Click **Add domain** and enter the domain name.

![Domain verification via dashboard](/.netlify/images?url=_astro%2F2026-05-26-19-09-32.CbEgDZch.png\&w=2918\&h=1580\&dpl=6ae471fc143c9a4c27550463)

Domains added this way are marked as **Admin-verified**, no DNS verification is required. They become active for SSO routing and SCIM immediately.

### Via API API

[Section titled “Via API ”](#via-api-)

Add and manage organization domains programmatically. Domains created via the API are also **Admin-verified** by default, no DNS verification is required. Use this when you have already confirmed domain ownership through another process and want to activate SSO routing or SCIM immediately.

* cURL

  Manage organization domains

  ```sh
  # 1. Add an organization domain
  curl 'https:///api/v1/organizations//domains' \
    --request POST \
    --header 'Authorization: Bearer ' \
    --header 'Content-Type: application/json' \
    --data '{
      "domain": "acmecorp.com",
      "domain_type": "ORGANIZATION_DOMAIN"
    }'


  # 2. List all organization domains
  curl 'https:///api/v1/organizations//domains?domain_type=ORGANIZATION_DOMAIN' \
    --header 'Authorization: Bearer '


  # 3. Get details of a specific domain
  curl 'https:///api/v1/organizations//domains/' \
    --header 'Authorization: Bearer '


  # 4. Delete a domain
  curl 'https:///api/v1/organizations//domains/' \
    --request DELETE \
    --header 'Authorization: Bearer '
  ```

* Node.js

  Manage organization domains

  ```ts
  1
  import { Scalekit } from '@scalekit-sdk/node';
  2


  3
  const scalekit = new Scalekit(
  4
    process.env.SCALEKIT_ENVIRONMENT_URL,
  5
    process.env.SCALEKIT_CLIENT_ID,
  6
    process.env.SCALEKIT_CLIENT_SECRET,
  7
  );
  8


  9
  // 1. Add an organization domain
  10
  const newDomain = await scalekit.organization.createDomain(organizationId, {
  11
    domain: 'acmecorp.com',
  12
    domainType: 'ORGANIZATION_DOMAIN',
  13
  });
  14
  // newDomain.txtRecordKey and newDomain.txtRecordSecret contain the DNS record values
  15


  16
  // 2. List all organization domains
  17
  const { domains } = await scalekit.organization.listDomains(organizationId, {
  18
    domainType: 'ORGANIZATION_DOMAIN',
  19
  });
  20


  21
  // 3. Get a specific domain
  22
  const domain = await scalekit.organization.getDomain(organizationId, domainId);
  23


  24
  // 4. Delete a domain
  25
  await scalekit.organization.deleteDomain(organizationId, domainId);
  ```

* Python

  Manage organization domains

  ```python
  1
  from scalekit import Scalekit
  2
  import os
  3


  4
  scalekit_client = Scalekit(
  5
      environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
  6
      client_id=os.environ.get("SCALEKIT_CLIENT_ID"),
  7
      client_secret=os.environ.get("SCALEKIT_CLIENT_SECRET"),
  8
  )
  9


  10
  # 1. Add an organization domain
  11
  new_domain = scalekit_client.organization.create_domain(
  12
      organization_id,
  13
      domain="acmecorp.com",
  14
      domain_type="ORGANIZATION_DOMAIN",
  15
  )
  16
  # new_domain.txt_record_key and new_domain.txt_record_secret contain the DNS record values
  17


  18
  # 2. List all organization domains
  19
  domains = scalekit_client.organization.list_domains(
  20
      organization_id, domain_type="ORGANIZATION_DOMAIN"
  21
  )
  22


  23
  # 3. Get a specific domain
  24
  domain = scalekit_client.organization.get_domain(organization_id, domain_id)
  25


  26
  # 4. Delete a domain
  27
  scalekit_client.organization.delete_domain(organization_id, domain_id)
  ```

* Go

  Manage organization domains

  ```go
  1
  import (
  2
      "context"
  3
      "os"
  4


  5
      "github.com/scalekit/sdk-go"
  6
  )
  7


  8
  scalekitClient := scalekit.New(
  9
      os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
  10
      os.Getenv("SCALEKIT_CLIENT_ID"),
  11
      os.Getenv("SCALEKIT_CLIENT_SECRET"),
  12
  )
  13


  14
  ctx := context.Background()
  15


  16
  // 1. Add an organization domain
  17
  newDomain, err := scalekitClient.Organization().CreateDomain(ctx, organizationID, scalekit.CreateDomainOptions{
  18
      Domain:     "acmecorp.com",
  19
      DomainType: "ORGANIZATION_DOMAIN",
  20
  })
  21
  // newDomain.TxtRecordKey and newDomain.TxtRecordSecret contain the DNS record values
  22


  23
  // 2. List all organization domains
  24
  domains, err := scalekitClient.Organization().ListDomains(ctx, organizationID, scalekit.ListDomainsOptions{
  25
      DomainType: "ORGANIZATION_DOMAIN",
  26
  })
  27


  28
  // 3. Get a specific domain
  29
  domain, err := scalekitClient.Organization().GetDomain(ctx, organizationID, domainID)
  30


  31
  // 4. Delete a domain
  32
  err = scalekitClient.Organization().DeleteDomain(ctx, organizationID, domainID)
  ```

* Java

  Manage organization domains

  ```java
  1
  import com.scalekit.client.Scalekit;
  2


  3
  Scalekit scalekitClient = new Scalekit(
  4
      System.getenv("SCALEKIT_ENVIRONMENT_URL"),
  5
      System.getenv("SCALEKIT_CLIENT_ID"),
  6
      System.getenv("SCALEKIT_CLIENT_SECRET")
  7
  );
  8


  9
  // 1. Add an organization domain
  10
  Domain newDomain = scalekitClient.organizations()
  11
      .createDomain(organizationId, new CreateDomainOptions()
  12
          .setDomain("acmecorp.com")
  13
          .setDomainType("ORGANIZATION_DOMAIN"));
  14
  // newDomain.getTxtRecordKey() and newDomain.getTxtRecordSecret() contain the DNS record values
  15


  16
  // 2. List all organization domains
  17
  List domains = scalekitClient.organizations()
  18
      .listDomains(organizationId, new ListDomainsOptions()
  19
          .setDomainType("ORGANIZATION_DOMAIN"));
  20


  21
  // 3. Get a specific domain
  22
  Domain domain = scalekitClient.organizations().getDomain(organizationId, domainId);
  23


  24
  // 4. Delete a domain
  25
  scalekitClient.organizations().deleteDomain(organizationId, domainId);
  ```

## Listen for organization domain events

[Section titled “Listen for organization domain events”](#listen-for-organization-domain-events)

Subscribe to these webhook events to react to domain lifecycle changes in your application:

| Event                                          | Fires when                                                         |
| ---------------------------------------------- | ------------------------------------------------------------------ |
| `organization.domain_created`                  | A domain is added to an organization via any method                |
| `organization.domain_deleted`                  | A domain is removed from an organization                           |
| `organization.domain_dns_verification_success` | The DNS check confirms domain ownership                            |
| `organization.domain_dns_verification_failed`  | The DNS verification window expired without a successful DNS match |

All four events have the same envelope and the same `data` shape. Every `data` key is sent, even when its value is empty or `null`. Domain events have no `object` field.

organization.domain_created

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_4567890123",
4
  "type": "organization.domain_created",
5
  "occurred_at": "2024-01-15T11:00:00.123456789Z",
6
  "environment_id": "env_1234567890",
7
  "organization_id": "org_1234567890",
8
  "data": {
9
    "domain": "acmecorp.com",
10
    "domain_type": "ORGANIZATION_DOMAIN",
11
    "id": "dom_1234567890",
12
    "organization_id": "org_1234567890",
13
    "txt_record_key": "scalekit-domain-verification",
14
    "txt_record_secret": "sk_dom_verify_abc123",
15
    "updated_at": "2024-01-15T11:00:00.123456789Z",
16
    "verification_method": "DNS",
17
    "verification_status": "PENDING"
18
  }
19
}
```

| Field                 | Type           | Description                                                                                                                      |
| --------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `id`                  | string         | Unique identifier for the domain (`dom_` prefix)                                                                                 |
| `domain`              | string         | The domain name                                                                                                                  |
| `domain_type`         | string         | `ORGANIZATION_DOMAIN` for SSO/SCIM domains; `ALLOWED_EMAIL_DOMAIN` for auto-join domains                                         |
| `organization_id`     | string         | Organization that owns the domain (`org_` prefix)                                                                                |
| `txt_record_key`      | string         | DNS TXT record host used for ownership verification                                                                              |
| `txt_record_secret`   | string         | DNS TXT value Scalekit expects. Treat as sensitive — do not log or expose in client UIs                                          |
| `verification_status` | string         | `PENDING`, `VERIFIED`, `FAILED`, or `AUTO_VERIFIED`                                                                              |
| `verification_method` | string or null | `DNS` (TXT record), `ADMIN` (added by the B2B app team), `NOT_APPLICABLE` (allowed email domains), or `null` if no method is set |
| `updated_at`          | string         | When the domain status last changed                                                                                              |

Configure handlers with the [webhooks guide](/authenticate/implement-workflows/implement-webhooks/).

## Common scenarios

[Section titled “Common scenarios”](#common-scenarios)

How is organization domains different from allowed email domains?

[Allowed email domains](/authenticate/manage-users-orgs/email-domain-rules/#configure-allowed-email-domains) let users with a matching email address see and join the organization via the organization switcher.

Organization domains prove domain ownership by an organization and unlock SSO routing (Home Realm Discovery) and SCIM provisioning. The distinction is about what the domain enables, not just who can join.

What happens if DNS hasn’t propagated yet?

DNS propagation can take anywhere from a few minutes to 48 hours depending on the TTL and the DNS provider.

Scalekit polls the domain on a scheduled interval and retries automatically until the TXT record is found or the verification window expires. The org admin can also click **Verify now** in the Admin portal or Hosted UI widgets to trigger an immediate check at any time.

If the window expires before the record propagates, the domain status moves to `FAILED`. Delete the domain and add it again to get a fresh verification token and restart the process.

Can I verify a domain without DNS?

Yes. Domains added via the Scalekit Dashboard or API are marked as **Admin-verified**, no DNS verification is required. Use this when you have already confirmed domain ownership through another process (contract, business verification, etc.) and want to activate SSO routing or SCIM immediately.

---
# DOCUMENT BOUNDARY
---

# Implement organization switcher

> Let users switch across workspaces using prompt-based selection or direct org routing via organization ID

Organization switching lets users access multiple organizations or workspaces within your application. This guide shows you how to implement organization switching using Scalekit’s built-in switcher or by building your own organization switcher in your application.

This feature is essential for B2B applications where users may belong to several organizations simultaneously. Common scenarios include:

* **Personal workspace to corporate workspace**: Users sign up with their organization’s email address, creating their personal workspace. Later, when their organization subscribes to your app, a new corporate workspace is created (for example, “AcmeCorp workspace”).
* **Multi-organization contractors**: External consultants or contractors who belong to multiple organizations, each with their own SSO authentication policies. These users need to switch between different client organizations while maintaining secure access to each workspace.

![Scalekit organization switcher screen listing a user's organizations and a pending invitation with a Join button](/.netlify/images?url=_astro%2F1-switcher.BmXDeGKX.png\&w=2940\&h=1662\&dpl=6ae471fc143c9a4c27550463)

## Default organization switching behavior

[Section titled “Default organization switching behavior”](#default-organization-switching-behavior)

When users belong to multiple organizations, Scalekit automatically handles organization switching during the authentication flow:

1. Users click **Sign In** on your application.
2. Your application redirects users to Scalekit’s sign-in page.
3. Users authenticate using one of the available sign-in methods.
4. Scalekit displays a list of organizations that users belong to.
5. Users select the organization they want to sign in to.
6. Users are redirected to the organization’s workspace and signed in.

Note

For organizations with Single Sign-On (SSO) enabled on a verified domain, the sign-in flow is automated. When a user enters their work email address, Scalekit redirects them to their organization’s identity provider to sign in. The organization selection step is skipped.

Scalekit provides built-in support for organization switching through automatic organization detection, a hosted organization switcher UI, and secure session management. Each organization maintains its own authentication context and policies.

## Control organization switching behavior

[Section titled “Control organization switching behavior”](#control-organization-switching-behavior)

You can customize the organization switcher’s behavior by adding query parameters when generating the authorization URL. These parameters give you precise control over how users navigate between organizations.

### Display organization switcher

[Section titled “Display organization switcher”](#display-organization-switcher)

Add the `prompt: 'select_account'` parameter when generating the authorization URL. This forces Scalekit to display a list of organizations the user belongs to, even if they’re already signed in.

* Node.js

  Express.js

  ```diff
  // Use case: Show organization switcher after user authentication
  const redirectUri = 'http://localhost:3000/api/callback';
  const options = {
    scopes: ['openid', 'profile', 'email', 'offline_access'],
    prompt: 'select_account'
  };


  const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options);


  res.redirect(authorizationUrl);
  ```

* Python

  Flask

  ```diff
  # Use case: Show organization switcher after user authentication
  from scalekit import AuthorizationUrlOptions


  redirect_uri = 'http://localhost:3000/api/callback'
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email', 'offline_access']
  options.prompt = 'select_account'


  authorization_url = scalekit.get_authorization_url(redirect_uri, options)
  return redirect(authorization_url)
  ```

* Go

  Gin

  ```diff
  // Use case: Show organization switcher after user authentication
  redirectUri := "http://localhost:3000/api/callback"
  options := scalekit.AuthorizationUrlOptions{
      Scopes: []string{"openid", "profile", "email", "offline_access"},
      +Prompt: "select_account",
  }


  authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  if err != nil {
      // handle error appropriately
      panic(err)
  }


  c.Redirect(http.StatusFound, authorizationUrl.String())
  ```

* Java

  Spring

  ```diff
  // Use case: Show organization switcher after user authentication
  import com.scalekit.internal.http.AuthorizationUrlOptions;
  import java.net.URL;
  import java.util.Arrays;


  String redirectUri = "http://localhost:3000/api/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  +options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access"));
  options.setPrompt("select_account");


  URL authorizationUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options);
  ```

This displays the organization switcher UI where users can choose which organization to access.

### Switch users directly to a specific organization

[Section titled “Switch users directly to a specific organization”](#switch-users-directly-to-a-specific-organization)

To bypass the organization switcher and directly authenticate users into a specific organization, include both the `prompt: 'select_account'` parameter and the `organizationId` parameter:

* Node.js

  Express.js

  ```diff
  // Use case: Directly route users to a specific organization
  const redirectUri = 'http://localhost:3000/api/callback';
  const options = {
    scopes: ['openid', 'profile', 'email', 'offline_access'],
    prompt: 'select_account',
    organizationId: 'org_1233434'
  };


  const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options);


  res.redirect(authorizationUrl);
  ```

* Python

  Flask

  ```diff
  # Use case: Directly route users to a specific organization
  from scalekit import AuthorizationUrlOptions


  redirect_uri = 'http://localhost:3000/api/callback'
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email', 'offline_access']
  options.prompt = 'select_account'
  options.organization_id = 'org_1233434'


  authorization_url = scalekit.get_authorization_url(redirect_uri, options)
  return redirect(authorization_url)
  ```

* Go

  Gin

  ```diff
  // Use case: Directly route users to a specific organization
  redirectUri := "http://localhost:3000/api/callback"
  options := scalekit.AuthorizationUrlOptions{
      +Scopes: []string{"openid", "profile", "email", "offline_access"},
      +Prompt: "select_account",
      OrganizationId: "org_1233434",
  }


  authorizationUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  if err != nil {
      // handle error appropriately
      panic(err)
  }


  c.Redirect(http.StatusFound, authorizationUrl.String())
  ```

* Java

  Spring

  ```diff
  // Use case: Directly route users to a specific organization
  import com.scalekit.internal.http.AuthorizationUrlOptions;
  import java.net.URL;
  import java.util.Arrays;


  String redirectUri = "http://localhost:3000/api/callback";
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  +options.setScopes(Arrays.asList("openid", "profile", "email", "offline_access"));
  +options.setPrompt("select_account");
  options.setOrganizationId("org_1233434");


  URL authorizationUrl = scalekit.authentication().getAuthorizationUrl(redirectUri, options);
  ```

When you include both parameters, Scalekit will:

* **If the user is already authenticated**: Directly sign them into the specified organization
* **If the user needs to authenticate**: First authenticate the user, then sign them into the specified organization

## Organization switching parameters

[Section titled “Organization switching parameters”](#organization-switching-parameters)

Use these parameters to control the organization switching behavior:

| Parameter                                      | Description                        | Example                                                                      |
| ---------------------------------------------- | ---------------------------------- | ---------------------------------------------------------------------------- |
| `prompt=select_account`                        | Shows the organization switcher UI | Forces organization selection even for authenticated users                   |
| `prompt=select_account&organizationId=org_123` | Direct organization access         | Bypasses switcher and authenticates directly into the specified organization |

Tip

The `organizationId` parameter works only when combined with `prompt=select_account`. Using `organizationId` alone will not have the desired effect.

---
# DOCUMENT BOUNDARY
---

# Provision users and groups with SCIM

> Automate user and group lifecycle management using SCIM provisioning

Scalekit supports user and group provisioning using the [SCIM protocol](/directory/guides/user-provisioning-basics/), allowing your customers to manage access to their organization in your app directly from their directory provider. With SCIM, the directory becomes the source of truth for organization membership, user profile attributes, and access — eliminating manual invites, role drift, and delayed deprovisioning. SCIM ensures that access to your application always reflects the organization’s directory state, from onboarding to offboarding.

Using SCIM, your customers can:

* Add users to their organization
* Keep user attributes (like name, email or role) in sync
* Remove users from their organization
* Control application roles through directory group membership

SCIM provisioning enables end-to-end lifecycle management, ensuring access is granted, updated, and revoked automatically as users move through the organization.

***

### Who should use SCIM provisioning?

[Section titled “Who should use SCIM provisioning?”](#who-should-use-scim-provisioning)

SCIM provisioning is recommended for:

* Enterprise customers that require **centralized identity management**
* Teams already using a directory provider like Okta, Azure AD (Entra ID), or Google Workspace
* Customers that need **group-based access control** and automated deprovisioning

***

Review the SCIM provisioning flow

![SCIM provisioning: the directory sends user and group changes to Scalekit, which validates them and syncs your app](/d2/docs/authenticate/manage-users-orgs/scim-provisioning-0.svg)

### Manage SCIM provisioning

[Section titled “Manage SCIM provisioning”](#manage-scim-provisioning)

1. ## Register organization-owned domains

   [Section titled “Register organization-owned domains”](#register-organization-owned-domains)

   Register the email domains owned by the organization. SCIM provisioning only works for users whose email domain matches one of the organization’s registered [Organization domains](/authenticate/manage-users-orgs/organization-domains/). This ensures that only verified members of the organization can be automatically provisioned.

   **Contractors and external users** with non-matching domains (e.g., `joe@ext.yourapp.com`) cannot be automatically provisioned via SCIM. These users must be [manually invited](/fsa/guides/user-invitations/) to join the organization. This ensures that unauthorized users cannot obtain access automatically.

   Navigate to **Dashboard** > **Organizations** and select the target organization > **Overview** > **Organization Domains** section to register organization domains.

2. ## Enable SCIM provisioning for the organization

   [Section titled “Enable SCIM provisioning for the organization”](#enable-scim-provisioning-for-the-organization)

   SCIM provisioning should be enabled for the target organization either through the Scalekit Dashboard or the self-service [Admin Portal](/authenticate/manage-users-orgs/admin-portal/). Follow the detailed setup instructions [here](/guides/user-management/scim-provisioning/).

3. ## Provision users and groups from the directory

   [Section titled “Provision users and groups from the directory”](#provision-users-and-groups-from-the-directory)

   Once SCIM provisioning is enabled for the organization, the directory becomes the system of record for that organization in your app. Organization administrators can manage access directly from their IdP by:

   * Assigning users or groups to your application
   * Updating user profile attributes
   * Removing users or groups to revoke access

4. ## Group-based role assignment

   [Section titled “Group-based role assignment”](#group-based-role-assignment)

   Scalekit supports assigning roles to users in your app based on directory group membership. This enables consistent, policy-driven access control managed entirely from the directory provider.

   * Map directory groups to application roles in Scalekit
   * Users receive roles automatically when added to mapped groups
   * Roles are revoked when users are removed from those groups, including when the group itself is deleted in the directory
   * If a user belongs to multiple mapped groups, deleting one group recalculates the user’s role from their remaining group memberships instead of leaving the deleted group’s role in place

   Note

   Users without an explicit role mapping are assigned the organization’s default member role. This applies when:

   * A directory group is not mapped to a role, or
   * A provisioned user is not a member of any mapped group

5. ## User attribute mapping

   [Section titled “User attribute mapping”](#user-attribute-mapping)

   Scalekit automatically maps the following user attributes from the directory to the Scalekit user profile:

   * `email`
   * `preferred_username`
   * `name`
   * `given_name`
   * `family_name`
   * `picture`
   * `phone_number`
   * `locale`
   * `custom_attributes`

   When attributes change in the directory, Scalekit updates the user profile automatically during SCIM synchronization.

***

### Supported directory providers

[Section titled “Supported directory providers”](#supported-directory-providers)

Scalekit supports SCIM provisioning with common enterprise directory providers including Okta, Entra ID (Azure AD), and Google Workspace. See the full list of supported providers [here](/guides/integrations/scim-integrations/).

***

### Common SCIM provisioning scenarios

[Section titled “Common SCIM provisioning scenarios”](#common-scim-provisioning-scenarios)

Why isn’t a user appearing in Scalekit after SCIM sync?

Check the following:

* The user is assigned to the Scalekit application in the directory
* The user has an email address defined in the directory
* The user’s email domain matches a registered organization domain
* The SCIM bearer token is valid and active

If a user’s email is changed in the directory, will this be reflected on the user’s email in Scalekit?

No. Scalekit treats email as an immutable, unique identifier. If a directory attempts to update a user’s email, the SCIM update request will be rejected.

Can user lifecycle management happen only via SCIM if a user is provisioned through a SCIM connection?

No. SCIM is not an exclusive control plane. Even if a user is provisioned via a SCIM connection, you can still manage that user using Scalekit APIs or SDKs. Scalekit follows a **last-write-wins** model. The most recent action — whether it comes from SCIM or from an API/SDK call — will be reflected on the user. This model gives you flexibility to:

* Perform administrative or break-glass actions from your application
* Run migrations or bulk updates using APIs
* Rely on SCIM for ongoing, automated lifecycle management

Can both SSO and SCIM work for an organization?

Yes. SSO handles authentication (how users log in), while SCIM handles lifecycle management (how users are created, updated, and removed). They are complementary and commonly used together.

---
# DOCUMENT BOUNDARY
---

# MCP Auth code samples

> MCP Auth authentication examples and patterns

### [Add Auth to Node.js MCP Servers](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node)

[Add Scalekit auth to a Node.js MCP server with minimal setup. Includes a working example with user greeting.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node)

### [Add Auth to Python MCP Servers](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python)

[Add Scalekit auth to a Python MCP server in minutes. Includes a working example with user greeting.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python)

### [Secure FastMCP Apps with Auth](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp)

[Build a secure FastMCP app with Scalekit. Features a complete todo list with protected endpoints and session management.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp)

---
# DOCUMENT BOUNDARY
---

# Publish your MCP connector to Claude & ChatGPT

> Give MCP connector directory reviewers a test user and static code so they can authenticate and test your server end to end.

To list your MCP connector in the [Claude connector directory](https://claude.com/docs/connectors/building/submission) or the [ChatGPT app directory](https://developers.openai.com/apps-sdk/deploy/submission#review-and-approval-faqs), a reviewer must sign in to your server and run every tool end to end. Both submission forms have a testing-access step that expects every credential and step needed to reach a fully-populated account.

Scalekit’s [Test Users](/authenticate/run-e2e-tests/) feature makes this possible without handing over a real inbox: allowlist a reviewer email and set a **static 6-digit code**, and the reviewer signs in through Scalekit’s hosted login using that code. This page shows how to prepare the credential and write the instructions to paste into the submission form.

Works only with Scalekit-managed auth

Test Users works only when **Scalekit manages your app’s login, sessions, and user management** (Full Stack Auth / SaaSKit), because it intercepts Scalekit’s own Magic Link/OTP delivery.

It does **not** work when your MCP server runs in [Bring Your Own Auth](/mcp/auth-methods/custom-auth/) mode. In that mode Scalekit federates to your own identity provider and never owns the login screen or the OTP, so there is no static code to hand a reviewer. For Bring Your Own Auth, create a reviewer account directly in your identity provider and share those credentials instead.

## Prerequisites

[Section titled “Prerequisites”](#prerequisites)

* An MCP connector already protected with Scalekit MCP Auth using Scalekit-managed auth. See the [MCP Auth quickstart](/authenticate/mcp/quickstart/).
* The **Magic Link & OTP** auth method enabled, with delivery set to `Verification Code` or `Magic Link + Verification Code`. `Magic Link`-only delivery is not supported. See the [Test Users prerequisite](/authenticate/run-e2e-tests/#prerequisites).

## How reviewer sign-in works

[Section titled “How reviewer sign-in works”](#how-reviewer-sign-in-works)

The reviewer follows the same OAuth 2.1 flow as any [human interacting with your MCP server](/authenticate/mcp/overview/#the-authorization-flow-in-practice):

1. The MCP client (MCP Inspector or Claude as a custom connector) calls your protected server and receives a `401` pointing to Scalekit.
2. The client redirects the reviewer to Scalekit’s hosted login.
3. The reviewer enters the **test email** and the **static code** you configured — no inbox required.
4. The reviewer approves the consent screen, Scalekit issues an access token, and the client runs your tools.

## Prepare the reviewer credential

[Section titled “Prepare the reviewer credential”](#prepare-the-reviewer-credential)

1. ## Enable Test Users and add a reviewer email

   [Section titled “Enable Test Users and add a reviewer email”](#enable-test-users-and-add-a-reviewer-email)

   In **Dashboard > Environment settings > Test users**, toggle **Enable test users** and add a reviewer address. The email must contain `+sktest` in the local part — for example `reviewer+sktest@yourapp.com`. Set the **Static confirmation code** (the default is `424242`).

   For the full field-by-field reference, see [Configure test users](/authenticate/run-e2e-tests/#configure-test-users).

2. ## Create the reviewer inside a fully-populated organization

   [Section titled “Create the reviewer inside a fully-populated organization”](#create-the-reviewer-inside-a-fully-populated-organization)

   Reviewers expect your tools to return real data, so add the reviewer user to an organization that already has representative content. Create the user with `sendInvitationEmail: false` so no email goes out, then confirm the same email is on the Test Users allowlist.

   * Node.js

     Create the reviewer user

     ```ts
     1
     import { ScalekitClient } from '@scalekit-sdk/node'
     2


     3
     const scalekit = new ScalekitClient(
     4
       process.env.SCALEKIT_ENVIRONMENT_URL!,
     5
       process.env.SCALEKIT_CLIENT_ID!,
     6
       process.env.SCALEKIT_CLIENT_SECRET!
     7
     )
     8


     9
     try {
     10
       // Add the reviewer to a populated org so tools return real data during review.
     11
       const { user } = await scalekit.user.createUserAndMembership(orgId, {
     12
         email: 'reviewer+sktest@yourapp.com',
     13
         sendInvitationEmail: false,
     14
       })
     15
     } catch (error) {
     16
       console.error('Failed to create reviewer user:', error)
     17
     }
     ```

   * Python

     Create the reviewer user

     ```python
     1
     import os
     2
     from scalekit import ScalekitClient
     3
     from scalekit.v1.users.users_pb2 import CreateUser
     4


     5
     scalekit_client = ScalekitClient(
     6
       env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
     7
       client_id=os.environ["SCALEKIT_CLIENT_ID"],
     8
       client_secret=os.environ["SCALEKIT_CLIENT_SECRET"],
     9
     )
     10


     11
     try:
     12
       # Add the reviewer to a populated org so tools return real data during review.
     13
       user_response = scalekit_client.users.create_user_and_membership(
     14
         org_id,
     15
         CreateUser(email="reviewer+sktest@yourapp.com"),
     16
         send_invitation_email=False,
     17
       )
     18
     except Exception as error:
     19
       print(f"Failed to create reviewer user: {error}")
     ```

   * Go

     Create the reviewer user

     ```go
     1
     import usersv1 "github.com/scalekit-inc/scalekit-sdk-go/v2/pkg/grpc/scalekit/v1/users"
     2


     3
     scalekitClient := scalekit.NewScalekitClient(
     4
       os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
     5
       os.Getenv("SCALEKIT_CLIENT_ID"),
     6
       os.Getenv("SCALEKIT_CLIENT_SECRET"),
     7
     )
     8


     9
     // Add the reviewer to a populated org so tools return real data during review.
     10
     userResp, err := scalekitClient.User().CreateUserAndMembership(ctx, orgID,
     11
       &usersv1.CreateUser{Email: "reviewer+sktest@yourapp.com"},
     12
       false,
     13
     )
     14
     if err != nil {
     15
       log.Fatalf("failed to create reviewer user: %v", err)
     16
     }
     ```

   * Java

     Create the reviewer user

     ```java
     1
     import com.scalekit.grpc.scalekit.v1.users.*;
     2


     3
     ScalekitClient scalekitClient = new ScalekitClient(
     4
       System.getenv("SCALEKIT_ENVIRONMENT_URL"),
     5
       System.getenv("SCALEKIT_CLIENT_ID"),
     6
       System.getenv("SCALEKIT_CLIENT_SECRET")
     7
     );
     8


     9
     try {
     10
       // Add the reviewer to a populated org so tools return real data during review.
     11
       CreateUserAndMembershipRequest request = CreateUserAndMembershipRequest.newBuilder()
     12
         .setUser(CreateUser.newBuilder().setEmail("reviewer+sktest@yourapp.com").build())
     13
         .setSendInvitationEmail(false)
     14
         .build();
     15


     16
       CreateUserAndMembershipResponse userResp = scalekitClient.users()
     17
         .createUserAndMembership(orgId, request);
     18
     } catch (Exception error) {
     19
       System.err.println("Failed to create reviewer user: " + error.getMessage());
     20
     }
     ```

## Verify

[Section titled “Verify”](#verify)

Connect to your server the same way the reviewer will — via [MCP Inspector](https://github.com/modelcontextprotocol/inspector) or as a custom connector in Claude — and sign in with the reviewer email and static code.

Verification succeeds when:

* Sign-in completes with the static code and no real verification email is sent.
* Every tool runs successfully and returns representative data from the populated organization.

Fix any gaps before you submit to a directory.

## Write the test setup instructions

[Section titled “Write the test setup instructions”](#write-the-test-setup-instructions)

Paste instructions into the submission form’s testing-access field — the **Test setup instructions** box on Claude’s **Test & launch** step, or the equivalent testing-instructions field in the ChatGPT app submission. Give the reviewer everything needed to authenticate autonomously:

Test setup instructions (paste-ready)

```text
Connector URL: https://mcp.yourapp.com/mcp


Authentication: OAuth 2.0 (hosted login). When prompted to connect, use the
test account below — no email inbox is required.


  Email: reviewer+sktest@yourapp.com
  Verification code: 424242


Steps:
  1. Add the connector and start the OAuth sign-in.
  2. On the login screen, enter the email above and continue.
  3. At the verification-code prompt, enter 424242 and continue.
  4. Approve the consent screen.
  5. The account belongs to a fully-populated organization, so every tool
     returns representative data.
```

Replace the URL, email, and code with your own values. Keep the code identical to the **Static confirmation code** configured in the dashboard.

## Next steps

[Section titled “Next steps”](#next-steps)

Once your connector is approved, remove the reviewer email from the allowlist or rotate the static code — see [Configure test users](/authenticate/run-e2e-tests/#configure-test-users) for how to manage the allowlist.

## Common scenarios

[Section titled “Common scenarios”](#common-scenarios)

The reviewer still receives a real verification email

All three conditions must be true for Test Users to apply:

* **Test Users** is enabled in the environment.
* The reviewer email is on the allowlist (case-insensitive match).
* **Magic Link & OTP** is enabled with delivery set to `Verification Code` or `Magic Link + Verification Code`.

My connector uses Bring Your Own Auth — can I still do this?

No. Test Users only affects Scalekit’s own Magic Link/OTP delivery. In [Bring Your Own Auth](/mcp/auth-methods/custom-auth/) mode, Scalekit federates to your identity provider, which owns the login and any verification codes. Create a dedicated reviewer account in your identity provider and share those credentials in the test setup instructions instead.

---
# DOCUMENT BOUNDARY
---

# Express.js quickstart

> Build a production-ready Express.js MCP server with TypeScript, custom middleware for OAuth token validation, and Scalekit authentication.

This guide shows you how to build a production-ready Express.js MCP server with TypeScript and Scalekit’s OAuth authentication. You’ll implement custom middleware for token validation, expose OAuth resource metadata for client discovery, and create MCP tools that enforce authorization using the MCP SDK.

Use this quickstart when you’re building Node.js-based MCP servers and want fine-grained control over request handling. The Express integration gives you flexibility to add custom routes, middleware chains, integrate with existing Express applications, and handle complex authorization requirements. The full code is available on [GitHub](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node).

**Prerequisites**

* A [Scalekit account](https://app.scalekit.com) with permission to manage MCP servers
* **Node.js 20+** installed locally
* Familiarity with Express.js, TypeScript, and OAuth token validation
* Basic understanding of MCP server architecture

Review the Express.js MCP authorization flow

![Express.js MCP server with Scalekit: the client authorizes, gets a bearer token, and calls POST /mcp](/d2/docs/authenticate/mcp/expressjs-quickstart-0.svg)

1. ## Register your MCP server in Scalekit

   [Section titled “Register your MCP server in Scalekit”](#register-your-mcp-server-in-scalekit)

   Create a protected resource entry so Scalekit can issue tokens that your custom Express middleware validates.

   1. Navigate to **[Dashboard](https://app.scalekit.com) > MCP Servers > Add MCP Server**.
   2. Enter a descriptive name (for example, `Greeting MCP`).
   3. Set **Server URL** to `http://localhost:3002/` (keep the trailing slash).
   4. Click **Save** to create the server.

   ![Greeting MCP Register](/.netlify/images?url=_astro%2Fgreeting-mcp-register.C9jsKOBy.png\&w=836\&h=1314\&dpl=6ae471fc143c9a4c27550463)

   When you save, Scalekit displays the OAuth-protected resource metadata. Copy this JSON—you’ll use it in your `.env` file.

   ![Greeting MCP Protected JSON](/.netlify/images?url=_astro%2Fgreeting-protected-json.DaFlRuyP.png\&w=716\&h=860\&dpl=6ae471fc143c9a4c27550463)

2. ## Create your project directory

   [Section titled “Create your project directory”](#create-your-project-directory)

   Set up a clean directory structure for your TypeScript Express project.

   Terminal

   ```bash
   mkdir express-mcp-node
   cd express-mcp-node
   ```

3. ## Add package dependencies

   [Section titled “Add package dependencies”](#add-package-dependencies)

   Create a `package.json` with scripts and all required dependencies for Express, TypeScript, and the MCP SDK.

   Terminal

   ```bash
   1
   cat <<'EOF' > package.json
   2
   {
   3
     "name": "express-mcp-node",
   4
     "version": "1.0.0",
   5
     "type": "module",
   6
     "scripts": {
   7
       "dev": "tsx src/server.ts",
   8
       "build": "tsc",
   9
       "start": "node dist/server.js"
   10
     },
   11
     "dependencies": {
   12
       "@modelcontextprotocol/sdk": "^1.13.0",
   13
       "@scalekit-sdk/node": "^2.0.1",
   14
       "cors": "^2.8.5",
   15
       "dotenv": "^16.4.5",
   16
       "express": "^5.1.0",
   17
       "zod": "^3.25.57"
   18
     },
   19
     "devDependencies": {
   20
       "@types/cors": "^2.8.19",
   21
       "@types/express": "^4.17.21",
   22
       "@types/node": "^20.11.19",
   23
       "tsx": "^4.7.0",
   24
       "typescript": "^5.4.5"
   25
     }
   26
   }
   27
   EOF
   ```

4. ## Configure TypeScript

   [Section titled “Configure TypeScript”](#configure-typescript)

   Add a TypeScript configuration file optimized for ES2022 modules and strict type checking.

   Terminal

   ```bash
   1
   cat <<'EOF' > tsconfig.json
   2
   {
   3
     "compilerOptions": {
   4
       "target": "ES2022",
   5
       "module": "ES2022",
   6
       "moduleResolution": "node",
   7
       "esModuleInterop": true,
   8
       "forceConsistentCasingInFileNames": true,
   9
       "strict": false,
   10
       "skipLibCheck": true,
   11
       "resolveJsonModule": true,
   12
       "outDir": "dist",
   13
       "rootDir": "src",
   14
       "types": ["node"]
   15
     },
   16
     "include": ["src/**/*"]
   17
   }
   18
   EOF
   ```

5. ## Install dependencies

   [Section titled “Install dependencies”](#install-dependencies)

   Install all packages declared in `package.json`.

   Terminal

   ```bash
   npm install
   ```

   Package manager choice

   This guide uses `npm`, but you can also use `yarn`, `pnpm`, or `bun` if you prefer.

6. ## Configure environment variables

   [Section titled “Configure environment variables”](#configure-environment-variables)

   Create a `.env` file with your Scalekit credentials and the protected resource metadata from step 1.

   Terminal

   ```bash
   cat <<'EOF' > .env
   PORT=3002
   SK_ENV_URL=https://.scalekit.com
   SK_CLIENT_ID=
   SK_CLIENT_SECRET=
   MCP_SERVER_ID=
   PROTECTED_RESOURCE_METADATA=''
   EXPECTED_AUDIENCE=http://localhost:3002/
   EOF


   open .env
   ```

   | Variable                      | Description                                                                                                          |
   | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
   | `PORT`                        | Local port for the Express server. Must match the Server URL registered in Scalekit (defaults to `3002`).            |
   | `SK_ENV_URL`                  | Your Scalekit environment URL from **Dashboard > Settings > API Credentials**                                        |
   | `SK_CLIENT_ID`                | Client ID from **Dashboard > Settings > API Credentials**. Used with `SK_CLIENT_SECRET` to initialize the SDK.       |
   | `SK_CLIENT_SECRET`            | Client secret from **Dashboard > Settings > API Credentials**. Keep this secret and rotate regularly.                |
   | `MCP_SERVER_ID`               | The MCP server ID (`res_...`) from **Dashboard > MCP Servers**. Used to build the token issuer.                      |
   | `PROTECTED_RESOURCE_METADATA` | The complete OAuth resource metadata JSON from step 1. Clients use this to discover authorization requirements.      |
   | `EXPECTED_AUDIENCE`           | The audience value that tokens must include. Should match your server’s public URL (e.g., `http://localhost:3002/`). |

   Protect your credentials

   Never commit `.env` to version control. Add it to `.gitignore` immediately and use a secret manager in production (e.g., AWS Secrets Manager, HashiCorp Vault, or your deployment platform’s secrets service).

7. ## Implement the Express MCP server

   [Section titled “Implement the Express MCP server”](#implement-the-express-mcp-server)

   This server validates tokens whose issuer (`iss`) is `/resources/`. If you see the **Resource bound issuer** option on your MCP server in the Scalekit dashboard, your workspace was created before October 21, 2026. Until you enable that option, your tokens carry your environment URL as the issuer, and this server rejects them with a `401`. Follow [Migrate to the new MCP spec](/authenticate/mcp/migrate-to-2026-07-28-mcp-spec/) first.

   Create `src/server.ts` with the complete server implementation. This includes the Scalekit client initialization, authentication middleware for token validation, CORS configuration, and the greeting MCP tool.

   src/server.ts

   ```typescript
   6 collapsed lines
   1
   import 'dotenv/config';
   2
   import cors from 'cors';
   3
   import express, { NextFunction, Request, Response } from 'express';
   4
   import { z } from 'zod';
   5
   import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
   6
   import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
   7
   import { Scalekit } from '@scalekit-sdk/node';
   8


   9
   // Load environment variables
   10
   const PORT = Number(process.env.PORT ?? 3002);
   11
   const SK_ENV_URL = process.env.SK_ENV_URL ?? '';
   12
   const SK_CLIENT_ID = process.env.SK_CLIENT_ID ?? '';
   13
   const SK_CLIENT_SECRET = process.env.SK_CLIENT_SECRET ?? '';
   14
   const MCP_SERVER_ID = process.env.MCP_SERVER_ID ?? '';
   15
   const EXPECTED_AUDIENCE = process.env.EXPECTED_AUDIENCE ?? '';
   16
   const PROTECTED_RESOURCE_METADATA = process.env.PROTECTED_RESOURCE_METADATA ?? '';
   17


   18
   // Use case: Configure OAuth resource metadata URL for MCP clients
   19
   // This allows MCP clients to discover authorization requirements via WWW-Authenticate header
   20
   // Security: The WWW-Authenticate header signals to clients where to obtain tokens
   21
   const RESOURCE_METADATA_URL = `http://localhost:${PORT}/.well-known/oauth-protected-resource`;
   22


   23
   // WWW-Authenticate header for 401 responses
   24
   const WWW_HEADER_KEY = 'WWW-Authenticate';
   25
   const WWW_HEADER_VALUE = `Bearer realm="OAuth", resource_metadata="${RESOURCE_METADATA_URL}"`;
   26


   27
   // Initialize Scalekit client for token validation
   28
   // Security: Use SDK to validate JWT signatures and claims
   29
   // This prevents accepting forged or tampered tokens
   30
   const scalekit = new Scalekit(SK_ENV_URL, SK_CLIENT_ID, SK_CLIENT_SECRET);
   31


   32
   // Initialize MCP server with greeting tool
   33
   // Context: The McpServer handles MCP protocol details while Express handles HTTP routing
   34
   const server = new McpServer({ name: 'Greeting MCP', version: '1.0.0' });
   35


   36
   // Use case: Simple greeting tool demonstrating OAuth-protected MCP operations
   37
   // Context: This tool is protected by the authentication middleware applied to all routes
   38
   server.tool(
   39
     'greet_user',
   40
     'Greets the user with a personalized message.',
   41
     {
   42
       name: z.string().min(1, 'Name is required'),
   43
     },
   44
     async ({ name }: { name: string }) => ({
   45
       content: [
   46
         {
   47
           type: 'text',
   48
           text: `Hi ${name}, welcome to Scalekit!`
   49
         }
   50
       ]
   51
     })
   52
   );
   53


   54
   // Initialize Express application
   55
   const app = express();
   56


   57
   // Enable CORS for cross-origin MCP clients
   58
   // Use case: Allow MCP clients from different origins to connect
   59
   app.use(cors({ origin: true, credentials: false }));
   60


   61
   // Parse JSON request bodies
   62
   // Context: MCP protocol uses JSON-RPC format
   63
   app.use(express.json());
   64


   65
   // Use case: Expose OAuth resource metadata for MCP client discovery
   66
   // This endpoint allows clients to discover authorization requirements and server capabilities
   67
   // Context: MCP clients use this metadata to initiate the OAuth flow
   68
   app.get('/.well-known/oauth-protected-resource', (_req: Request, res: Response) => {
   69
     if (!PROTECTED_RESOURCE_METADATA) {
   70
       res.status(500).json({ error: 'PROTECTED_RESOURCE_METADATA config missing' });
   71
       return;
   72
     }
   73


   74
     const metadata = JSON.parse(PROTECTED_RESOURCE_METADATA);
   75
     res.type('application/json').send(JSON.stringify(metadata, null, 2));
   76
   });
   77


   78
   // Use case: Health check endpoint for monitoring and load balancers
   79
   // Context: Keep this separate from protected endpoints for deployment health checks
   80
   app.get('/health', (_req: Request, res: Response) => {
   81
     res.json({ status: 'healthy' });
   82
   });
   83


   84
   // Security: Validate Bearer tokens on all protected endpoints
   85
   // Public endpoints (health, metadata) are exempt from authentication
   86
   // This prevents unauthorized access to MCP tools and operations
   87
   app.use(async (req: Request, res: Response, next: NextFunction) => {
   88
     // Allow public endpoints without authentication
   89
     // Use case: Health checks for monitoring; metadata for client discovery
   90
     if (req.path === '/.well-known/oauth-protected-resource' || req.path === '/health') {
   91
       next();
   92
       return;
   93
     }
   94


   95
     // Extract Bearer token from Authorization header
   96
     // Use case: OAuth 2.1 Bearer token format (RFC 6750)
   97
     // Security: Reject requests without valid Bearer token prefix
   98
     const header = req.headers.authorization;
   99
     const token = header?.startsWith('Bearer ')
   100
       ? header.slice('Bearer '.length).trim()
   101
       : undefined;
   102


   103
     if (!token) {
   104
       res.status(401)
   105
         .set(WWW_HEADER_KEY, WWW_HEADER_VALUE)
   106
         .json({ error: 'Missing Bearer token' });
   107
       return;
   108
     }
   109


   110
     try {
   111
       // Validate token using Scalekit SDK
   112
       // Security: Verifies signature, expiration, issuer, and audience claims
   113
       // Context: This critical step prevents accepting tokens from other issuers
   114
       await scalekit.validateToken(token, { issuer: `${SK_ENV_URL}/resources/${MCP_SERVER_ID}`, audience: [EXPECTED_AUDIENCE] });
   115
       next();
   116
     } catch (error) {
   117
       res.status(401)
   118
         .set(WWW_HEADER_KEY, WWW_HEADER_VALUE)
   119
         .json({ error: 'Token validation failed' });
   120
     }
   121
   });
   122


   123
   // Handle MCP protocol requests at root path
   124
   // Use case: Process authenticated MCP tool requests using StreamableHTTPServerTransport
   125
   // Context: The transport layer handles MCP JSON-RPC communication
   126
   app.post('/', async (req: Request, res: Response) => {
   127
     const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
   128
     await server.connect(transport);
   129


   130
     try {
   131
       await transport.handleRequest(req, res, req.body);
   132
     } catch (error) {
   133
       res.status(500).json({ error: 'MCP transport error' });
   134
     }
   135
   });
   136


   137
   // Start the Express server
   138
   app.listen(PORT, () => {
   139
     console.log(`MCP server running on http://localhost:${PORT}`);
   140
   });
   ```

8. ## Start the Express server

   [Section titled “Start the Express server”](#start-the-express-server)

   Start the Express server in development mode with auto-reload enabled. The server will listen on `http://localhost:3002/` and display logs indicating Express is ready to receive authenticated MCP requests.

   Terminal

   ```bash
   npm run dev
   ```

   The server starts on `http://localhost:3002/` and logs indicate Express is ready. The MCP endpoint at `/` accepts authenticated POST requests, and the metadata endpoint is accessible at `/.well-known/oauth-protected-resource`.

   Production deployment

   For production deployment, build the TypeScript code with `npm run build`, then start the compiled server with `npm start` behind a reverse proxy like Nginx or use a process manager like PM2.

9. ## Connect with MCP Inspector

   [Section titled “Connect with MCP Inspector”](#connect-with-mcp-inspector)

   Test your server end-to-end using the MCP Inspector to verify the OAuth flow works correctly. This allows you to see the authentication handshake and test calling your MCP tools with validated tokens.

   Terminal

   ```bash
   npx @modelcontextprotocol/inspector@latest
   ```

   In the Inspector UI:

   1. Enter your MCP Server URL: `http://localhost:3002/`
   2. Click **Connect** to initiate the OAuth flow
   3. Authenticate with Scalekit when prompted
   4. Run the `greet_user` tool with any name

   ![MCP Inspector](/.netlify/images?url=_astro%2Fmcp-inspector-google.B0jhj-ep.png\&w=3022\&h=1318\&dpl=6ae471fc143c9a4c27550463)

   Debugging token validation

   The middleware validates every request’s token. If you see authentication errors: verify environment variables match dashboard settings, confirm the token audience matches `EXPECTED_AUDIENCE`, and check token expiration in the Inspector network tab.

You now have a working Express.js MCP server with Scalekit-protected OAuth authentication. Extend this implementation by adding more MCP tools using `server.tool()` with Zod schema validation, implementing scope-based authorization using custom middleware, integrating with your existing Express application, or adding features like rate limiting and request logging using Express’s middleware ecosystem.

---
# DOCUMENT BOUNDARY
---

# FastAPI + FastMCP quickstart

> Build a production-ready MCP server with FastAPI custom middleware for OAuth token validation and Scalekit authentication.

This guide shows you how to build a production-ready FastAPI + FastMCP server with Scalekit’s OAuth authentication. You’ll implement custom middleware for token validation, expose OAuth resource metadata for client discovery, and create MCP tools that enforce authorization.

Use this quickstart when you need more control over your server’s behavior than FastMCP’s built-in provider offers. The FastAPI integration gives you flexibility to add custom middleware, implement additional endpoints, integrate with existing FastAPI applications, and handle complex authorization requirements. The full code is available on [GitHub](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python).

**Prerequisites**

* A [Scalekit account](https://app.scalekit.com) with permission to manage MCP servers
* **Python 3.11+** installed locally
* Familiarity with FastAPI and OAuth token validation
* Basic understanding of MCP server architecture

Review the FastAPI + FastMCP authorization flow

![FastAPI and FastMCP server with Scalekit: the client authorizes, gets a bearer token, and calls tools](/d2/docs/authenticate/mcp/fastapi-fastmcp-quickstart-0.svg)

1. ## Register your MCP server in Scalekit

   [Section titled “Register your MCP server in Scalekit”](#register-your-mcp-server-in-scalekit)

   Create a protected resource entry so Scalekit can issue tokens that your custom FastAPI middleware validates.

   1. Navigate to **[Dashboard](https://app.scalekit.com) > MCP Servers > Add MCP Server**.
   2. Enter a descriptive name (for example, `Greeting MCP`).
   3. Set **Server URL** to `http://localhost:3002/` (keep the trailing slash).
   4. Click **Save** to create the server.

   ![Greeting MCP Register](/.netlify/images?url=_astro%2Fgreeting-mcp-register.C9jsKOBy.png\&w=836\&h=1314\&dpl=6ae471fc143c9a4c27550463)

   When you save, Scalekit displays the OAuth-protected resource metadata. Copy this JSON—you’ll use it in your `.env` file.

   ![Greeting MCP Protected JSON](/.netlify/images?url=_astro%2Fgreeting-protected-json.DaFlRuyP.png\&w=716\&h=860\&dpl=6ae471fc143c9a4c27550463)

2. ## Create your project directory

   [Section titled “Create your project directory”](#create-your-project-directory)

   Set up a clean directory structure with a Python virtual environment to isolate FastAPI and FastMCP dependencies.

   Terminal

   ```bash
   mkdir fastapi-mcp-python
   cd fastapi-mcp-python
   python3 -m venv .venv
   source .venv/bin/activate
   ```

3. ## Add dependencies

   [Section titled “Add dependencies”](#add-dependencies)

   Create a `requirements.txt` file with all required packages and install them.

   Terminal

   ```bash
   cat <<'EOF' > requirements.txt
   mcp>=1.0.0
   fastapi>=0.104.0
   fastmcp>=0.8.0
   uvicorn>=0.24.0
   pydantic>=2.5.0
   python-dotenv>=1.0.0
   httpx>=0.25.0
   python-jose[cryptography]>=3.3.0
   cryptography>=41.0.0
   scalekit-sdk-python>=2.4.0
   starlette>=0.27.0
   EOF


   pip install -r requirements.txt
   ```

   Version pinning

   Pin exact versions in production to ensure reproducible builds and avoid unexpected breaking changes.

4. ## Configure environment variables

   [Section titled “Configure environment variables”](#configure-environment-variables)

   Create a `.env` file with your Scalekit credentials and the protected resource metadata from step 1.

   Terminal

   ```bash
   cat <<'EOF' > .env
   PORT=3002
   SK_ENV_URL=https://.scalekit.com
   SK_CLIENT_ID=
   SK_CLIENT_SECRET=
   MCP_SERVER_ID=
   PROTECTED_RESOURCE_METADATA=''
   EXPECTED_AUDIENCE=http://localhost:3002/
   EOF


   open .env
   ```

   | Variable                      | Description                                                                                                          |
   | ----------------------------- | -------------------------------------------------------------------------------------------------------------------- |
   | `PORT`                        | Local port for the FastAPI server. Must match the Server URL registered in Scalekit (defaults to `3002`).            |
   | `SK_ENV_URL`                  | Your Scalekit environment URL from **Dashboard > Settings > API Credentials**                                        |
   | `SK_CLIENT_ID`                | Client ID from **Dashboard > Settings > API Credentials**. Used with `SK_CLIENT_SECRET` to initialize the SDK.       |
   | `SK_CLIENT_SECRET`            | Client secret from **Dashboard > Settings > API Credentials**. Keep this secret and rotate regularly.                |
   | `MCP_SERVER_ID`               | The MCP server ID (`res_...`) from **Dashboard > MCP Servers**. Used to build the token issuer.                      |
   | `PROTECTED_RESOURCE_METADATA` | The complete OAuth resource metadata JSON from step 1. Clients use this to discover authorization requirements.      |
   | `EXPECTED_AUDIENCE`           | The audience value that tokens must include. Should match your server’s public URL (e.g., `http://localhost:3002/`). |

   Protect your credentials

   Never commit `.env` to version control. Add it to `.gitignore` immediately and use a secret manager in production (e.g., AWS Secrets Manager, HashiCorp Vault, or your deployment platform’s secrets service).

5. ## Implement the FastAPI + FastMCP server

   [Section titled “Implement the FastAPI + FastMCP server”](#implement-the-fastapi--fastmcp-server)

   This server validates tokens whose issuer (`iss`) is `/resources/`. If you see the **Resource bound issuer** option on your MCP server in the Scalekit dashboard, your workspace was created before October 21, 2026. Until you enable that option, your tokens carry your environment URL as the issuer, and this server rejects them with a `401`. Follow [Migrate to the new MCP spec](/authenticate/mcp/migrate-to-2026-07-28-mcp-spec/) first.

   Create `main.py` with the complete server implementation. This includes the Scalekit client initialization, authentication middleware for token validation, CORS configuration, and the greeting MCP tool.

   main.py

   ```python
   10 collapsed lines
   1
   import json
   2
   import os
   3
   from fastapi import FastAPI, Request, Response
   4
   from fastmcp import FastMCP, Context
   5
   from scalekit import ScalekitClient
   6
   from scalekit.common.scalekit import TokenValidationOptions
   7
   from starlette.middleware.cors import CORSMiddleware
   8
   from dotenv import load_dotenv
   9


   10
   load_dotenv()
   11


   12
   # Load environment variables
   13
   PORT = int(os.getenv("PORT", "3002"))
   14
   SK_ENV_URL = os.getenv("SK_ENV_URL", "")
   15
   SK_CLIENT_ID = os.getenv("SK_CLIENT_ID", "")
   16
   SK_CLIENT_SECRET = os.getenv("SK_CLIENT_SECRET", "")
   17
   MCP_SERVER_ID = os.getenv("MCP_SERVER_ID", "")
   18
   EXPECTED_AUDIENCE = os.getenv("EXPECTED_AUDIENCE", "")
   19
   PROTECTED_RESOURCE_METADATA = os.getenv("PROTECTED_RESOURCE_METADATA", "")
   20


   21
   # Use case: Configure OAuth resource metadata URL for MCP clients
   22
   # This allows MCP clients to discover authorization requirements via WWW-Authenticate header
   23
   # Security: The WWW-Authenticate header signals to clients where to obtain tokens
   24
   RESOURCE_METADATA_URL = f"http://localhost:{PORT}/.well-known/oauth-protected-resource"
   25
   WWW_HEADER = {
   26
       "WWW-Authenticate": f'Bearer realm="OAuth", resource_metadata="{RESOURCE_METADATA_URL}"'
   27
   }
   28


   29
   # Initialize Scalekit client for token validation
   30
   # Security: Use SDK to validate JWT signatures and claims
   31
   # This prevents accepting forged or tampered tokens
   32
   scalekit_client = ScalekitClient(
   33
       env_url=SK_ENV_URL,
   34
       client_id=SK_CLIENT_ID,
   35
       client_secret=SK_CLIENT_SECRET,
   36
   )
   37


   38
   # Initialize FastMCP with stateless HTTP transport
   39
   # HTTP transport allows MCP clients to connect via standard OAuth flows
   40
   mcp = FastMCP("Greeting MCP", stateless_http=True)
   41


   42


   43
   @mcp.tool(
   44
       name="greet_user",
   45
       description="Greets the user with a personalized message."
   46
   )
   47
   async def greet_user(name: str, ctx: Context | None = None) -> dict:
   48
       """
   49
       Use case: Simple greeting tool demonstrating OAuth-protected MCP operations
   50
       Context: This tool is protected by the authentication middleware
   51
       """
   52
       return {
   53
           "content": [
   54
               {
   55
                   "type": "text",
   56
                   "text": f"Hi {name}, welcome to Scalekit!"
   57
               }
   58
           ]
   59
       }
   60


   61


   62
   # Mount FastMCP as a FastAPI application
   63
   # Context: This allows us to layer FastAPI middleware on top of FastMCP
   64
   mcp_app = mcp.http_app(path="/")
   65
   app = FastAPI(lifespan=mcp_app.lifespan)
   66


   67
   # Enable CORS for cross-origin MCP clients
   68
   # Use case: Allow MCP clients from different origins to connect
   69
   app.add_middleware(
   70
       CORSMiddleware,
   71
       allow_origins=["*"],
   72
       allow_credentials=True,
   73
       allow_methods=["GET", "POST", "OPTIONS"],
   74
       allow_headers=["*"]
   75
   )
   76


   77


   78
   @app.middleware("http")
   79
   async def auth_middleware(request: Request, call_next):
   80
       """
   81
       Security: Validate Bearer tokens on all protected endpoints.
   82
       Public endpoints (health, metadata) are exempt from authentication.
   83
       This prevents unauthorized access to MCP tools and operations.
   84
       """
   85
       # Allow public endpoints without authentication
   86
       # Use case: Health checks for monitoring; metadata for client discovery
   87
       if request.url.path in {"/health", "/.well-known/oauth-protected-resource"}:
   88
           return await call_next(request)
   89


   90
       # Extract Bearer token from Authorization header
   91
       # Use case: OAuth 2.1 Bearer token format (RFC 6750)
   92
       # Security: Reject requests without valid Bearer token prefix
   93
       auth_header = request.headers.get("authorization")
   94
       if not auth_header or not auth_header.startswith("Bearer "):
   95
           return Response(
   96
               '{"error": "Missing Bearer token"}',
   97
               status_code=401,
   98
               headers=WWW_HEADER,
   99
               media_type="application/json"
   100
           )
   101


   102
       token = auth_header.split("Bearer ", 1)[1].strip()
   103


   104
       # Validate token using Scalekit SDK
   105
       # Security: Verifies signature, expiration, issuer, and audience claims
   106
       # Context: This critical step prevents accepting tokens from other issuers
   107
       options = TokenValidationOptions(
   108
           issuer=f"{SK_ENV_URL}/resources/{MCP_SERVER_ID}",
   109
           audience=[EXPECTED_AUDIENCE]
   110
       )
   111


   112
       try:
   113
           is_valid = scalekit_client.validate_access_token(token, options=options)
   114
           if not is_valid:
   115
               raise ValueError("Invalid token")
   116
       except Exception:
   117
           return Response(
   118
               '{"error": "Token validation failed"}',
   119
               status_code=401,
   120
               headers=WWW_HEADER,
   121
               media_type="application/json"
   122
           )
   123


   124
       # Token is valid, proceed with request
   125
       # This allows MCP clients to call tools with authenticated context
   126
       return await call_next(request)
   127


   128


   129
   @app.get("/.well-known/oauth-protected-resource")
   130
   async def oauth_metadata():
   131
       """
   132
       Use case: Expose OAuth resource metadata for MCP client discovery
   133
       This endpoint allows clients to discover authorization requirements and server capabilities
   134
       Context: MCP clients use this metadata to initiate the OAuth flow
   135
       """
   136
       if not PROTECTED_RESOURCE_METADATA:
   137
           return Response(
   138
               '{"error": "PROTECTED_RESOURCE_METADATA config missing"}',
   139
               status_code=500,
   140
               media_type="application/json"
   141
           )
   142


   143
       metadata = json.loads(PROTECTED_RESOURCE_METADATA)
   144
       return Response(
   145
           json.dumps(metadata, indent=2),
   146
           media_type="application/json"
   147
       )
   148


   149


   150
   @app.get("/health")
   151
   async def health_check():
   152
       """
   153
       Use case: Health check endpoint for monitoring and load balancers
   154
       Context: Keep this separate from protected endpoints for deployment health checks
   155
       """
   156
       return {"status": "healthy"}
   157


   158


   159
   # Mount the FastMCP application at root path
   160
   app.mount("/", mcp_app)
   161


   162


   163
   if __name__ == "__main__":
   164
       import uvicorn
   165
       # Start server with auto-reload for development
   166
       # Production: Use 'uvicorn main:app --host 0.0.0.0 --port 3002 --workers 4' behind a reverse proxy
   167
       uvicorn.run(app, host="0.0.0.0", port=PORT)
   ```

6. ## Start the FastAPI server

   [Section titled “Start the FastAPI server”](#start-the-fastapi-server)

   Start the FastAPI server in development mode with auto-reload enabled. The server will listen on `http://localhost:3002/` and display logs indicating FastAPI is ready to receive authenticated MCP requests.

   Terminal

   ```bash
   python main.py
   ```

   The server starts on `http://localhost:3002/` and logs indicate FastAPI is ready. The MCP endpoint accepts authenticated requests, and the metadata endpoint is accessible at `/.well-known/oauth-protected-resource`.

   Production deployment

   During development, Uvicorn’s auto-reload watches for file changes. For production, use `uvicorn main:app —host 0.0.0.0 —port 3002 —workers 4` behind a reverse proxy like Nginx.

7. ## Connect with MCP Inspector

   [Section titled “Connect with MCP Inspector”](#connect-with-mcp-inspector)

   Test your server end-to-end using the MCP Inspector to verify the OAuth flow works correctly. This allows you to see the authentication handshake and test calling your MCP tools with validated tokens.

   Terminal

   ```bash
   npx @modelcontextprotocol/inspector@latest
   ```

   In the Inspector UI:

   1. Enter your MCP Server URL: `http://localhost:3002/`
   2. Click **Connect** to initiate the OAuth flow
   3. Authenticate with Scalekit when prompted
   4. Run the `greet_user` tool with any name

   ![MCP Inspector](/.netlify/images?url=_astro%2Fmcp-inspector-google.B0jhj-ep.png\&w=3022\&h=1318\&dpl=6ae471fc143c9a4c27550463)

   Debugging token validation

   The middleware validates every request’s token. If you see authentication errors: verify environment variables match dashboard settings, confirm the token audience matches `EXPECTED_AUDIENCE`, and check token expiration in the Inspector network tab.

You now have a working FastAPI + FastMCP server with Scalekit-protected OAuth authentication. Extend this implementation by adding more MCP tools with the `@mcp.tool` decorator, implementing scope-based authorization using custom middleware, integrating with your existing FastAPI application, or adding features like rate limiting and request logging using FastAPI’s middleware pipeline.

---
# DOCUMENT BOUNDARY
---

# FastMCP quickstart

> FastMCP todo server with OAuth scope validation and CRUD operations.

This guide shows you how to build a production-ready FastMCP server protected by Scalekit’s OAuth authentication. You’ll register your server as a protected resource, implement scope-based authorization for CRUD operations, and validate tokens on every request.

Use this quickstart to experience a working reference implementation with a simple todo application. The todo app demonstrates how to enforce `todo:read` and `todo:write` scopes across multiple tools. After completing this guide, you can apply the same authentication pattern to secure your own FastMCP tools. The full code is available on [GitHub](https://github.com/scalekit-inc/mcp-demo/tree/main/todo-fastmcp).

**Prerequisites**

* A [Scalekit account](https://app.scalekit.com) with permission to manage MCP servers
* **Python 3.11+** installed locally
* Familiarity with OAuth scopes and basic terminal commands

Review the FastMCP authorization flow

![FastMCP server with Scalekit: the client authorizes, gets a bearer token, and calls the todo tools](/d2/docs/authenticate/mcp/fastmcp-quickstart-0.svg)

1. ## Register your MCP server in Scalekit

   [Section titled “Register your MCP server in Scalekit”](#register-your-mcp-server-in-scalekit)

   Create a protected resource entry so Scalekit can issue scoped tokens that FastMCP validates on every request.

   1. Navigate to **[Dashboard](https://app.scalekit.com) > MCP Servers > Add MCP Server**.
   2. Enter a descriptive name (for example, `FastMCP Todo Server`).
   3. Set **Server URL** to `http://localhost:3002/` (keep the trailing slash). This field is a required.\
      For a server running at `http://localhost:3002/mcp`, register `http://localhost:3002/`. FastMCP appends `/mcp` automatically, so always provide the base URL with a trailing slash.
   4. Create or link the scopes below, then click **Save**.

   ![Register FastMCP server](/.netlify/images?url=_astro%2Fregister-fastmcp.yj75FoPt.png\&w=772\&h=1316\&dpl=6ae471fc143c9a4c27550463)

   | Scope        | Description                                  | Required |
   | ------------ | -------------------------------------------- | -------- |
   | `todo:read`  | Grants read access to todo tasks             | Yes      |
   | `todo:write` | Allows creating, updating, or deleting tasks | Yes      |

2. ## Create your FastMCP todo server

   [Section titled “Create your FastMCP todo server”](#create-your-fastmcp-todo-server)

   Prepare a fresh directory and virtual environment to keep FastMCP dependencies isolated.

   Terminal

   ```bash
   mkdir -p fastmcp-todo
   cd fastmcp-todo
   python3 -m venv venv
   source venv/bin/activate
   ```

3. ## Add dependencies and configuration templates

   [Section titled “Add dependencies and configuration templates”](#add-dependencies-and-configuration-templates)

   Create the support files that FastMCP and Scalekit expect, then install the required libraries.

   Terminal

   ```bash
   cat <<'EOF' > requirements.txt
   fastmcp>=2.13.0.2
   python-dotenv>=1.0.0
   EOF


   pip install -r requirements.txt


   cat <<'EOF' > env.example
   PORT=3002
   SCALEKIT_ENVIRONMENT_URL=https://your-environment-url.scalekit.com
   SCALEKIT_CLIENT_ID=your_client_id
   SCALEKIT_RESOURCE_ID=mcp_server_id
   MCP_URL=http://localhost:3002/
   EOF
   ```

   Check in templates, not secrets

   Keep `env.example` under version control so teammates know which variables to supply, but never commit the populated `.env` file.

4. ## Implement the FastMCP todo server

   [Section titled “Implement the FastMCP todo server”](#implement-the-fastmcp-todo-server)

   Copy the following code into `server.py`. It registers the Scalekit provider, defines an in-memory todo store, and exposes CRUD tools guarded by OAuth scopes.

   server.py

   ```python
   15 collapsed lines
   1
   """Scalekit-authenticated FastMCP server providing in-memory CRUD tools for todos.
   2


   3
   This example demonstrates how to protect FastMCP tools with OAuth scopes.
   4
   Each tool validates the required scope before executing operations.
   5
   """
   6


   7
   import os
   8
   import uuid
   9
   from dataclasses import dataclass, asdict
   10
   from typing import Optional
   11


   12
   from dotenv import load_dotenv
   13
   from fastmcp import FastMCP
   14
   from fastmcp.server.auth.providers.scalekit import ScalekitProvider
   15
   from fastmcp.server.dependencies import AccessToken, get_access_token
   16


   17
   load_dotenv()
   18


   19
   # Use case: Configure FastMCP server with OAuth protection
   20
   # Security: Scalekit provider validates every request's Bearer token
   21
   mcp = FastMCP(
   22
       "Todo Server",
   23
       stateless_http=True,
   24
       auth=ScalekitProvider(
   25
           environment_url=os.getenv("SCALEKIT_ENVIRONMENT_URL"),
   26
           client_id=os.getenv("SCALEKIT_CLIENT_ID"),
   27
           resource_id=os.getenv("SCALEKIT_RESOURCE_ID"),
   28
           # FastMCP appends /mcp automatically; keep base URL with trailing slash only
   29
           mcp_url=os.getenv("MCP_URL"),
   30
       ),
   31
   )
   32


   33


   34
   @dataclass
   35
   class TodoItem:
   36
       id: str
   37
       title: str
   38
       description: Optional[str]
   39
       completed: bool = False
   40


   41
       def to_dict(self) -> dict:
   42
           return asdict(self)
   43


   44


   45
   # Use case: In-memory storage for demo purposes
   46
   # Production: Replace with your database or persistent storage
   47
   _TODO_STORE: dict[str, TodoItem] = {}
   48


   49


   50
   def _require_scope(scope: str) -> Optional[str]:
   51
       """
   52
       Security: Validate that the current request's token includes the required scope.
   53
       This prevents unauthorized access to protected operations.
   54
       """
   55
       token: AccessToken = get_access_token()
   56
       if scope not in token.scopes:
   57
           return f"Insufficient permissions: `{scope}` scope required."
   58
       return None
   59


   60


   61
   @mcp.tool
   62
   def create_todo(title: str, description: Optional[str] = None) -> dict:
   63
       """
   64
       Use case: Create a new todo item for task tracking
   65
       Requires: todo:write scope
   66
       """
   67
       error = _require_scope("todo:write")
   68
       if error:
   69
           return {"error": error}
   70


   71
       todo = TodoItem(id=str(uuid.uuid4()), title=title, description=description)
   72
       _TODO_STORE[todo.id] = todo
   73
       return {"todo": todo.to_dict()}
   74


   75


   76
   @mcp.tool
   77
   def list_todos(completed: Optional[bool] = None) -> dict:
   78
       """
   79
       Use case: Retrieve all todos, optionally filtered by completion status
   80
       Requires: todo:read scope
   81
       """
   82
       error = _require_scope("todo:read")
   83
       if error:
   84
           return {"error": error}
   85


   86
       todos = [
   87
           todo.to_dict()
   88
           for todo in _TODO_STORE.values()
   89
           if completed is None or todo.completed == completed
   90
       ]
   91
       return {"todos": todos}
   92


   93


   94
   @mcp.tool
   95
   def get_todo(todo_id: str) -> dict:
   96
       """
   97
       Use case: Retrieve a specific todo by ID
   98
       Requires: todo:read scope
   99
       """
   100
       error = _require_scope("todo:read")
   101
       if error:
   102
           return {"error": error}
   103


   104
       todo = _TODO_STORE.get(todo_id)
   105
       if todo is None:
   106
           return {"error": f"Todo `{todo_id}` not found."}
   107


   108
       return {"todo": todo.to_dict()}
   109


   110


   111
   @mcp.tool
   112
   def update_todo(
   113
       todo_id: str,
   114
       title: Optional[str] = None,
   115
       description: Optional[str] = None,
   116
       completed: Optional[bool] = None,
   117
   ) -> dict:
   118
       """
   119
       Use case: Update existing todo properties or mark as complete
   120
       Requires: todo:write scope
   121
       """
   122
       error = _require_scope("todo:write")
   123
       if error:
   124
           return {"error": error}
   125


   126
       todo = _TODO_STORE.get(todo_id)
   127
       if todo is None:
   128
           return {"error": f"Todo `{todo_id}` not found."}
   129


   130
       if title is not None:
   131
           todo.title = title
   132
       if description is not None:
   133
           todo.description = description
   134
       if completed is not None:
   135
           todo.completed = completed
   136


   137
       return {"todo": todo.to_dict()}
   138


   139


   140
   @mcp.tool
   141
   def delete_todo(todo_id: str) -> dict:
   142
       """
   143
       Use case: Remove a todo from the system
   144
       Requires: todo:write scope
   145
       """
   146
       error = _require_scope("todo:write")
   147
       if error:
   148
           return {"error": error}
   149


   150
       todo = _TODO_STORE.pop(todo_id, None)
   151
       if todo is None:
   152
           return {"error": f"Todo `{todo_id}` not found."}
   153


   154
       return {"deleted": todo_id}
   155


   156


   157
   if __name__ == "__main__":
   158
       # Start HTTP transport server
   159
       mcp.run(transport="http", port=int(os.getenv("PORT", "3002")))
   ```

5. ## Provide runtime secrets

   [Section titled “Provide runtime secrets”](#provide-runtime-secrets)

   Copy the environment template and populate the values from your Scalekit dashboard.

   Terminal

   ```bash
   cp env.example .env
   open .env
   ```

   | Variable                   | Description                                                                                                                          |
   | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
   | `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL, labeled **Environment URL** in **Dashboard > Settings > API Credentials**                             |
   | `SCALEKIT_CLIENT_ID`       | Client ID from **Dashboard > Settings > API Credentials**                                                                            |
   | `SCALEKIT_RESOURCE_ID`     | The resource identifier assigned to your MCP server (starts with `res_`). Find it on your MCP server’s detail page in the dashboard. |
   | `MCP_URL`                  | The base public URL you registered (keep trailing slash, e.g., `http://localhost:3002/`)                                             |
   | `PORT`                     | Local port for FastMCP HTTP transport (defaults to `3002`)                                                                           |

   Store secrets securely

   Avoid committing `.env` to source control. Use your team’s secret manager in production and rotate credentials if they appear in logs or terminal history.

6. ## Run the FastMCP server locally

   [Section titled “Run the FastMCP server locally”](#run-the-fastmcp-server-locally)

   Start the server so it can accept authenticated MCP requests at `/mcp`.

   Terminal

   ```bash
   source venv/bin/activate
   python server.py
   ```

   When the server boots successfully, you’ll see FastMCP announce the HTTP transport and listen on `http://localhost:3002/`, ready to enforce Scalekit-issued tokens.

   ![Run MCP server](/.netlify/images?url=_astro%2Fvenv-activate-fastmcp.UYaMwNRn.png\&w=2986\&h=926\&dpl=6ae471fc143c9a4c27550463)

   Token enforcement

   Every tool in `server.py` calls `_require_scope`. If you see `Insufficient permissions` in responses, verify the caller’s token includes the expected scope.

7. ## Connect with an MCP client

   [Section titled “Connect with an MCP client”](#connect-with-an-mcp-client)

   Use any MCP-compatible client to exercise the todo tools with scoped tokens. During development, the MCP Inspector demonstrates how the Scalekit provider enforces scopes end-to-end.

   Terminal

   ```bash
   npx @modelcontextprotocol/inspector@latest
   ```

   In the Inspector UI, point the client to `http://localhost:3002/mcp` and click **Connect**. The client initiates OAuth authentication with Scalekit. After successful authentication, run any tool—the server exposes `create_todo`, `list_todos`, `get_todo`, `update_todo`, and `delete_todo`.

   ![MCP Inspector](/.netlify/images?url=_astro%2Fmcp-inspector-fastmcp.CcqqKz2X.png\&w=3024\&h=1502\&dpl=6ae471fc143c9a4c27550463)

   Note

   Leave the Inspector’s Authentication fields empty. This quickstart uses dynamic client registration (DCR)

   Testing scope enforcement

   Try calling `create_todo` with a token that only has `todo:read`. The server will reject the request with an insufficient permissions error.

Once you’re satisfied with the quickstart example, extend `server.py` with your own FastMCP tools or replace the in-memory store with your production data source. Scalekit’s provider handles authentication for any toolset you add.

---
# DOCUMENT BOUNDARY
---

# New to MCP?

> Lock down MCP connections with OAuth 2.1 so agents get only the access they need

AI systems are moving beyond chatbots to agents that act in the real world. They handle sensitive data and run complex workflows. As they grow, they need a secure, standard way to connect. The Model Context Protocol (MCP) provides that standard. It defines how AI applications safely discover and use external tools and data.

![MCP server with Scalekit as its OAuth 2.1 server, connecting chat interfaces, IDEs, developer tools and other AI applications](/d2/docs/authenticate/mcp/intro-to-mcp-auth-0.svg)

MCP incorporates OAuth 2.1 authorization mechanisms at the transport level. This enables MCP clients to make secure requests to restricted MCP servers on behalf of resource owners.

| Features                | Benefit                                                                                                                                      |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| Industry standard       | Well-established authorization framework with extensive tooling and ecosystem support                                                        |
| Security best practices | Incorporates improvements over OAuth 2.0, removing deprecated flows and enforcing security measures like PKCE                                |
| Multiple grant types    | Supports different use cases: **Authorization code** for human user scenarios and **Client credentials** for machine-to-machine integrations |
| Ecosystem compatibility | Integrates with existing identity providers and authorization servers                                                                        |

MCP authorization specification overview

This authorization mechanism is based on established specifications listed below, but implements a selected subset of their features to ensure security and interoperability while maintaining simplicity:

* OAuth 2.1
* OAuth 2.0 Authorization Server Metadata (RFC8414)
* OAuth 2.0 Dynamic Client Registration Protocol (RFC7591)
* OAuth 2.0 Protected Resource Metadata (RFC9728)

Quick reference: High-level flow

This simplified diagram shows the key actors and main interactions. Use this for quick reference while scrolling through the detailed flow below.

![MCP client, MCP server, authorization server and user in the OAuth 2.1 flow](/d2/docs/authenticate/mcp/intro-to-mcp-auth-1.svg)

## Complete MCP OAuth 2.1 flow

[Section titled “Complete MCP OAuth 2.1 flow”](#complete-mcp-oauth-21-flow)

Here’s the complete end-to-end authorization flow showing all phases from discovery to token refresh in a single sequence diagram:

![Complete MCP OAuth 2.1 flow: discovery, client registration, user authorization, token exchange and tool calls](/d2/docs/authenticate/mcp/intro-to-mcp-auth-2.svg)

### Understanding the MCP authorization flow

[Section titled “Understanding the MCP authorization flow”](#understanding-the-mcp-authorization-flow)

Discovery phase

1. MCP client attempts to access a protected resource without credentials
2. MCP server responds with `401 Unauthorized` and includes authorization metadata in the `WWW-Authenticate` header
3. Client retrieves resource metadata to identify authorization servers
4. Client discovers authorization server capabilities through the metadata endpoint

Dynamic client registration

5. Client submits registration request with metadata (redirect URIs, application info)
6. Authorization server validates the request and issues client credentials
7. Client stores credentials securely for subsequent authorization requests

Authorization code flow

8. Client generates PKCE code verifier and challenge
9. Client redirects user to authorization server with PKCE challenge
10. User authenticates and grants consent for requested scopes
11. Authorization server redirects back with authorization code
12. Client exchanges code and PKCE verifier for access token
13. Authorization server validates PKCE and issues tokens with granted scopes

Access phase

14. Client includes access token in the Authorization header
15. MCP server validates the token signature and expiration
16. Server checks if token scopes match the required permissions
17. **If token is valid and scope is sufficient**: Server processes the request and returns 200 OK with the requested data
18. **If token is invalid or scope is insufficient**: Server returns 401 Unauthorized or 403 Forbidden error

Token refresh (when needed)

19. Client detects token expiration (through 401 response or token expiry time)
20. Client sends refresh token request to authorization server
21. Authorization server validates refresh token and issues new access tokens
22. Client updates stored tokens and retries the original request

MCP OAuth 2.1 provides secure, standardized authorization for AI agents accessing protected resources. The flow establishes trust, authenticates users, authorizes access, and maintains security throughout the session lifecycle by building each phase on the previous one.

Original diagram reference

For reference, here’s the complete flow diagram showing all phases and interactions in a traditional sequence diagram format:

![MCP OAuth 2.1 Authorization Flow](/.netlify/images?url=_astro%2Fmcp-auth-flow.C_xyzsAR.png\&w=1440\&h=2088\&dpl=6ae471fc143c9a4c27550463)

---
# DOCUMENT BOUNDARY
---

# Production readiness checklist

> A focused checklist for launching your MCP server authentication, with emphasis on custom domain and branding for a production-grade OAuth consent experience.

As you prepare to launch MCP server authentication to production, verify these items before going live.

## Production environment

[Section titled “Production environment”](#production-environment)

**Verify production environment configuration**

Confirm that your environment URL (`SCALEKIT_ENVIRONMENT_URL`), client ID (`SCALEKIT_CLIENT_ID`), and client secret (`SCALEKIT_CLIENT_SECRET`) are correctly set for production, not dev or staging.

## Custom domain (CNAME)

[Section titled “Custom domain (CNAME)”](#custom-domain-cname)

Configuring a custom domain is the highest-impact step for MCP auth. Without it, the OAuth consent screen your users see during authorization displays `yourapp.scalekit.com` (Scalekit’s default domain) instead of your own brand. Complete this section before you test any end-to-end auth flows in production.

**Configure a custom domain in the Scalekit dashboard**

Go to **Dashboard > Auth for SaaS > Customization > Custom Domain** and follow the CNAME setup instructions. CNAME configuration is available only in production environments. See [Branded custom domains](/guides/custom-domain/) for step-by-step instructions.

**Verify SSL certificate provisioning**

After CNAME verification, Scalekit automatically provisions an SSL certificate. Click **Check** in **Dashboard > Auth for SaaS > Customization > Custom Domain** to confirm status. Provisioning can take up to 24 hours. Contact  if it takes longer.

**Update customer-facing authorization URLs to use your custom domain**

Backend API calls work with either URL, but the authorization URL and any other endpoints your users interact with must use your custom domain (for example, `mcp.yourapp.com`) for branding to take effect. Update those URLs in your application code after CNAME setup is complete.

**Confirm the consent screen shows your branded domain**

Open an incognito window, trigger an authorization flow, and verify the OAuth consent screen shows your custom domain (for example, `mcp.yourapp.com`) and not the default Scalekit URL.

## Consent screen branding

[Section titled “Consent screen branding”](#consent-screen-branding)

**Upload your logo and set brand colors**

Go to **Dashboard > Auth for SaaS > Customization > Branding** to upload your logo, set a favicon, and configure background colors. This is what users see during the OAuth authorization flow.

**Set a recognizable MCP server name**

When registering your MCP server in **Dashboard > MCP Servers**, provide a name that users will recognize (for example, `Acme Calendar`, not `mcp-prod-v2`). This name appears on the consent page that MCP hosts display to users when they authorize access.

## Bring your own auth (if applicable)

[Section titled “Bring your own auth (if applicable)”](#bring-your-own-auth-if-applicable)

If you are using a federated authentication setup where Scalekit acts as the OAuth 2.1 layer but delegates user authentication to your existing system, verify these additional items.

**Configure your login endpoint URL in the Scalekit dashboard**

Scalekit redirects users to your login endpoint with a `login_request_id` and `state` parameter. Confirm the endpoint URL is correctly set for your production environment.

**Verify your login endpoint handles the `login_request_id` and `state` parameters**

Your login page must capture both parameters from the redirect and pass them through the authentication flow. Missing either parameter breaks the callback handshake.

**Test the backend-to-backend user handoff**

After authenticating a user, your system POSTs user attributes to Scalekit at `/api/v1/connections//auth-requests//user`. Verify this call succeeds with production credentials and that the `connection_id` (starts with `conn_`) is correct for your production MCP server.

**Verify the state parameter in your callback redirect**

After the user handoff, your system redirects back to Scalekit’s callback URL with the `state` value from step 1. Confirm the values match exactly. Mismatched state is silently rejected and breaks the flow.

**Test the complete federated flow end-to-end**

Trigger a full authorization flow from an MCP client through your login page, the user handoff API call, and the callback redirect, and verify the MCP client receives a valid access token at the end. See [Bring your own auth](/authenticate/mcp/custom-auth/) for implementation details.

## Core auth flows

[Section titled “Core auth flows”](#core-auth-flows)

**Test the human → MCP authorization flow end-to-end**

After CNAME and branding are configured, trigger a complete authorization code flow: from the initial tool call through the `401` challenge, consent screen, token exchange, and authorized response. Run this in production with your live credentials.

**Test the agent → MCP client credentials flow**

Verify that API clients can obtain tokens and call MCP tools successfully. Confirm that client credentials are stored in environment variables or a secrets manager, not hardcoded.

## Monitoring

[Section titled “Monitoring”](#monitoring)

**Enable auth logs and set up alerts**

Monitor **Dashboard > Auth Logs** for repeated `401` errors, invalid scope requests, and token failures after launch. Set up alerts for unusual activity patterns.

After launch, use [Auth logs](/guides/dashboard/auth-logs/) to debug authentication issues and review the [MCP auth troubleshooting guide](/authenticate/mcp/troubleshooting/) for common problems.

---
# DOCUMENT BOUNDARY
---

# Managing MCP Clients

> Manage MCP clients by viewing registered MCP clients, tracking user consent, and revoking access to your MCP servers.

To maintain security and control over your MCP Server, you need to manage which client applications can access it. Scalekit provides several ways for clients to connect, including automatic registration for modern apps and manual pre-registration for custom or trusted clients.

This guide covers the different types of MCP clients and shows you how to:

* View all registered clients
* See which users have granted consent to a client
* Revoke user access for any client

There are three main categories of MCP Clients that can interact with your MCP Server:

## 1. Automatic registration with DCR

[Section titled “1. Automatic registration with DCR”](#1-automatic-registration-with-dcr)

These are MCP Clients that automatically register themselves as OAuth clients. Most modern MCP clients, such as Claude Desktop, OpenAI, VS Code, and Cursor, support Dynamic Client Registration (DCR). They initiate the registration process and start the OAuth Authorization flow with the Scalekit server to obtain an access token without requiring manual configuration.

During the consent flow, users see your **environment domain** as the requesting identifier - not “Scalekit” and not your application name. This is by design: the domain identifies the authorization server handling the request, similar to how Google OAuth shows the requesting domain. To display a custom branded domain, configure a [custom domain](/guides/custom-domain/) for your Scalekit environment.

## 2. Manual client pre-registration

[Section titled “2. Manual client pre-registration”](#2-manual-client-pre-registration)

These are MCP Clients that you manually register in the Scalekit Dashboard. This is useful when you want to restrict access to specific, pre-approved clients or when you are building a custom client that requires fixed credentials. You can create OAuth clients that can either act as themselves or on behalf of the user.

### How to pre-register a client

[Section titled “How to pre-register a client”](#how-to-pre-register-a-client)

If you need to manually register an MCP Client, you can do so in the Scalekit Dashboard.

1. Navigate to the **Clients** section of your MCP Server.

2. Click the **Create Client** button.

   ![Create Client](/_astro/mcp_create_client.lIT_Y1hO.png)

   **Configuration:**

   * **Client name**: A display name (e.g., “My Custom Client”).
   * **Redirect URI**: The URL where the client will redirect users after authorization.

3. **Choosing the right OAuth flow:**

   * **For Client Credentials Flow**: Leave the Redirect URI field empty. Your application will authenticate using only the `client_id` and `client_secret`. This is suitable for server-to-server communication.
   * **For Authorization Code Grant Flow**: Provide one or more Redirect URIs where users will be redirected after granting consent. This is required for user-facing applications that need to act on behalf of users.

   **Cross-App Access (XAA):** XAA doesn’t require any explicit setting - your static clients by default support cross-app access under enterprise plan.

   Once the client is created, you will receive a `client_id` and `client_secret` to configure in your application.

   ![Redirect URI](/_astro/mcp_configure_client.CQDvSRQa.png)

### 2.1 OAuth client credential flow

[Section titled “2.1 OAuth client credential flow”](#21-oauth-client-credential-flow)

Use this flow when your MCP Client needs to act on its own behalf rather than on behalf of a specific user. This is ideal for machine-to-machine communication scenarios.

**When to use:**

* Backend services or server-side applications
* Automated scripts or batch processes
* System integrations that don’t require user interaction
* Applications that need to access resources without user context

**Characteristics:**

* No user interaction required
* No redirect URI needed
* Client authenticates using `client_id` and `client_secret`
* Access token represents the client itself

### 2.2 OAuth authorization code grant flow

[Section titled “2.2 OAuth authorization code grant flow”](#22-oauth-authorization-code-grant-flow)

Use this flow when your MCP Client needs to act on behalf of a user. This is the standard OAuth flow that requires user consent.

**When to use:**

* User-facing applications (web, desktop, or mobile)
* Applications that need to access user-specific resources
* Scenarios requiring explicit user consent
* Applications where actions should be attributed to specific users

**Characteristics:**

* Requires user authentication and consent
* Redirect URI is mandatory
* Client receives authorization code, exchanges it for access token
* Access token represents the user’s authorization

## 3. Registration via metadata URL (CIMD)

[Section titled “3. Registration via metadata URL (CIMD)”](#3-registration-via-metadata-url-cimd)

These are MCP Clients that support Client ID Metadata Document (CIMD), an OAuth 2.0 mechanism that allows clients to use a URL as their client identifier. When a CIMD-compatible client initiates the OAuth flow, Scalekit fetches the client’s metadata (such as name, redirect URIs, and other registration information) from the provided URL. This provides an alternative registration method without requiring manual pre-registration or Dynamic Client Registration, making it easier for clients to authenticate across different authorization servers.

## Manage registered clients

[Section titled “Manage registered clients”](#manage-registered-clients)

### View all registered clients

[Section titled “View all registered clients”](#view-all-registered-clients)

You can view a list of all MCP Clients that have been registered with your MCP Server (both DCR and pre-registered) in the Scalekit Dashboard.

1. Go to your MCP Server in the dashboard.
2. Click on the **Clients** tab.

![View all MCP Clients](/.netlify/images?url=_astro%2Fview_all_clients.ClEAh2pi.png\&w=2544\&h=896\&dpl=6ae471fc143c9a4c27550463)

### View consented users

[Section titled “View consented users”](#view-consented-users)

For each registered MCP Client that uses the OAuth Authorization Code Grant Flow, you can view all users who have granted consent.

1. From the **Clients** list, click on a specific client.
2. Navigate to the **Consents** tab to see the list of users who have authorized this client.

![View Consented Users](/.netlify/images?url=_astro%2Fview_consented_users.bNB41DHP.png\&w=2050\&h=1500\&dpl=6ae471fc143c9a4c27550463)

Note

Clients using the Client Credentials Flow do not have user consents since they act on their own behalf rather than on behalf of users.

To audit the same consents from your own admin screens, list them with the Node SDK. See [Resource](/saaskit/sdks/node/resources/).

### Revoke user access

[Section titled “Revoke user access”](#revoke-user-access)

As an administrator, you can revoke a user’s consent for a specific MCP Client at any time. This is useful when:

* A user requests to revoke access
* You need to remove access for security reasons
* An employee leaves the organization
* You want to force re-authentication

**To revoke access:**

1. Navigate to the specific MCP Client from the **Clients** list.
2. Go to the **Consents** tab.
3. Find the user whose access you want to revoke.
4. Click the **Revoke** or **Delete** action for that user.

Once revoked, the user will need to go through the authorization flow again to grant consent if they want to use the MCP Client.

To revoke a consent programmatically, call `revokeUserConsent` in the Node SDK. See [Resource](/saaskit/sdks/node/resources/).

### Revoke access from a public MCP client

[Section titled “Revoke access from a public MCP client”](#revoke-access-from-a-public-mcp-client)

On Scalekit, MCP clients registered through Dynamic Client Registration (`DCR`) or a Client ID Metadata Document (`CIMD`) are public clients: they have no client secret. Scalekit’s OAuth token revocation endpoint requires client authentication, so a direct revoke call from these clients returns `invalid_client`. That response is expected, not a misconfiguration.

To revoke access, use one of the supported paths instead:

* **Revoke consent from the dashboard**: revoke the user’s consent from the **Consents** tab, as described in [Revoke user access](/authenticate/mcp/managing-mcp-clients/#revoke-user-access).
* **Disconnect in the client**: most MCP clients expose a **Disconnect** action that clears tokens stored on the client. This is local sign-out only; it does not revoke the grant on Scalekit.
* **Revoke the session in your own system**: when you use [Bring your own auth](/authenticate/mcp/custom-auth/), revoke the user’s session in your auth system. After your MCP server rejects that session, the next request returns `401 Unauthorized` and the client must authenticate again.

### How revocation affects active access tokens

[Section titled “How revocation affects active access tokens”](#how-revocation-affects-active-access-tokens)

Revoking consent or ending a session stops Scalekit from issuing **new** access tokens for that user, but it does not cancel an access token that was already issued. The existing token keeps working until it expires.

This is expected behavior, not a vulnerability. Scalekit issues access tokens as self-contained JWTs. Your MCP server validates each token locally against Scalekit’s public keys and does not call back to the authorization server on every request, so Scalekit cannot reach out and cancel a token that is already in circulation. Every OAuth server that issues self-contained JWTs behaves this way. The exposure is bounded by the time left on the token - its `exp` claim.

To shrink or close that window, use either approach:

* **Shorten the access token lifetime.** Lower the **Access token lifetime** for the MCP server at **Dashboard > MCP servers > your server > Access token lifetime** (you can also set it when you [add an MCP server](/authenticate/mcp/quickstart/); the recommended range is 300–3600 seconds). You can also set it per client. A shorter lifetime means a revoked token stops working sooner. This MCP server setting is separate from the access token lifetime configured for a Web Application - changing the Web Application setting does not affect tokens minted for this MCP server.
* **Maintain a token denylist.** Validate the token with the Scalekit SDK as you do today, then check its `jti` claim against a denylist before serving the request, and reject the request when the `jti` is present. Store each revoked `jti` with a time-to-live equal to the token’s remaining lifetime - for example, a Redis key that expires at the token’s `exp`. This gives near-immediate revocation at the cost of one lookup per request.

---
# DOCUMENT BOUNDARY
---

# Migrate to the new MCP spec

> Migrate your MCP server's auth to the MCP 2026-07-28 spec with zero downtime. Trust both issuers, then enable Resource bound issuer.

Scalekit is changing the issuer of access tokens for MCP servers. The **issuer** is the `iss` claim inside a token, and it tells your MCP server who minted the token. Today the issuer is your environment URL. The new MCP specification (2026-07-28) asks clients to check the issuer on every authorization response and to keep credentials separate for each authorization server. To support this, Scalekit gives each MCP server its own issuer, `/resources/`. The dashboard option that turns this on is called **Resource bound issuer**.

This migration applies only to MCP servers registered in Scalekit in workspaces created before October 21, 2026.

To check, open your MCP server in the Scalekit dashboard. If you see the **Resource bound issuer** option, your MCP server requires the migration, and this guide is relevant for you. If you do not see it, your MCP server already has Resource bound issuer turned on and you do not need to do anything.

If you need any assistance or help, contact .

This migration guide helps you migrate your MCP server’s auth to the new MCP specification with zero downtime.

## Migrate development first, then production

[Section titled “Migrate development first, then production”](#migrate-development-first-then-production)

**Resource bound issuer** is a setting on each MCP server. Complete every step for every MCP server in your development environment first. After you validate the result, repeat the same steps for every MCP server in your production environment.

## What changes

[Section titled “What changes”](#what-changes)

|                       | Before the migration                        | After the migration                                |
| --------------------- | ------------------------------------------- | -------------------------------------------------- |
| `iss` of new tokens   | `https://example.scalekit.cloud`            | `https://example.scalekit.cloud/resources/res_123` |
| Tokens already issued | Keep their original `iss` until they expire | Keep their original `iss` until they expire        |

Old and new tokens are both in circulation for a while, so your server must trust both issuers during the migration.

1. ## Trust both issuers in your token validation

   [Section titled “Trust both issuers in your token validation”](#trust-both-issuers-in-your-token-validation)

   Find the code in your MCP server that validates access tokens. In the quickstart, it is the call that passes `issuer`. Today it passes only your environment URL. Update it to trust two issuers: your environment URL and `/resources/`. In the examples, `ENV_URL` is your environment URL and `RESOURCE_ID` is your resource ID.

   ### If you use a Scalekit SDK

   [Section titled “If you use a Scalekit SDK”](#if-you-use-a-scalekit-sdk)

   Update your MCP server to use the latest Scalekit SDK version, which supports trusting multiple issuers. Then add both issuers while calling the validate method.

   * Node.js

     Trust both issuers

     ```javascript
     await scalekit.validateToken(token, {
       // Before: issuer: ENV_URL
       issuer: [ENV_URL, `${ENV_URL}/resources/${RESOURCE_ID}`],
       audience: [RESOURCE_ID],
     });
     ```

   * Python

     Trust both issuers

     ```python
     options = TokenValidationOptions(
         # Before: issuer=ENV_URL
         issuer=[ENV_URL, f"{ENV_URL}/resources/{RESOURCE_ID}"],
         audience=[RESOURCE_ID],
     )
     scalekit_client.validate_token(token, options=options)
     ```

   * Go

     Trust both issuers

     ```go
     ok, err := scalekitClient.ValidateTokenWithOptions(ctx, token, &scalekit.ValidateTokenOptions{
         // Before: Issuer: envURL
         Issuers:  []string{envURL, envURL + "/resources/" + RESOURCE_ID},
         Audience: []string{RESOURCE_ID},
     })
     if err != nil || !ok {
         // Reject the request
     }
     ```

   * Java

     Trust both issuers

     ```java
     TokenValidationOptions options = TokenValidationOptions.builder()
         // Before: .issuer(envUrl)
         .issuers(List.of(envUrl, envUrl + "/resources/" + RESOURCE_ID))
         .audience(List.of(RESOURCE_ID))
         .build();
     boolean isValid = scalekitClient.authentication().validateAccessToken(token, options);
     if (!isValid) {
         // Reject the request
     }
     ```

   ### If you use FastMCP

   [Section titled “If you use FastMCP”](#if-you-use-fastmcp)

   The Scalekit provider in FastMCP already trusts both your environment URL and `/resources/`, so you do not need to change your validation code. Continue with [Enable Resource bound issuer](#enable-resource-bound-issuer). Make sure you are using the latest version of FastMCP.

   ### If you use your own JWT validator

   [Section titled “If you use your own JWT validator”](#if-you-use-your-own-jwt-validator)

   Configure your validator to trust exactly two `iss` values, and reject any other issuer. Most JWT libraries accept a list of allowed issuers.

2. ## Release your MCP server

   [Section titled “Release your MCP server”](#release-your-mcp-server)

   Release the change. Then check that both kinds of tokens work:

   * **A token issued before the release**: call a tool from a session that was authorized before the release, without authorizing again. The request should still succeed.
   * **A new token**: disconnect your MCP client, for example MCP Inspector, and connect again so that it authorizes again. Call a tool. The request should succeed.

   Nothing changes for your users yet, because tokens still carry your environment URL.

3. ## Enable Resource bound issuer

   [Section titled “Enable Resource bound issuer”](#enable-resource-bound-issuer)

   Enable the option in the Scalekit dashboard:

   1. Go to **MCP servers** and select your MCP server.
   2. Enable **Resource bound issuer**.
   3. Read the confirmation dialog. It explains that tokens issued from now on carry `/resources/` as the `iss` claim. Confirm after you have read this guide.

   ![Resource bound issuer option set to Enabled in the MCP server settings](/.netlify/images?url=_astro%2Fmcp-resource-bound-issuer.RiuniKLh.png\&w=3100\&h=892\&dpl=6ae471fc143c9a4c27550463)

   Repeat the checks from the previous step. Existing tokens must still work. For a new token, the `iss` claim should be `/resources/`, and your MCP server should accept it.

   Do not disable Resource bound issuer

   After you enable it, do not disable it. Switching back changes the issuer your clients see, which can break connected clients.

Optional: Trust only the server’s own issuer

Wait until every token that was issued before you enabled the option has expired. After that, every token carries the server’s own issuer, and you can clean up your code to trust only that issuer. If you are not sure whether older tokens are still in use, skip this section. Trusting both issuers is safe.

* Node.js

  Trust only the server's own issuer

  ```javascript
  await scalekit.validateToken(token, {
    issuer: `${ENV_URL}/resources/${RESOURCE_ID}`,
    audience: [RESOURCE_ID],
  });
  ```

* Python

  Trust only the server's own issuer

  ```python
  options = TokenValidationOptions(
      issuer=f"{ENV_URL}/resources/{RESOURCE_ID}",
      audience=[RESOURCE_ID],
  )
  scalekit_client.validate_token(token, options=options)
  ```

* Go

  Trust only the server's own issuer

  ```go
  ok, err := scalekitClient.ValidateTokenWithOptions(ctx, token, &scalekit.ValidateTokenOptions{
      Issuer:   envURL + "/resources/" + RESOURCE_ID,
      Audience: []string{RESOURCE_ID},
  })
  if err != nil || !ok {
      // Reject the request
  }
  ```

* Java

  Trust only the server's own issuer

  ```java
  TokenValidationOptions options = TokenValidationOptions.builder()
      .issuer(envUrl + "/resources/" + RESOURCE_ID)
      .audience(List.of(RESOURCE_ID))
      .build();
  boolean isValid = scalekitClient.authentication().validateAccessToken(token, options);
  if (!isValid) {
      // Reject the request
  }
  ```

If you use your own JWT validator, remove the environment URL from its list of trusted issuers in the same way.

If you use FastMCP, skip this section. The Scalekit provider keeps trusting both issuers.

## Frequently asked questions

[Section titled “Frequently asked questions”](#frequently-asked-questions)

Do my users need to sign in again?

No. Tokens issued before you enable the option keep their original `iss` and work until they expire, as long as your MCP server trusts both issuers.

Do I need to enable the option for every MCP server?

Yes. The option is set on each MCP server. Enable it for every MCP server in your development environment first, and then for every MCP server in your production environment.

Can I turn the option off again?

Leave it enabled on every MCP server. If you need to change it, contact .

## Why Scalekit is making this change

[Section titled “Why Scalekit is making this change”](#why-scalekit-is-making-this-change)

The Model Context Protocol (MCP) [2026-07-28 specification](https://blog.modelcontextprotocol.io/posts/2026-07-28/) strengthens issuer checks. Authorization servers should return the `iss` parameter per RFC 9207, and clients must validate it before redeeming a code. This closes an authorization-server mix-up hole. The specification also binds client credentials to the issuer that minted them.

To support these checks, each MCP server in Scalekit has its own issuer. That is why your MCP server must trust the server’s own issuer.

If a token is rejected unexpectedly, see [Troubleshooting MCP auth](/authenticate/mcp/troubleshooting/).

---
# DOCUMENT BOUNDARY
---

# Agent / Machine interacting with MCP Server

> Learn how an autonomous agent or machine securely authenticates with an MCP Server using OAuth 2.1 Client Credentials flow in Scalekit.

An **autonomous agent** or any **machine-to-machine process** can directly interact with an **MCP Server** secured by Scalekit. In this model, the agent acts as a **confidential OAuth client**, authenticated using a `client_id` and `client_secret` issued by Scalekit.

This topology uses the **OAuth 2.1 Client Credentials flow**, allowing the agent to obtain an access token without user interaction. Tokens are scoped and time-bound, ensuring secure and auditable automation between services.

Flow Summary

The agent authenticates with Scalekit using the **OAuth 2.1 Client Credentials Flow** to obtain a scoped access token, then calls the MCP Server’s tools using that token for secure, automated communication.

***

## Authorization Sequence

[Section titled “Authorization Sequence”](#authorization-sequence)

![Agent to MCP server authorization: the agent gets a token with client credentials and calls tools with it](/d2/docs/authenticate/mcp/topologies/agent-mcp-0.svg)

***

## How It Works

[Section titled “How It Works”](#how-it-works)

**Client Registration** Before an agent can request tokens, you must create a **Machine-to-Machine (M2M) client** for your MCP Server in Scalekit.

Steps to create a client:

1. Navigate to **Dashboard ? MCP Servers** and select your MCP Server. Go to the **Clients** tab. ![Clients tab placeholder](/.netlify/images?url=_astro%2Fmcp-client-nav.C6UPUhIu.png\&w=1148\&h=1242\&dpl=6ae471fc143c9a4c27550463)
2. Click **Create Client**. ![Create client placeholder](/.netlify/images?url=_astro%2Fmcp-clients-tab.UgPaVUGm.png\&w=3020\&h=1040\&dpl=6ae471fc143c9a4c27550463)
3. Copy the **client_id** and **client_secret** immediately - the secret will not be shown again. ![Client Sidesheet](/.netlify/images?url=_astro%2Fmcp-client-sidesheet.D9KN4b5q.png\&w=3020\&h=1500\&dpl=6ae471fc143c9a4c27550463)
4. Optionally, set scopes (e.g., `todo:read`, `todo:write`) that correspond to the permissions configured for your MCP Server. Hit **Save**

***

## Requesting an Access Token

[Section titled “Requesting an Access Token”](#requesting-an-access-token)

Once you have the client credentials, the agent can request a token directly from the Scalekit Authorization Server:

Terminal

```bash
curl --location '{{env_url}}/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id={{client_id}}' \
--data-urlencode 'client_secret={{secret_value}}' \
--data-urlencode 'scope=todo:read todo:write'
```

Scalekit responds with a JSON payload similar to:

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIn0...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "todo:read todo:write"
}
```

Use the `access_token` in the `Authorization` header when calling your MCP Server’s endpoint.

Tip

Scalekit issues short-lived tokens that can be safely reused until they expire. Cache the token locally and request a new one shortly before expiration to maintain efficient, secure machine-to-machine communication.

***

## Try It Yourself

[Section titled “Try It Yourself”](#try-it-yourself)

If you’d like to simulate this flow, use the same **FastMCP Todo Server** from the [FastMCP Example](/authenticate/mcp/fastmcp-quickstart). Create an **M2M client** in the Scalekit Dashboard and run your token request using `curl` or programmatically within your agent. Once the token is obtained, attach it as a Bearer token in the `Authorization` header when calling your MCP Server’s tools.

---
# DOCUMENT BOUNDARY
---

# Human interacting with MCP Server

> Learn how a human authenticates with an MCP Server via OAuth 2.1 when using MCP-compliant hosts such as ChatGPT, Claude, VSCode, or Windsurf.

When a human uses a compliant MCP host, that host acts as the OAuth client. It initiates authorization with the Scalekit Authorization Server, obtains a scoped access token, and interacts securely with the MCP Server on behalf of the user.

This topology represents the most common interaction model for real-world MCP usecases - **humans interacting with an MCP**, while Scalekit ensures tokens are valid, scoped, and auditable.

Flow Summary

In general, human-initiated MCP flow uses the **OAuth 2.1 Authorization Code Flow**. Scalekit acts as the Authorization Server, the MCP Server as the Protected Resource, and the host (ChatGPT, Claude, Windsurf, etc.) as the OAuth Client.

***

## Authorization Sequence

[Section titled “Authorization Sequence”](#authorization-sequence)

![Human to MCP server authorization: the user authorizes, the client exchanges the code, and calls tools with the token](/d2/docs/authenticate/mcp/topologies/human-mcp-0.svg)

***

## How It Works

[Section titled “How It Works”](#how-it-works)

1. **Initiation** ? The human configures an MCP server in their MCP client.

2. **Challenge** ? The MCP Server responds with an HTTP `401` containing a `WWW-Authenticate` header that points to the Scalekit Authorization Server.

3. **Authorization Flow** ? The MCP Client opens the user’s browser to initiate the OAuth 2.1 authorization flow. During this step, the Scalekit Authorization Server handles user authentication through Passwordless, Passkeys, Social login providers (like Google, GitHub, or LinkedIn), or Enterprise SSO integrations (such as Okta, Microsoft Entra ID, or ADFS). The user is then prompted to grant consent for the requested scopes. Once approved, Scalekit returns an authorization code, which the MCP Client exchanges for an access token.

4. **Token Issuance** ? Scalekit issues an OAuth 2.1 access token containing claims and scopes (for example, `todo:read`, `calendar:write`) that represent the user’s permissions.

5. **Authorized Request** ? The client calls the MCP Server again, now attaching the Bearer token in the `Authorization` header.

6. **Validation and Execution** ? The MCP Server validates the token issued by scalekit and executes the requested tool.

***

## Try It Yourself

[Section titled “Try It Yourself”](#try-it-yourself)

Head to the **[FastMCP Examples section](/authenticate/mcp/fastmcp-quickstart)** to experience this topology in action. There you’ll register a FastMCP server, configure Scalekit Auth, and observe token issuance and validation end-to-end.

---
# DOCUMENT BOUNDARY
---

# MCP Server interacting with MCPs / APIs

> Understand how an MCP Server integrates with internal systems, other MCP servers, or external APIs using secure tokens or API keys.

In real-world scenarios, an **MCP Server** often needs to make backend calls - to your **own APIs**, to **another MCP Server**, or to **external APIs** such as CRM, ticketing, or SaaS tools. This page explains three secure ways to perform these downstream integrations, each corresponding to a different trust boundary and authorization pattern.

## 1. Using API Keys or Custom Tokens

[Section titled “1. Using API Keys or Custom Tokens”](#1-using-api-keys-or-custom-tokens)

Your MCP Server can communicate with internal or external backend systems that have their own authorization servers or API key?based access. In this setup, the MCP Server manages its own credentials securely (for example, an environment variable, vault, or secrets manager) and injects them when making downstream calls.

Best practice

Always store downstream API credentials securely using a secret manager. Do not expose API keys through MCP tool schemas or client-facing logs.

![MCP server calling a downstream API with an API key or custom token](/d2/docs/authenticate/mcp/topologies/mcp-api-0.svg)

### Example

[Section titled “Example”](#example)

* The MCP Server stores an API key as `EXTERNAL_API_KEY` in environment variables.
* When a tool (e.g., `get_weather_data`) is called, your MCP server attaches the key in the request.
* The backend API validates the key and responds with data.

***

## 2. Interacting with Another MCP Server autonomously

[Section titled “2. Interacting with Another MCP Server autonomously”](#2-interacting-with-another-mcp-server-autonomously)

If you have two MCP Servers that need to communicate - for example, `crm-mcp` calling tools from `tickets-mcp` - you can follow the same authentication pattern described in the [Agent ? MCP](/authenticate/mcp/topologies/agent-mcp/) topology.

The calling MCP Server (in this case, `crm-mcp`) acts as an **autonomous agent**, authenticating with the receiving MCP Server via **OAuth 2.1 Client Credentials Flow**. Once the token is issued by Scalekit, the calling MCP uses it to call tools exposed by the second MCP Server.

![MCP server calling another MCP server autonomously with its own token](/d2/docs/authenticate/mcp/topologies/mcp-api-1.svg)

You can find a detailed explanation of this topology in [this section](/authenticate/mcp/topologies/agent-mcp).

***

## 3. Cascading the Same Token to Downstream Systems

[Section titled “3. Cascading the Same Token to Downstream Systems”](#3-cascading-the-same-token-to-downstream-systems)

In some cases, you may want your MCP Server to forward (or “cascade”) the **same access token** it received from the client - for example, when your backend system lies within the same trust boundary as the Scalekit Authorization Server and can validate the token based on its issuer, scopes, and expiry.

![MCP server forwarding the caller's bearer token to a downstream API](/d2/docs/authenticate/mcp/topologies/mcp-api-2.svg)

### When to Use This Pattern

[Section titled “When to Use This Pattern”](#when-to-use-this-pattern)

* Both systems (MCP Server and backend MCP/API) trust **the same Authorization Server** (Scalekit).
* The backend API can validate JWTs using public keys or JWKS URL.
* Scopes and issuer claims (`iss`, `scope`, `exp`) are sufficient to determine access.

Caution

Only cascade tokens across services that share the same trust boundary. If your backend MCP or API does not validate Scalekit-issued tokens, use a separate service credential or client credentials flow instead.

---
# DOCUMENT BOUNDARY
---

# Troubleshooting MCP auth

> Troubleshooting guide for common errors while adding auth for MCP Servers

Use this guide when you use Scalekit as the authentication server for your MCP servers and a client fails to connect, a request stalls, or the authentication handshake breaks. Start with the category that matches your symptom, then open the matching scenario.

Most issues fall into configuration and setup, CORS and network access, or client-specific behavior. Each scenario explains the cause and the fix.

## Configuration and setup

[Section titled “Configuration and setup”](#configuration-and-setup)

Token rejected with `unexpected "iss" claim value`

The token’s `iss` claim does not match the issuer your MCP server trusts. Tokens for an MCP server carry `/resources/` as the issuer. A server that validates against your environment URL only rejects them.

Decode the rejected token and compare its `iss` claim with the issuer in your validation code. Matching is exact, so a trailing slash or a different environment fails.

To fix it, validate against the server’s own issuer:

```javascript
await scalekit.validateToken(token, {
  issuer: `${ENV_URL}/resources/${RESOURCE_ID}`,
  audience: [RESOURCE_ID],
});
```

If your workspace was created before October 21, 2026, your tokens may still carry the environment URL until you enable **Resource bound issuer**. Trust both issuers during the change by following [Migrate to the new MCP spec](/authenticate/mcp/migrate-to-2026-07-28-mcp-spec/).

`/auth-requests/` returns a 404 or “invalid ID” error

You may be passing the MCP server’s resource ID instead of the connection ID in the URL path.

These are two different identifiers with different purposes:

| Identifier      | Format     | Purpose                                                                       |
| --------------- | ---------- | ----------------------------------------------------------------------------- |
| `resource_id`   | `res_xxx`  | Identifies the MCP server; used in token audiences and client registration    |
| `connection_id` | `conn_xxx` | Identifies your BYOA auth connection; required in `/auth-requests/` endpoints |

The correct endpoint uses `connection_id`:

```txt
/api/v1/connections//auth-requests//user
```

To find your `connection_id`, open **Dashboard > MCP Servers > \[your server] > Advanced Configurations > Connection ID**.

Client fails with an `invalid_client_metadata_url` error

This error means Scalekit rejected the OAuth client that tried to connect, because the client sent a `client_id` that Scalekit never issued or a client metadata URL it can’t resolve. The connection often succeeds for some users and fails for others on the same MCP server, which points to client-side configuration rather than a problem with your Scalekit setup.

Two causes account for most occurrences.

**Cause 1: The connecting application has stale client credentials configured**

Some applications (for example, Glean) let you paste a `client_id` and `client_secret` for the MCP server. When those fields hold credentials that Scalekit didn’t issue, the auth server rejects the connection.

To resolve it:

1. Open the connecting application’s integration settings for your MCP server.
2. Clear the `client_id` and `client_secret` fields.
3. Reconnect. Applications that support Dynamic Client Registration (DCR) obtain valid credentials automatically once the fields are empty.
4. If the application cannot use DCR, generate a static pre-registered client in **Dashboard > MCP Servers > \[your server]** and share those credentials instead.

**Cause 2: `mcp-remote` is sending a `client_id` it did not register**

`mcp-remote` can reuse a `client_id` from a different MCP server session or send a non-Scalekit value. Scalekit issues DCR clients only in the `m2m_xxx` format, so it rejects any other value. This affects only the users who route the connection through `mcp-remote`.

`mcp-remote` was a temporary bridge for clients that couldn’t reach remote MCP servers directly. Claude Desktop, Claude Code, and Cursor now support remote HTTP MCP servers natively, so you no longer need it. Connect with the native HTTP transport instead:

* **Claude Desktop**: open **Settings > Connectors > Add custom connector** and enter your MCP server URL.

* **Claude Code**: run the following in your terminal.

  ```sh
  claude mcp add --transport http  https://your-mcp-server.example.com/mcp
  ```

The error points to the client, not Scalekit

Because this error originates from the client’s registration data, it reproduces per user. When one user fails while others succeed against the same MCP server, inspect that user’s client configuration first.

What is the difference between DCR and CIMD?

MCP clients identify themselves to Scalekit’s authorization server using one of two mechanisms:

* **Dynamic Client Registration (DCR)**: The client registers itself with Scalekit and receives a `client_id` in the `m2m_xxx` format. See [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591).
* **Client ID Metadata Document (CIMD)**: The client presents a URL that hosts its client metadata document, and Scalekit fetches that URL to identify the client.

The `invalid_client_metadata_url` error belongs to the CIMD path: Scalekit couldn’t resolve or validate the metadata URL the client sent. A client that completes DCR correctly doesn’t trigger this error. Follow the resolution steps in the previous scenario.

Is an unauthenticated DCR endpoint a security risk?

No. Any client can call the DCR endpoint without prior authentication and register with a `redirect_uri` you didn’t pre-approve. This is expected behavior, not a misconfiguration. The MCP specification’s OAuth 2.1 profile doesn’t require a preshared token before a client can register. This lets clients like Claude Desktop, Cursor, or your own custom connector register themselves without you configuring each one manually.

Registering a client doesn’t grant it access. A registered client still needs a real user to complete the authorization flow and approve consent before Scalekit issues a token. During that step, the consent screen shows your **environment domain** (or your [custom domain](/guides/custom-domain/)) as the party requesting access, not the connecting client’s name. If a user doesn’t recognize that domain, they should deny the request; Scalekit doesn’t issue a code or token to that client.

Scalekit also doesn’t restrict `redirect_uri` values to a preapproved domain allowlist. Doing so would break legitimate MCP clients that rely on values an allowlist would normally reject, such as a custom URI scheme (for example, `cursor://`) or a loopback address for a local desktop client.

View authentication logs to debug a failed connection

Open **Dashboard > Authentication > Logs** to inspect the authorization and token requests for your environment. Each entry shows the OAuth client, the requested scopes, and the outcome of each step in the flow.

When you debug a failed connection, change the log filter to show all events rather than successful events only. Failed authorization and token requests appear only when you include error events, so a success-only view can hide the request you need to diagnose.

Per-client MCP connection logs

Connection logs for individual MCP clients aren’t yet available for self-serve viewing. When the authentication logs don’t explain a failure, contact Scalekit support with the failing `client_id` and the approximate time of the attempt.

MCP server does not connect to the MCP Inspector

A failed connection to the MCP Inspector usually points to a problem with the authentication handshake or metadata configuration. Work through these checks.

**Verify the MCP server is responding correctly:**

1. Open your browser’s developer tools (Network tab)
2. Navigate to your MCP server URL (for example, `http://localhost:3002/`)
3. Confirm the response returns a `401` status code
4. Check the response headers for `www-authenticate` containing `resource_metadata=""`

**Validate the metadata:**

1. Copy the metadata URL from the `www-authenticate` header
2. Open it in your browser
3. Confirm the JSON structure matches what you see in your Scalekit dashboard

Note

If all checks pass but the connection still fails, see the CORS and network access scenarios below.

`redirect_uri` mismatch during authorization

This error typically occurs when your MCP client has cached an old MCP server domain after you changed it. The client keeps sending requests to the old URL, which no longer matches your Scalekit configuration.

Clear cached authentication by client type.

**mcp-remote:**

1. Delete the cached configuration folder: `~/.mcp-auth/mcp-remote-`
2. Reconnect to your MCP server

**VS Code:**

1. Open the Command Palette (Cmd/Ctrl + Shift + P)
2. Search for **Authentication: Remove Dynamic Authentication Provider**
3. Select and remove the cached entry
4. Reconnect to your MCP server

**Claude Desktop:**

Caution

Claude Desktop does not currently support clearing cached authentication data. As a workaround, use a different domain or subdomain for your MCP server, or contact Claude support for assistance.

GitHub Copilot CLI: stale cached credentials after an environment switch

GitHub Copilot CLI caches OAuth client credentials locally. If you switch your Scalekit environment (for example, from US to EU), the cached `client_id` no longer matches the new environment and login fails with `unable to retrieve client by id`.

1. Locate and delete the cached OAuth config files:

   ```sh
   rm -rf ~/.copilot/mcp-oauth-config
   ```

2. Reconnect your MCP server in GitHub Copilot CLI. It registers a fresh client against the correct environment.

Note

If you cannot find the files using the path above, also check `~/.config/github-copilot/` for cached MCP auth files.

## CORS and network access

[Section titled “CORS and network access”](#cors-and-network-access)

CORS errors in the network logs when using MCP Inspector

CORS errors occur when your MCP client cannot make cross-origin requests to your Scalekit environment during the authentication handshake, which prevents the flow from completing.

1. Navigate to **Dashboard > Authentication > Redirect URLs > Allowed Callback URLs**
2. Add your MCP Inspector URL to the allowed list: `http://localhost:6274/`
3. Retry the connection

Development vs production URLs

Add callback URLs for both your development (`http://localhost:6274/`) and production environments to avoid CORS errors in either environment.

MCP client requests never reach the server

If requests from your MCP client silently fail to reach your server, a proxy or firewall may be blocking them. This often happens in corporate environments or when you use CDN services.

1. Check whether you’re using a proxy (for example, Cloudflare, AWS WAF, or a corporate proxy)
2. Configure your proxy to allow or exempt requests from your MCP client to your server domain
3. Review proxy logs to confirm whether requests are being blocked
4. Test direct connectivity from your client machine to your MCP server, without the proxy if possible

Note

Some corporate proxies require explicit allowlisting of authentication endpoints. Contact your network administrator if you suspect this is the case.

Cloudflare bot protection is blocking MCP client requests

If your MCP server is behind Cloudflare and AI agents (such as Claude Desktop, Cursor, or other MCP clients) cannot reach it, Cloudflare’s **Bot Fight Mode**, **Super Bot Fight Mode**, or **AI Crawl Control** settings may classify agent traffic as bot traffic and block it at the edge.

**Symptoms:**

* MCP client connections fail silently or return `403 Forbidden`
* The authentication handshake never completes
* Browser access to the MCP server works, but programmatic access from AI agents does not
* Cloudflare serves a JavaScript challenge or Turnstile page instead of your MCP response

**Diagnose which rule is blocking:**

1. Open the [Cloudflare dashboard](https://dash.cloudflare.com/) for your domain
2. Navigate to **Security > Events**
3. Filter for your MCP server path and look for blocked or challenged requests
4. Note the **rule name**. It tells you which Cloudflare feature caused the block (Bot Fight Mode, Super Bot Fight Mode, AI Crawl Control, or a managed rule)

**Resolution for the Cloudflare Free plan (Bot Fight Mode):**

On the Free plan, Bot Fight Mode runs before the WAF Ruleset Engine, so custom WAF skip rules have no effect on it. Your options are:

1. Open **Security Settings** in the Cloudflare dashboard (direct link: `https://dash.cloudflare.com/?to=/:account/:zone/security/settings`)
2. Under **Bot traffic**, turn **Bot Fight Mode** off
3. If **Block AI Scrapers and Crawlers** is also enabled, disable it
4. Retry the MCP client connection

**Resolution for Pro, Business, or Enterprise plans (Super Bot Fight Mode):**

On paid plans, you can create a WAF custom rule that skips bot protection only for MCP traffic while keeping the rest of your domain protected:

1. Navigate to **Security > WAF > Custom rules > Create rule**
2. Set the expression to match your MCP server path: `starts_with(http.request.uri.path, "/mcp")`. Adjust the path to match your MCP server’s base path
3. Set the action to **Skip**, then select **Super Bot Fight Mode**
4. Move this rule to the top of your custom rules list so it evaluates first
5. Save and deploy, then retry the MCP client connection

Alternatively, navigate to **AI Crawl Control** in your bot settings and set Claude-related bots (`ClaudeBot`, `Claude-User`) to **Allow**.

Tip

The WAF skip rule is recommended for paid plans because it disables bot checks only for MCP server traffic while keeping your other endpoints protected.

Note

Some MCP client SDKs send empty or missing `User-Agent` headers, which can trigger separate WAF rules unrelated to bot protection. If the Security Events log shows a user-agent-based block rather than a bot rule, check your MCP client’s request headers.

## Client-specific issues

[Section titled “Client-specific issues”](#client-specific-issues)

Claude Desktop ignores custom ports when connecting to MCP servers

Claude Desktop currently supports only standard HTTPS traffic on port `443`. If your MCP server runs on a custom port (for example, `https://mymcp.internal:8443/`), Claude Desktop still attempts to connect to port `443`, so the connection fails.

* Expose your MCP server on port `443` (requires a proxy or load balancer)
* Use a reverse proxy that listens on `443` and forwards requests to your custom port

Note

Future versions of Claude Desktop may add custom port support. Check the Claude Desktop release notes for updates.

Multiple authentication tabs open when using both mcp-remote and Claude Desktop

Recent versions of Claude Desktop include Connectors, which removes the need to run mcp-remote separately. The **Custom Connector** feature lets you configure MCP servers directly without additional tools.

* Use Claude Desktop’s built-in Custom Connector feature for MCP server management
* Disable or stop mcp-remote if you’re only using Claude Desktop
* If you have a specific use case that requires both, contact Claude’s official support

Tip

To avoid duplicate authentication flows, use only one MCP client at a time.

OAuth popup closes immediately or shows “window closed too soon”

MCP clients that use a popup-based OAuth flow (such as Amazon Q) may report that the popup closed too soon or that authentication failed, even though the OAuth flow completed successfully in the popup window.

**Root cause:** Your application’s login page returns a `Cross-Origin-Opener-Policy: same-origin` HTTP header. When a cross-origin MCP client (for example, `quick.aws.com`) opens a popup that navigates to a page with this header, the browser severs the opener’s reference to the popup. The MCP client sees the popup as `closed` immediately, before the OAuth callback can complete.

**Diagnosis:**

Check the response headers on your login or redirect endpoint:

```sh
curl -sI https://your-app.com/login | grep -i cross-origin-opener-policy
```

If the output includes `cross-origin-opener-policy: same-origin`, this is the cause.

**Resolution:**

Remove the `Cross-Origin-Opener-Policy` header from your login and OAuth callback endpoints, or change its value to `unsafe-none`:

```plaintext
Cross-Origin-Opener-Policy: unsafe-none
```

You can scope this change to only the endpoints involved in the OAuth flow rather than your entire application. After the change, retry the MCP client connection.

This is an application-side fix

This header is set by your application or its hosting platform, not by Scalekit. Common sources include framework defaults (for example, Next.js or Rails), CDN or reverse proxy settings, and security middleware.

The browser does not open during authentication

Some MCP clients require permission to open your default browser during the authentication flow. If your browser doesn’t launch, the authentication handshake may time out.

**macOS:**

1. Open **System Preferences > Security & Privacy > App Management**
2. Confirm the MCP client has permission to open applications
3. Restart your MCP client

**Windows:**

1. Navigate to **Settings > Privacy > App permissions**
2. Enable **Allow apps to manage your default app settings**
3. Restart your MCP client

**Linux:**

1. Confirm `xdg-open` or your default browser opener is installed: `which xdg-open`
2. Verify the command is accessible from your terminal
3. Restart your MCP client

Note

After you update permissions, restart your MCP client so the changes take effect.

## Best practices

[Section titled “Best practices”](#best-practices)

Follow these best practices to avoid common issues and maintain a robust MCP authentication setup:

1. **Use separate Scalekit environments** for development and production to prevent configuration conflicts

2. **Register MCP servers with environment-specific domains:**

   * Development: `https://mcp-dev.yourdomain.com/`
   * Production: `https://mcp.yourdomain.com/`

3. **Update your MCP client configuration** to point to the correct Scalekit environment for each deployment

4. **Test authentication independently** in each environment before you deploy to production

5. **Monitor authentication logs** in **Dashboard > Authentication > Logs** to identify and resolve issues quickly

6. **Keep callback URLs updated** whenever you change domains or ports

Environment management

Maintain separate environment variables for your MCP server configuration (for example, `SCALEKIT_ENVIRONMENT_URL` and `MCP_SERVER_URL`) to switch between development and production environments.

## Get help

[Section titled “Get help”](#get-help)

If none of the scenarios resolve the issue, open **Dashboard > Authentication > Logs** and review the authorization and token requests for the failing attempt.

When you contact [support](mailto:support@scalekit.com), include:

* The failing `client_id` and the MCP server `resource_id`
* The MCP client and transport (for example, a Claude Desktop custom connector or `mcp-remote`)
* The full error text and the approximate timestamp of the attempt
* The steps that reproduce the failure

---
# DOCUMENT BOUNDARY
---

# Add auth to xmcp server

> Build an MCP server with xmcp framework and Scalekit OAuth 2.1 authentication. xmcp handles transport, routing, and bundling so you focus on tools.

This guide shows you how to build an MCP server with [xmcp](https://xmcp.dev) and secure it with Scalekit OAuth 2.1. xmcp is a TypeScript MCP framework that handles transport setup, bundling, and hot reload — you write tools as plain functions and add auth via a middleware file.

Use this quickstart when you want a framework that manages MCP protocol details for you. xmcp gives you file-based routing for tools, built-in Streamable HTTP transport, and a middleware convention that keeps auth separate from business logic. The full code is available on [GitHub](https://github.com/scalekit-developers/xmcp-scalekit-example).

**Prerequisites**

* A [Scalekit account](https://app.scalekit.com) with permission to manage MCP servers
* **Node.js 18+** installed locally
* Basic understanding of TypeScript and OAuth

Review the xmcp MCP authorization flow

![xmcp server with Scalekit: the client authorizes with PKCE, gets a bearer token, and the server verifies it against JWKS](/d2/docs/authenticate/mcp/xmcp-quickstart-0.svg)

1. ## Register your MCP server in Scalekit

   [Section titled “Register your MCP server in Scalekit”](#register-your-mcp-server-in-scalekit)

   Create a protected resource entry so Scalekit can issue and validate tokens for your server.

   1. Navigate to **[Dashboard](https://app.scalekit.com) > MCP Servers > Add MCP Server**.
   2. Enter a descriptive name (for example, `xmcp Demo`).
   3. Set **Server URL** to `http://localhost:3001`.
   4. Ensure **Allow dynamic client registration** is checked — this is required for MCP clients like Claude Desktop and Cursor to connect automatically.
   5. Click **Save** to create the server.

   After saving, note the **Resource ID** shown below the server name (for example, `res_...`). You’ll need it in the next step.

2. ## Create your project

   [Section titled “Create your project”](#create-your-project)

   Scaffold a new xmcp project and add the dependencies for Scalekit authentication.

   Terminal

   ```bash
   mkdir xmcp-scalekit-example
   cd xmcp-scalekit-example
   ```

   Create `package.json`:

   package.json

   ```json
   1
   {
   2
     "name": "xmcp-scalekit-example",
   3
     "private": true,
   4
     "scripts": {
   5
       "dev": "xmcp dev",
   6
       "build": "xmcp build",
   7
       "start": "node dist/http.js"
   8
     },
   9
     "dependencies": {
   10
       "xmcp": "^0.6.10",
   11
       "@scalekit-sdk/node": "^2.6.2",
   12
       "jose": "^5.2.0",
   13
       "express": "^4.22.1",
   14
       "zod": "^4.0.10"
   15
     },
   16
     "devDependencies": {
   17
       "@types/express": "^4.17.25",
   18
       "@types/node": "^22.19.2",
   19
       "typescript": "^5.9.3"
   20
     }
   21
   }
   ```

   Install dependencies:

   Terminal

   ```bash
   npm install
   ```

3. ## Configure environment variables

   [Section titled “Configure environment variables”](#configure-environment-variables)

   Create a `.env` file with your Scalekit credentials from step 1.

   Terminal

   ```bash
   cat <<'EOF' > .env
   SCALEKIT_ENVIRONMENT_URL=https://.scalekit.com
   SCALEKIT_CLIENT_ID=
   SCALEKIT_CLIENT_SECRET=
   SCALEKIT_RESOURCE_ID=
   BASE_URL=http://localhost:3001
   PORT=3001
   EOF


   open .env
   ```

   | Variable                   | Description                                                                                    |
   | -------------------------- | ---------------------------------------------------------------------------------------------- |
   | `SCALEKIT_ENVIRONMENT_URL` | Your Scalekit environment URL from **Dashboard > Settings > API Credentials**                  |
   | `SCALEKIT_CLIENT_ID`       | Client ID from **Dashboard > Settings > API Credentials**                                      |
   | `SCALEKIT_CLIENT_SECRET`   | Client secret from **Dashboard > Settings > API Credentials**                                  |
   | `SCALEKIT_RESOURCE_ID`     | Resource ID from **Dashboard > MCP Servers** (the `res_...` value shown below the server name) |
   | `BASE_URL`                 | Public URL where your server is reachable. Must match the Server URL registered in Scalekit    |
   | `PORT`                     | Local port for the server                                                                      |

   Resource ID is required

   Without `SCALEKIT_RESOURCE_ID`, the server cannot serve the resource-specific authorization server metadata that includes the `registration_endpoint`. MCP clients that rely on Dynamic Client Registration (DCR) — including Claude Desktop and Cursor — will fail to connect.

4. ## Enable Streamable HTTP transport

   [Section titled “Enable Streamable HTTP transport”](#enable-streamable-http-transport)

   Create `xmcp.config.ts` at the project root to enable Streamable HTTP mode. By default xmcp uses stdio; setting `http: true` starts an Express-based HTTP server.

   xmcp.config.ts

   ```typescript
   import { XmcpConfig } from "xmcp";


   const config: XmcpConfig = {
     http: true,
     paths: {
       prompts: false,
       resources: false,
     },
   };


   export default config;
   ```

5. ## Add the Scalekit auth provider

   [Section titled “Add the Scalekit auth provider”](#add-the-scalekit-auth-provider)

   This server validates tokens whose issuer (`iss`) is `/resources/`. If you see the **Resource bound issuer** option on your MCP server in the Scalekit dashboard, your workspace was created before October 21, 2026. Until you enable that option, your tokens carry your environment URL as the issuer, and this server rejects them with a `401`. Follow [Migrate to the new MCP spec](/authenticate/mcp/migrate-to-2026-07-28-mcp-spec/) first.

   Create `src/lib/scalekit-auth.ts` — this is the auth provider that handles JWT verification and OAuth discovery endpoints.

   The provider returns an xmcp `Middleware` object with a `router` (for discovery endpoints) and a `middleware` (for token validation on `/mcp`).

   src/lib/scalekit-auth.ts

   ```typescript
   10 collapsed lines
   1
   import {
   2
     Router,
   3
     Request,
   4
     Response,
   5
     NextFunction,
   6
     type RequestHandler,
   7
   } from "express";
   8
   import { createContext, type Middleware } from "xmcp";
   9
   import { createRemoteJWKSet, jwtVerify, errors } from "jose";
   10
   import { Scalekit } from "@scalekit-sdk/node";
   11


   12
   // --- Types ---
   13


   14
   export interface ScalekitConfig {
   15
     readonly environmentUrl: string;
   16
     readonly clientId: string;
   17
     readonly clientSecret: string;
   18
     readonly baseURL: string;
   19
     readonly resourceId?: string;
   20
     readonly docsURL?: string;
   21
     readonly scopes?: readonly string[];
   22
   }
   23


   24
   export interface JWTClaims {
   25
     readonly sub: string;
   26 collapsed lines
   26
     readonly iss: string;
   27
     readonly aud?: string | readonly string[];
   28
     readonly exp: number;
   29
     readonly iat: number;
   30
     readonly scope?: string;
   31
     readonly sid?: string;
   32
     readonly org_id?: string;
   33
   }
   34


   35
   export interface Session {
   36
     readonly userId: string;
   37
     readonly scopes: readonly string[];
   38
     readonly organizationId?: string;
   39
     readonly expiresAt: Date;
   40
     readonly issuedAt: Date;
   41
     readonly claims: JWTClaims;
   42
   }
   43


   44
   interface SessionContext {
   45
     session: Session | null;
   46
   }
   47


   48
   interface ClientContext {
   49
     client: Scalekit;
   50
   }
   51


   52
   const sessionContext = createContext({
   53
     name: "scalekit-context-session",
   54
   });
   55


   56
   const clientContext = createContext({
   57
     name: "scalekit-context-client",
   58
   });
   59


   60
   // --- Public accessors (call from tools) ---
   61


   62
   export function getSession(): Session {
   63
     const ctx = sessionContext.getContext();
   64
     if (!ctx.session) {
   65
       throw new Error(
   66
         "[Scalekit] No session. Is the request authenticated?"
   67
       );
   68
     }
   69
     return ctx.session;
   70
   }
   71


   72
   export function getClient(): Scalekit {
   73
     const { client } = clientContext.getContext();
   74
     if (!client) {
   75
       throw new Error(
   76
         "[Scalekit] Client not initialized."
   77
       );
   78
     }
   79
     return client;
   80
   }
   81


   82
   // --- JWT helpers ---
   83


   84
   async function verifyScalekitToken(
   85
     token: string,
   59 collapsed lines
   86
     jwksUrl: URL,
   87
     issuer: string
   88
   ) {
   89
     const JWKS = createRemoteJWKSet(jwksUrl);
   90
     const { payload } = await jwtVerify(token, JWKS, {
   91
       issuer,
   92
       clockTolerance: 30,
   93
     });
   94
     if (!payload.sub) throw new Error("Missing sub claim");
   95
     return payload as unknown as JWTClaims;
   96
   }
   97


   98
   function claimsToSession(claims: JWTClaims): Session {
   99
     return {
   100
       userId: claims.sub,
   101
       scopes: claims.scope ? claims.scope.split(" ") : [],
   102
       organizationId: claims.org_id,
   103
       expiresAt: new Date(claims.exp * 1000),
   104
       issuedAt: new Date(claims.iat * 1000),
   105
       claims,
   106
     };
   107
   }
   108


   109
   function extractBearerToken(
   110
     header: string | undefined
   111
   ): string | null {
   112
     if (!header) return null;
   113
     const parts = header.split(" ");
   114
     if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer")
   115
       return null;
   116
     return parts[1];
   117
   }
   118


   119
   // --- Provider factory ---
   120


   121
   export function scalekitProvider(
   122
     config: ScalekitConfig
   123
   ): Middleware {
   124
     const client = new Scalekit(
   125
       config.environmentUrl,
   126
       config.clientId,
   127
       config.clientSecret
   128
     );
   129


   130
     clientContext.provider({ client }, () => {});
   131
     sessionContext.provider({ session: null }, () => {});
   132


   133
     const envUrl = config.environmentUrl.replace(/\/$/, "");
   134
     const authServerBase = config.resourceId
   135
       ? `${envUrl}/resources/${config.resourceId}`
   136
       : envUrl;
   137


   138
     // Pre-fetch JWKS URI — try OAuth AS metadata first.
   139
     // Resource-specific paths serve /.well-known/oauth-authorization-server
   140
     // but NOT /.well-known/openid-configuration.
   141
     let resolvedJwksUri: URL | null = null;
   142
     (async () => {
   143
       try {
   144
         const urls = [
   145
           `${authServerBase}/.well-known/oauth-authorization-server`,
   146
           `${authServerBase}/.well-known/openid-configuration`,
   147
         ];
   148
         for (const url of urls) {
   149
           const response = await fetch(url);
   150
           if (response.ok) {
   151
             const meta = (await response.json()) as {
   152
               jwks_uri?: string;
   153
             };
   154
             if (meta.jwks_uri) {
   155
               resolvedJwksUri = new URL(meta.jwks_uri);
   156
               console.log(
   157
                 "[Scalekit] Resolved JWKS URI:",
   158
                 resolvedJwksUri.toString()
   159
               );
   160
               return;
   161
             }
   162
           }
   163
         }
   164
       } catch (e) {
   165
         console.warn("[Scalekit] Could not pre-fetch JWKS URI:", e);
   166
       }
   167
     })();
   168


   169
     return {
   170
       middleware: buildMiddleware(
   171
         config,
   172
         authServerBase,
   173
         () => resolvedJwksUri
   174
       ),
   175
       router: buildRouter(config, authServerBase),
   176
     };
   177
   }
   178


   179
   // --- Router (discovery endpoints) ---
   180


   181
   function buildRouter(
   182
     config: ScalekitConfig,
   183
     authServerBase: string
   184
   ): Router {
   185
     const router = Router();
   186
     const baseUrl = config.baseURL.replace(/\/$/, "");
   187


   188
     // RFC 9728: Protected Resource Metadata
   189
     router.get(
   190
       "/.well-known/oauth-protected-resource",
   191
       (_req: Request, res: Response) => {
   192
         res.json({
   193
           resource: baseUrl,
   194
           authorization_servers: [authServerBase],
   195
           bearer_methods_supported: ["header"],
   196
           ...(config.scopes &&
   197
             config.scopes.length > 0 && {
   198
               scopes_supported: config.scopes,
   199
             }),
   200
         });
   201
       }
   202
     );
   203


   204
     // RFC 8414: Authorization Server Metadata (proxied from Scalekit)
   205
     // Tries /.well-known/oauth-authorization-server first because
   206
     // resource-specific paths include registration_endpoint for DCR.
   207
     router.get(
   208
       "/.well-known/oauth-authorization-server",
   209
       async (_req: Request, res: Response) => {
   210
         try {
   211
           const asUrl = `${authServerBase}/.well-known/oauth-authorization-server`;
   212
           const asRes = await fetch(asUrl);
   213
           if (asRes.ok) {
   214
             res.json(await asRes.json());
   215
             return;
   216
           }
   217
           const oidcUrl = `${authServerBase}/.well-known/openid-configuration`;
   218
           const oidcRes = await fetch(oidcUrl);
   219
           if (oidcRes.ok) {
   220
             res.json(await oidcRes.json());
   221
             return;
   222
           }
   223
           res.status(502).json({
   224
             error: "Failed to fetch authorization server metadata",
   225
           });
   226
         } catch {
   227
           res.status(502).json({
   228
             error: "Failed to fetch authorization server metadata",
   229
           });
   230
         }
   231
       }
   232
     );
   233


   234
     return router;
   235
   }
   236


   237
   // --- Middleware (token validation) ---
   238


   239
   function buildMiddleware(
   240
     config: ScalekitConfig,
   241
     authServerBase: string,
   242
     getJwksUri: () => URL | null
   243
   ): RequestHandler {
   244
     const wwwAuth =
   245
       'Bearer resource_metadata="/.well-known/oauth-protected-resource"';
   246


   247
     return async (
   248
       req: Request,
   249
       res: Response,
   250
       next: NextFunction
   251
     ) => {
   252
       if (!req.path.startsWith("/mcp")) {
   253
         next();
   254
         return;
   255
       }
   256


   257
       const token = extractBearerToken(req.headers.authorization);
   258
       if (!token) {
   259
         res.setHeader("WWW-Authenticate", wwwAuth);
   260
         res.status(401).json({
   261
           error: "unauthorized",
   262
           error_description: "Missing or invalid bearer token",
   263
         });
   264
         return;
   265
       }
   266


   267
       try {
   268
         const jwksUrl =
   269
           getJwksUri() ||
   270
           new URL(`${authServerBase}/.well-known/jwks`);
   271


   272
         // Validate against authServerBase, the server's own issuer:
   273
         // environmentUrl/resources/. Workspaces created before
   274
         // October 21, 2026: see the migration guide first.
   275
         const claims = await verifyScalekitToken(
   276
           token,
   277
           jwksUrl,
   278
           authServerBase
   279
         );
   280


   281
         const session = claimsToSession(claims);
   282
         sessionContext.provider({ session }, () => next());
   283
       } catch {
   284
         res.setHeader(
   285
           "WWW-Authenticate",
   286
           `${wwwAuth}, error="invalid_token"`
   287
         );
   288
         res.status(401).json({
   289
           error: "invalid_token",
   290
           error_description: "Token verification failed",
   291
         });
   292
       }
   293
     };
   294
   }
   ```

   Three details that are easy to get wrong:

   * **JWKS resolution:** Resource-specific paths on Scalekit serve `/.well-known/oauth-authorization-server` but return 404 for `/.well-known/openid-configuration`. The provider tries both in order.
   * **AS metadata proxy:** The resource-specific `oauth-authorization-server` response includes the `registration_endpoint` that DCR clients need. The environment-level `openid-configuration` does not.
   * **Issuer validation:** The middleware validates against `authServerBase`, which is `/resources/`, the server’s own issuer. The verifier uses `jose`, whose `issuer` option accepts an array of accepted issuers. For MCP servers in workspaces created before October 21, 2026, first [migrate to the new MCP spec](/authenticate/mcp/migrate-to-2026-07-28-mcp-spec/).

6. ## Wire the middleware

   [Section titled “Wire the middleware”](#wire-the-middleware)

   Create `src/middleware.ts` — this is the file xmcp looks for to apply auth middleware to all requests.

   src/middleware.ts

   ```typescript
   import { scalekitProvider } from "./lib/scalekit-auth";


   export default scalekitProvider({
     environmentUrl: process.env.SCALEKIT_ENVIRONMENT_URL!,
     clientId: process.env.SCALEKIT_CLIENT_ID!,
     clientSecret: process.env.SCALEKIT_CLIENT_SECRET!,
     baseURL: process.env.BASE_URL || "http://localhost:3001",
     resourceId: process.env.SCALEKIT_RESOURCE_ID,
     scopes: ["openid", "profile", "email"],
   });
   ```

   xmcp middleware convention

   xmcp automatically loads `src/middleware.ts` as a special entry point. Tools cannot import from this file directly — shared code like `getSession()` lives in `src/lib/` instead.

7. ## Add tools

   [Section titled “Add tools”](#add-tools)

   xmcp uses file-based routing — each file in `src/tools/` becomes an MCP tool. Create two tools that use the authenticated session.

   src/tools/whoami.ts

   ```typescript
   1
   import { type ToolMetadata } from "xmcp";
   2
   import { getSession } from "../lib/scalekit-auth";
   3


   4
   export const metadata: ToolMetadata = {
   5
     name: "whoami",
   6
     description:
   7
       "Returns the authenticated user's session information",
   8
   };
   9


   10
   export default function whoami(): string {
   11
     const session = getSession();
   12
     return JSON.stringify(
   13
       {
   14
         userId: session.userId,
   15
         scopes: session.scopes,
   16
         organizationId: session.organizationId || "N/A",
   17
         expiresAt: session.expiresAt.toISOString(),
   18
         issuedAt: session.issuedAt.toISOString(),
   19
       },
   20
       null,
   21
       2
   22
     );
   23
   }
   ```

   src/tools/greet.ts

   ```typescript
   1
   import { z } from "zod";
   2
   import { type InferSchema, type ToolMetadata } from "xmcp";
   3
   import { getSession } from "../lib/scalekit-auth";
   4


   5
   export const schema = {
   6
     name: z.string().describe("The name to greet"),
   7
   };
   8


   9
   export const metadata: ToolMetadata = {
   10
     name: "greet",
   11
     description: "Greet the user with their Scalekit identity",
   12
   };
   13


   14
   export default function greet({
   15
     name,
   16
   }: InferSchema): string {
   17
     const session = getSession();
   18
     return `Hello, ${name}! Your user ID is ${session.userId}`;
   19
   }
   ```

8. ## Start the server

   [Section titled “Start the server”](#start-the-server)

   Terminal

   ```bash
   npm run dev
   ```

   You should see:

   ```plaintext
   ✔ MCP Server running on http://127.0.0.1:3001/mcp
   [Scalekit] Resolved JWKS URI: https://.scalekit.com/keys
   ```

   The JWKS log confirms the server successfully connected to Scalekit and can validate tokens.

9. ## Test with MCP Inspector

   [Section titled “Test with MCP Inspector”](#test-with-mcp-inspector)

   Verify the full flow using MCP Inspector.

   Terminal

   ```bash
   npx @modelcontextprotocol/inspector@latest
   ```

   In the Inspector UI:

   1. Set transport to **Streamable HTTP**, URL to `http://localhost:3001/mcp`, connection to **Direct**

   2. Get an access token using client credentials:

      Terminal

      ```bash
      curl -s -X POST "$SCALEKIT_ENVIRONMENT_URL/oauth/token" \
        -H "Content-Type: application/x-www-form-urlencoded" \
        -d "grant_type=client_credentials\
      &client_id=$SCALEKIT_CLIENT_ID\
      &client_secret=$SCALEKIT_CLIENT_SECRET" | jq .access_token -r
      ```

   3. Enable the **Authorization** custom header and set its value to `Bearer `

   4. Click **Connect** — you should see `whoami` and `greet` in the Tools tab

   Connect from Claude Code

   MCP clients that support OAuth 2.1 handle the full flow automatically — DCR, authorization code + PKCE, and token refresh. For Claude Code:

   ```bash
   claude mcp add --transport http xmcp-server http://localhost:3001/mcp
   ```

You now have a working xmcp MCP server with Scalekit OAuth 2.1 authentication. Add more tools by creating files in `src/tools/`, and access the authenticated user from any tool via `getSession()`. For production deployment, build with `npm run build` and run the compiled output with `npm start`.

---
# DOCUMENT BOUNDARY
---

# Test users

> Set up test users to run automated E2E tests for login and signup flows

Test users let you run Playwright, Cypress, and other E2E tests against signup and login flows without waiting for real OTP or magic-link emails. Scalekit accepts a static 6-digit code that you configure, so tests are fast, reliable, and inbox-free.

**What doesn’t change:** Test users only affects OTP email delivery. Everything else — organization creation, user creation, session creation, audit logs, webhooks, and post-login redirects — behaves the same as it does for a real user.

### Prerequisites

[Section titled “Prerequisites”](#prerequisites)

Enable the **Magic Link & OTP** auth method, with the delivery option set to `Verification Code` or `Magic Link + Verification Code`. `Magic Link`-only delivery is not supported. ![Magic link/OTP configuration](/.netlify/images?url=_astro%2F2026-05-14-10-45-39.sA74CAt7.png\&w=2906\&h=1408\&dpl=6ae471fc143c9a4c27550463)

## Configure test users

[Section titled “Configure test users”](#configure-test-users)

The test users allowlist is set per environment and accepts up to 5 emails. Configure it once — every signup and login test in that environment uses the same emails and static code.

1. ### Open Test users settings

   [Section titled “Open Test users settings”](#open-test-users-settings)

   Go to **Dashboard > Environment settings > Test users**. ![Test users settings page](/.netlify/images?url=_astro%2Ftest-users-settings.zn18KeZw.png\&w=2176\&h=1410\&dpl=6ae471fc143c9a4c27550463)

2. ### Enable and add emails to the allowlist

   [Section titled “Enable and add emails to the allowlist”](#enable-and-add-emails-to-the-allowlist)

   Toggle **Enable test users**. Add up to 5 test emails — emails are stored lowercase and de-duplicated. Each entry must contain `+sktest` in the local part (before `@`); any domain works.

   | Email                         | Valid? | Reason                                |
   | ----------------------------- | ------ | ------------------------------------- |
   | `john+sktest@acmecorp.com`    | ✓      | `+sktest` present in the local part   |
   | `bob+sktest@anything.example` | ✓      | Any domain works                      |
   | `john@acmecorp.com`           | ✗      | Missing `+sktest` marker              |
   | `john+test@acmecorp.com`      | ✗      | Marker must be exactly `+sktest`      |
   | `sktest+john@acmecorp.com`    | ✗      | Must appear after `+`, not before `@` |

   ![Test users allowlist with an email added](/.netlify/images?url=_astro%2Ftest-users-allowlist.BzUbAWIw.png\&w=2174\&h=1410\&dpl=6ae471fc143c9a4c27550463)

3. ### Set the static confirmation code

   [Section titled “Set the static confirmation code”](#set-the-static-confirmation-code)

   Enter a **Static confirmation code** — exactly 6 digits. The default is `424242`. Tests will enter this code at the OTP prompt.

4. ### Save

   [Section titled “Save”](#save)

   Click **Save** and reload the page to confirm the settings persisted.

## Test the signup flow

[Section titled “Test the signup flow”](#test-the-signup-flow)

Use this flow to verify that a new user can complete signup and end up as a member of an organization.

Use environment variables

Store the test email, static code, and authorization URL as environment variables (`SCALEKIT_TEST_USER_EMAIL`, `SCALEKIT_TEST_USER_CODE`, `SCALEKIT_TEST_AUTH_URL`) — don’t hard-code them in test files.

Point the test at Scalekit’s authorization endpoint with `prompt=create` so the browser lands directly on the hosted signup screen:

Authorization URL (signup)

```sh
/oauth/authorize?
  response_type=code&
  client_id=&
  redirect_uri=&
  scope=openid+profile+email+offline_access&
  state=&
  prompt=create
```

After OTP verification, Scalekit redirects to `` with an authorization code. Assert on the callback URL in your test.

* Playwright

  playwright/e2e/signup.spec.ts

  ```ts
  1
  import { test, expect } from '@playwright/test'
  2


  3
  const AUTH_URL        = process.env.SCALEKIT_TEST_AUTH_URL!
  4
  const TEST_USER_EMAIL = process.env.SCALEKIT_TEST_USER_EMAIL!
  5
  const TEST_USER_CODE  = process.env.SCALEKIT_TEST_USER_CODE!
  6


  7
  test('OTP signup with test user', async ({ page }) => {
  8
    await page.goto(AUTH_URL)
  9


  10
    await page.fill('input[name="email"]', TEST_USER_EMAIL)
  11
    await page.getByRole('button', { name: 'Continue', exact: true }).click()
  12


  13
    await page.locator('[data-scope="pin-input"][data-part="input"]').first().pressSequentially(TEST_USER_CODE)
  14
    await page.getByRole('button', { name: 'Continue', exact: true }).click()
  15


  16
    // Replace with your app's post-login URL pattern
  17
    await expect(page).toHaveURL(/dashboard\.sampleapp\.com/)
  18
  })
  ```

* Cypress

  cypress/e2e/signup.cy.ts

  ```ts
  1
  const AUTH_URL        = Cypress.env('SCALEKIT_TEST_AUTH_URL')
  2
  const TEST_USER_EMAIL = Cypress.env('SCALEKIT_TEST_USER_EMAIL')
  3
  const TEST_USER_CODE  = Cypress.env('SCALEKIT_TEST_USER_CODE')
  4


  5
  describe('OTP signup with test user', () => {
  6
    it('signs up using the static code', () => {
  7
      cy.visit(AUTH_URL)
  8


  9
      cy.get('input[name="email"]').type(TEST_USER_EMAIL)
  10
      cy.get('button[type="submit"]').contains('Continue').click()
  11


  12
      cy.get('[data-scope="pin-input"][data-part="input"]').first().type(TEST_USER_CODE)
  13
      cy.get('button[type="submit"]').contains('Continue').click()
  14


  15
      // Replace with your app's post-login URL pattern
  16
      cy.url().should('include', 'dashboard.sampleapp.com')
  17
    })
  18
  })
  ```

**Verify:** After running the test, check:

* A new organization exists with the test user as a member. Verify in **Dashboard > Organizations > Newly created organization > Users**. ![New organization and user created](/.netlify/images?url=_astro%2F2026-05-14-10-37-41.C1uM9642.png\&w=2910\&h=1404\&dpl=6ae471fc143c9a4c27550463)

* **Auth logs** in **Dashboard > Auth logs** show two test-user events:

  * *Verification email suppressed (test user)*
  * *OTP verification successful (test user)*

  ![Auth logs for test users in signup flow](/.netlify/images?url=_astro%2F2026-05-14-10-40-10.D-kzd9Ri.png\&w=2916\&h=1386\&dpl=6ae471fc143c9a4c27550463)

## Test the login flow

[Section titled “Test the login flow”](#test-the-login-flow)

Use this flow to verify login for a user that already exists in an organization. The test user must be created before the test runs. Add them manually in **Dashboard > Organizations > Newly created organization > Users > Create user**, or create them programmatically with the Scalekit API or SDK:

* Node.js

  Create test user in an organization

  ```ts
  import { ScalekitClient } from '@scalekit-sdk/node'


  const scalekit = new ScalekitClient(
    process.env.SCALEKIT_ENVIRONMENT_URL!,
    process.env.SCALEKIT_CLIENT_ID!,
    process.env.SCALEKIT_CLIENT_SECRET!
  )


  const { user } = await scalekit.user.createUserAndMembership(orgId, {
    email: 'john+sktest@acmecorp.com',
    sendInvitationEmail: false,
  })
  ```

* Python

  Create test user in an organization

  ```python
  import os
  from scalekit import ScalekitClient
  from scalekit.v1.users.users_pb2 import CreateUser


  scalekit_client = ScalekitClient(
    env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"],
    client_id=os.environ["SCALEKIT_CLIENT_ID"],
    client_secret=os.environ["SCALEKIT_CLIENT_SECRET"]
  )


  user_response = scalekit_client.users.create_user_and_membership(
    org_id,
    CreateUser(email="john+sktest@acmecorp.com"),
    send_invitation_email=False,
  )
  ```

* Go

  Create test user in an organization

  ```go
  import usersv1 "github.com/scalekit-inc/scalekit-sdk-go/v2/pkg/grpc/scalekit/v1/users"


  scalekitClient := scalekit.NewScalekitClient(
    os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
    os.Getenv("SCALEKIT_CLIENT_ID"),
    os.Getenv("SCALEKIT_CLIENT_SECRET"),
  )


  userResp, err := scalekitClient.User().CreateUserAndMembership(ctx, orgID,
    &usersv1.CreateUser{Email: "john+sktest@acmecorp.com"},
    false,
  )
  if err != nil {
    log.Fatalf("failed to create user: %v", err)
  }
  ```

* Java

  Create test user in an organization

  ```java
  import com.scalekit.grpc.scalekit.v1.users.*;


  ScalekitClient scalekitClient = new ScalekitClient(
    System.getenv("SCALEKIT_ENVIRONMENT_URL"),
    System.getenv("SCALEKIT_CLIENT_ID"),
    System.getenv("SCALEKIT_CLIENT_SECRET")
  );


  CreateUserAndMembershipRequest request = CreateUserAndMembershipRequest.newBuilder()
    .setUser(CreateUser.newBuilder().setEmail("john+sktest@acmecorp.com").build())
    .setSendInvitationEmail(false)
    .build();


  CreateUserAndMembershipResponse userResp = scalekitClient.users()
    .createUserAndMembership(orgId, request);
  ```

Make sure the same email is also on the [test users allowlist](#enable-and-add-emails-to-the-allowlist).

### Run the test

[Section titled “Run the test”](#run-the-test)

Point the test at Scalekit’s authorization endpoint with `prompt=login` so the browser lands directly on the hosted login screen:

Authorization URL (login)

```sh
/oauth/authorize?
  response_type=code&
  client_id=&
  redirect_uri=&
  scope=openid+profile+email+offline_access&
  state=&
  prompt=login
```

After OTP verification, Scalekit redirects to `` with an authorization code. Assert on the callback URL in your test.

* Playwright

  playwright/e2e/login.spec.ts

  ```ts
  1
  import { test, expect } from '@playwright/test'
  2


  3
  const AUTH_URL        = process.env.SCALEKIT_TEST_AUTH_URL!
  4
  const TEST_USER_EMAIL = process.env.SCALEKIT_TEST_USER_EMAIL!
  5
  const TEST_USER_CODE  = process.env.SCALEKIT_TEST_USER_CODE!
  6


  7
  test('OTP login with test user', async ({ page }) => {
  8
    await page.goto(AUTH_URL)
  9


  10
    await page.fill('input[name="email"]', TEST_USER_EMAIL)
  11
    await page.getByRole('button', { name: 'Continue', exact: true }).click()
  12


  13
    await page.locator('[data-scope="pin-input"][data-part="input"]').first().pressSequentially(TEST_USER_CODE)
  14
    await page.getByRole('button', { name: 'Continue', exact: true }).click()
  15


  16
    // Replace with your app's post-login URL pattern
  17
    await expect(page).toHaveURL(/dashboard\.sampleapp\.com/)
  18
  })
  ```

* Cypress

  cypress/e2e/login.cy.ts

  ```ts
  1
  const AUTH_URL        = Cypress.env('SCALEKIT_TEST_AUTH_URL')
  2
  const TEST_USER_EMAIL = Cypress.env('SCALEKIT_TEST_USER_EMAIL')
  3
  const TEST_USER_CODE  = Cypress.env('SCALEKIT_TEST_USER_CODE')
  4


  5
  describe('OTP login with test user', () => {
  6
    it('logs in using the static code', () => {
  7
      cy.visit(AUTH_URL)
  8


  9
      cy.get('input[name="email"]').type(TEST_USER_EMAIL)
  10
      cy.get('button[type="submit"]').contains('Continue').click()
  11


  12
      cy.get('[data-scope="pin-input"][data-part="input"]').first().type(TEST_USER_CODE)
  13
      cy.get('button[type="submit"]').contains('Continue').click()
  14


  15
      // Replace with your app's post-login URL pattern
  16
      cy.url().should('include', 'dashboard.sampleapp.com')
  17
    })
  18
  })
  ```

**Verify:** After running the test, check:

* **Auth logs** in **Dashboard > Auth logs** show two test-user events:

  * *Verification email suppressed (test user)*
  * *OTP verification successful (test user)*

  ![Auth log showing test-user events](/.netlify/images?url=_astro%2Ftest-users-auth-log.DqWyyLRk.png\&w=2916\&h=1388\&dpl=6ae471fc143c9a4c27550463)

## Production safety

[Section titled “Production safety”](#production-safety)

Test Users bypass real email verification

Keep Test Users off in production environments unless you have a deliberate reason to enable it. When it is on, a warning banner appears in the dashboard.

## Common scenarios

[Section titled “Common scenarios”](#common-scenarios)

Why am I still getting a real verification email?

All three conditions must be true for Test Users to apply:

* **Test Users** feature is enabled in the environment.
* The exact email is on the allowlist (case-insensitive match).
* **Magic Link & OTP** auth method is **enabled** and the configuration is set to `Verification Code` or `Magic Link + Verification Code`.

Can I use a different code for each test run?

No, the static verification code is set per environment. For parallel isolation, add multiple `+sktest` addresses (up to 5) and assign a different one to each test run.

How do I keep Test Users out of production CI?

Never enable Test Users in your production environment. In CI, point to a dev or staging environment. Guard your test runner with an environment check — for example, require `SCALEKIT_ENVIRONMENT_URL` to contain `.dev.` or `.staging.` before running E2E tests.

---
# DOCUMENT BOUNDARY
---

# Add Modular SSO

> Enable enterprise SSO for any customer in minutes with built-in SAML and OIDC integrations

Enterprise customers often require Single Sign-On (SSO) support for their applications. Rather than building custom integrations for every identity provider—such as Okta, Entra ID, or JumpCloud—and managing the detailed configuration of OIDC and SAML protocols, there are more scalable approaches available.

See a walkthrough of the integration

[Play](https://youtube.com/watch?v=I7SZyFhKg-s)

Review the authentication sequence

After your customer’s identity provider verifies the user, Scalekit forwards the authentication response directly to your application. You receive the verified identity claims and handle all subsequent user management—creating accounts, managing sessions, and controlling access—using your own systems.

![Diagram showing the SSO authentication flow: User initiates login → Scalekit handles protocol translation → Identity Provider authenticates → User gains access to your application](/.netlify/images?url=_astro%2F1.Bj4LD99k.png\&w=4936\&h=3744\&dpl=6ae471fc143c9a4c27550463)

This approach gives you maximum flexibility to integrate SSO into existing authentication architectures while offloading the complexity of SAML and OIDC protocol handling to Scalekit.

Modular SSO is designed for applications that maintain their own user database and session management. This lightweight integration focuses solely on identity verification, giving you complete control over user data and authentication flows.

Choose Modular SSO when you:

* Want to manage user records in your own database
* Prefer to implement custom session management logic
* Need to integrate SSO without changing your existing authentication architecture
* Already have existing user management infrastructure

Using Full stack auth?

[Full stack auth](/authenticate/fsa/quickstart/) includes SSO functionality by default. If you’re using Full stack auth, you can skip this guide.

Using a coding agent?Copy SSO prompt

1. ## Configure “Modular Auth” mode

   [Section titled “Configure “Modular Auth” mode”](#configure-modular-auth-mode)

   Ensure your environment is configured in Modular Auth mode.

   1. Go to Dashboard > Settings > Authentication Mode
   2. Select “Modular Auth” and save

   Now you’re ready to start integrating SSO into your app! Next, we’ll cover how to use the SDK to authenticate users.

2. ## Set up Scalekit

   [Section titled “Set up Scalekit”](#set-up-scalekit)

   Use the following instructions to install the SDK for your technology stack.

   * Node.js

     ```bash
     npm install @scalekit-sdk/node
     ```

   * Python

     ```sh
     pip install scalekit-sdk-python
     ```

   * Go

     ```sh
     go get -u github.com/scalekit-inc/scalekit-sdk-go
     ```

   * Java

     ```groovy
     /* Gradle users - add the following to your dependencies in build file */
     implementation "com.scalekit:scalekit-sdk-java:2.1.3"
     ```

     ```xml
     
     
         com.scalekit
         scalekit-sdk-java
         2.1.3
     
     ```

   Configure your environment with API credentials. Navigate to **Dashboard > Developers > Settings > API credentials** and copy these values to your `.env` file:

   .env

   ```sh
   SCALEKIT_ENVIRONMENT_URL= # Example: https://acme.scalekit.dev or https://auth.acme.com (if custom domain is set)
   SCALEKIT_CLIENT_ID= # Example: skc_1234567890abcdef
   SCALEKIT_CLIENT_SECRET= # Example: test_abcdef1234567890
   ```

   ### Register redirect URL for your app

   [Section titled “Register redirect URL for your app”](#register-redirect-url-for-your-app)

   You need to register redirect URL for your application. Go to **Scalekit dashboard** → **Authentication** → **Redirect URLs** and configure:

   * **Allowed callback URLs**: The endpoint where users are sent after successful authentication to exchange authorization codes and retrieve profile information. [Learn more](/guides/dashboard/redirects/#allowed-callback-urls)
   * **Initiate login URL**: The endpoint in your app that redirects users to Scalekit’s `/authorize` endpoint. Required when user starts sign-in directly from their identity provider (IdP-initiated SSO). [Learn more](/guides/dashboard/redirects/#initiate-login-url)

3. ## Redirect the users to their enterprise identity provider login page

   [Section titled “Redirect the users to their enterprise identity provider login page”](#redirect-the-users-to-their-enterprise-identity-provider-login-page)

   Create an authorization URL to redirect users to Scalekit’s sign-in page. Use the Scalekit SDK to construct this URL with your redirect URI and required scopes.

   * Node.js

     authorization-url.js

     ```javascript
     8 collapsed lines
     1
     import { Scalekit } from '@scalekit-sdk/node';
     2


     3
     const scalekit = new ScalekitClient(
     4
       '',    // Your Scalekit environment URL
     5
       '',          // Unique identifier for your app
     6
       '',
     7
     );
     8


     9
     const options = {};
     10


     11
     // Specify which SSO connection to use (choose one based on your use case)
     12
     // These identifiers are evaluated in order of precedence:
     13


     14
     // 1. connectionId (highest precedence) - Use when you know the exact SSO connection
     15
     options['connectionId'] = 'conn_15696105471768821';
     16


     17
     // 2. organizationId - Routes to organization's SSO (useful for multi-tenant apps)
     18
     //    If org has multiple connections, the first active one is selected
     19
     options['organizationId'] = 'org_15421144869927830';
     20


     21
     // 3. loginHint (lowest precedence) - Extracts domain from email to find connection
     22
     //    Domain must be registered to the organization (manually via Dashboard or through admin portal during enterprise onboarding)
     23
     options['loginHint'] = 'user@example.com';
     24


     25
     // redirect_uri: Your callback endpoint that receives the authorization code
     26
     // Must match the URL registered in your Scalekit dashboard
     27
     const redirectUrl = 'https://your-app.com/auth/callback';
     28


     29
     const authorizationURL = scalekit.getAuthorizationUrl(redirectUrl, options);
     30
     // Redirect user to this URL to begin SSO authentication
     ```

   * Python

     authorization_url.py

     ```python
     8 collapsed lines
     1
     from scalekit import ScalekitClient, AuthorizationUrlOptions
     2


     3
     scalekit = ScalekitClient(
     4
       '',    # Your Scalekit environment URL
     5
       '',          # Unique identifier for your app
     6
       ''
     7
     )
     8


     9
     options = AuthorizationUrlOptions()
     10


     11
     # Specify which SSO connection to use (choose one based on your use case)
     12
     # These identifiers are evaluated in order of precedence:
     13


     14
     # 1. connection_id (highest precedence) - Use when you know the exact SSO connection
     15
     options.connection_id = 'conn_15696105471768821'
     16


     17
     # 2. organization_id - Routes to organization's SSO (useful for multi-tenant apps)
     18
     #    If org has multiple connections, the first active one is selected
     19
     options.organization_id = 'org_15421144869927830'
     20


     21
     # 3. login_hint (lowest precedence) - Extracts domain from email to find connection
     22
     #    Domain must be registered to the organization (manually via Dashboard or through admin portal during enterprise onboarding)
     23
     options.login_hint = 'user@example.com'
     24


     25
     # redirect_uri: Your callback endpoint that receives the authorization code
     26
     # Must match the URL registered in your Scalekit dashboard
     27
     redirect_uri = 'https://your-app.com/auth/callback'
     28


     29
     authorization_url = scalekit_client.get_authorization_url(
     30
       redirect_uri=redirect_uri,
     31
       options=options
     32
     )
     33
     # Redirect user to this URL to begin SSO authentication
     ```

   * Go

     authorization_url.go

     ```go
     11 collapsed lines
     1
     import (
     2
       "github.com/scalekit-inc/scalekit-sdk-go"
     3
     )
     4


     5
     func main() {
     6
       scalekitClient := scalekit.NewScalekitClient(
     7
         "",  // Your Scalekit environment URL
     8
         "",        // Unique identifier for your app
     9
         ""
     10
       )
     11


     12
       options := scalekitClient.AuthorizationUrlOptions{}
     13


     14
       // Specify which SSO connection to use (choose one based on your use case)
     15
       // These identifiers are evaluated in order of precedence:
     16


     17
       // 1. ConnectionId (highest precedence) - Use when you know the exact SSO connection
     18
       options.ConnectionId = "conn_15696105471768821"
     19


     20
       // 2. OrganizationId - Routes to organization's SSO (useful for multi-tenant apps)
     21
       //    If org has multiple connections, the first active one is selected
     22
       options.OrganizationId = "org_15421144869927830"
     23


     24
       // 3. LoginHint (lowest precedence) - Extracts domain from email to find connection
     25
       //    Domain must be registered to the organization (manually via Dashboard or through admin portal during enterprise onboarding)
     26
       options.LoginHint = "user@example.com"
     27


     28
       // redirectUrl: Your callback endpoint that receives the authorization code
     29
       // Must match the URL registered in your Scalekit dashboard
     30
       redirectUrl := "https://your-app.com/auth/callback"
     31


     32
       authorizationURL := scalekitClient.GetAuthorizationUrl(
     33
         redirectUrl,
     34
         options,
     35
       )
     36
       // Redirect user to this URL to begin SSO authentication
     37
     }
     ```

   * Java

     AuthorizationUrl.java

     ```java
     1
     package com.scalekit;
     2


     3
     import com.scalekit.ScalekitClient;
     4
     import com.scalekit.internal.http.AuthorizationUrlOptions;
     5


     6
     public class Main {
     7


     8
       public static void main(String[] args) {
     9
         ScalekitClient scalekitClient = new ScalekitClient(
     10
           "",  // Your Scalekit environment URL
     11
           "",        // Unique identifier for your app
     12
           ""
     13
         );
     14


     15
         AuthorizationUrlOptions options = new AuthorizationUrlOptions();
     16


     17
         // Specify which SSO connection to use (choose one based on your use case)
     18
         // These identifiers are evaluated in order of precedence:
     19


     20
         // 1. connectionId (highest precedence) - Use when you know the exact SSO connection
     21
         options.setConnectionId("con_13388706786312310");
     22


     23
        // 2. organizationId - Routes to organization's SSO (useful for multi-tenant apps)
     24
        //    If org has multiple connections, the first active one is selected
     25
        options.setOrganizationId("org_13388706786312310");
     26


     27
        // 3. loginHint (lowest precedence) - Extracts domain from email to find connection
     28
        //    Domain must be registered to the organization (manually via Dashboard or through admin portal during enterprise onboarding)
     29
        options.setLoginHint("user@example.com");
     30


     31
         // redirectUrl: Your callback endpoint that receives the authorization code
     32
         // Must match the URL registered in your Scalekit dashboard
     33
         String redirectUrl = "https://your-app.com/auth/callback";
     34


     35
         try {
     36
           String url = scalekitClient
     37
             .authentication()
     38
             .getAuthorizationUrl(redirectUrl, options)
     39
             .toString();
     40
           // Redirect user to this URL to begin SSO authentication
     41
         } catch (Exception e) {
     42
           System.out.println(e.getMessage());
     43
         }
     44
       }
     45
     }
     ```

   * Direct URL (No SDK)

     OAuth2 authorization URL

     ```sh
     /oauth/authorize?
       response_type=code&                        # OAuth2 authorization code flow
       client_id=&            # Your Scalekit client ID
       redirect_uri=&               # URL-encoded callback URL
       scope=openid profile email&                # Note: "offline_access" scope is not supported in Modular SSO
       organization_id=org_15421144869927830&     # (Optional) Route by organization
       connection_id=conn_15696105471768821&      # (Optional) Specific SSO connection
       login_hint=user@example.com                # (Optional) Extract domain from email
     ```

     **SSO identifiers** (choose one or more, evaluated in order of precedence):

     * `connection_id` - Direct to specific SSO connection (highest precedence)
     * `organization_id` - Route to organization’s SSO
     * `domain_hint` - Lookup connection by domain
     * `login_hint` - Extract domain from email (lowest precedence). Domain must be registered to the organization (manually via Dashboard or through admin portal when [onboarding an enterprise customer](/sso/guides/onboard-enterprise-customers/))

     Example with actual values

     ```http
     https://tinotat-dev.scalekit.dev/oauth/authorize?
       response_type=code&
       client_id=skc_88036702639096097&
       redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback&
       scope=openid%20profile%20email&
       organization_id=org_15421144869927830
     ```

   Enterprise users see their identity provider’s login page. Users verify their identity through the authentication policies set by their organization’s administrator. Post successful verification, the user profile is [normalized](/sso/guides/user-profile-details/) and sent to your app.

   For details on how Scalekit determines which SSO connection to use, refer to the [SSO identifier precedence rules](/sso/guides/authorization-url/#parameter-precedence).

4. ## Handle IdP-initiated SSO Recommended

   [Section titled “Handle IdP-initiated SSO ”](#handle-idp-initiated-sso-)

   When users start the login process from their identity provider’s portal (rather than your application), this is called IdP-initiated SSO. Scalekit converts these requests to secure SP-initiated flows automatically.

   Your initiate login endpoint receives an `idp_initiated_login` JWT parameter containing the user’s organization and connection details. Decode this token and generate a new authorization URL to complete the authentication flow securely.

   ```sh
   https://yourapp.com/login?idp_initiated_login=
   ```

   Configure your initiate login endpoint in [Dashboard > Authentication > Redirects](/guides/dashboard/redirects/#initiate-login-url)

   * Node.js

     handle-idp-initiated.js

     ```javascript
     1
     // Your initiate login endpoint receives the IdP-initiated login token
     2
     const { idp_initiated_login, error, error_description } = req.query;
     5 collapsed lines
     3


     4
     if (error) {
     5
       return res.status(400).json({ message: error_description });
     6
     }
     7


     8
     // When users start login from their IdP portal, convert to SP-initiated flow
     9
     if (idp_initiated_login) {
     10
       // Decode the JWT to extract organization and connection information
     11
       const claims = await scalekit.getIdpInitiatedLoginClaims(idp_initiated_login);
     12


     13
       const options = {
     14
         connectionId: claims.connection_id,      // Specific SSO connection
     15
         organizationId: claims.organization_id,  // User's organization
     16
         loginHint: claims.login_hint,            // User's email for context
     17
         state: claims.relay_state                // Preserve state from IdP
     18
       };
     19


     20
       // Generate authorization URL and redirect to complete authentication
     21
       const authorizationURL = scalekit.getAuthorizationUrl(
     22
         'https://your-app.com/auth/callback',
     23
         options
     24
       );
     25


     26
       return res.redirect(authorizationURL);
     27
     }
     ```

   * Python

     handle_idp_initiated.py

     ```python
     1
     # Your initiate login endpoint receives the IdP-initiated login token
     2
     idp_initiated_login = request.args.get('idp_initiated_login')
     3
     error = request.args.get('error')
     4
     error_description = request.args.get('error_description')
     4 collapsed lines
     5


     6
     if error:
     7
         raise Exception(error_description)
     8


     9
     # When users start login from their IdP portal, convert to SP-initiated flow
     10
     if idp_initiated_login:
     11
         # Decode the JWT to extract organization and connection information
     12
         claims = await scalekit.get_idp_initiated_login_claims(idp_initiated_login)
     13


     14
         options = AuthorizationUrlOptions()
     15
         options.connection_id = claims.get('connection_id')      # Specific SSO connection
     16
         options.organization_id = claims.get('organization_id')  # User's organization
     17
         options.login_hint = claims.get('login_hint')            # User's email for context
     18
         options.state = claims.get('relay_state')                # Preserve state from IdP
     19


     20
         # Generate authorization URL and redirect to complete authentication
     21
         authorization_url = scalekit.get_authorization_url(
     22
             redirect_uri='https://your-app.com/auth/callback',
     23
             options=options
     24
         )
     25


     26
         return redirect(authorization_url)
     ```

   * Go

     handle_idp_initiated.go

     ```go
     1
     // Your initiate login endpoint receives the IdP-initiated login token
     2
     idpInitiatedLogin := r.URL.Query().Get("idp_initiated_login")
     3
     errorDesc := r.URL.Query().Get("error_description")
     4


     5 collapsed lines
     5
     if errorDesc != "" {
     6
         http.Error(w, errorDesc, http.StatusBadRequest)
     7
         return
     8
     }
     9


     10
     // When users start login from their IdP portal, convert to SP-initiated flow
     11
     if idpInitiatedLogin != "" {
     12
         // Decode the JWT to extract organization and connection information
     13
         claims, err := scalekitClient.GetIdpInitiatedLoginClaims(r.Context(), idpInitiatedLogin)
     14
         if err != nil {
     15
             http.Error(w, err.Error(), http.StatusInternalServerError)
     16
             return
     17
         }
     18


     19
         options := scalekit.AuthorizationUrlOptions{
     20
             ConnectionId:   claims.ConnectionID,    // Specific SSO connection
     21
             OrganizationId: claims.OrganizationID,  // User's organization
     22
             LoginHint:      claims.LoginHint,       // User's email for context
     23
         }
     24


     25
         // Generate authorization URL and redirect to complete authentication
     26
         authUrl, err := scalekitClient.GetAuthorizationUrl(
     27
             "https://your-app.com/auth/callback",
     28
             options
     29
         )
     8 collapsed lines
     30


     31
         if err != nil {
     32
             http.Error(w, err.Error(), http.StatusInternalServerError)
     33
             return
     34
         }
     35


     36
         http.Redirect(w, r, authUrl.String(), http.StatusFound)
     37
     }
     ```

   * Java

     HandleIdpInitiated.java

     ```java
     1
     // Your initiate login endpoint receives the IdP-initiated login token
     2
     @GetMapping("/login")
     3
     public RedirectView handleInitiateLogin(
     4
         @RequestParam(required = false, name = "idp_initiated_login") String idpInitiatedLoginToken,
     5
         @RequestParam(required = false) String error,
     6
         @RequestParam(required = false, name = "error_description") String errorDescription,
     7
         HttpServletResponse response) throws IOException {
     8


     9
         if (error != null) {
     10
             response.sendError(HttpStatus.BAD_REQUEST.value(), errorDescription);
     11
             return null;
     12
         }
     13


     14
         // When users start login from their IdP portal, convert to SP-initiated flow
     15
         if (idpInitiatedLoginToken != null) {
     16
             // Decode the JWT to extract organization and connection information
     17
             IdpInitiatedLoginClaims claims = scalekit
     18
                 .authentication()
     19
                 .getIdpInitiatedLoginClaims(idpInitiatedLoginToken);
     20


     21
             if (claims == null) {
     22
                 response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid token");
     23
                 return null;
     24
             }
     25


     26
             AuthorizationUrlOptions options = new AuthorizationUrlOptions();
     27
             options.setConnectionId(claims.getConnectionID());      // Specific SSO connection
     28
             options.setOrganizationId(claims.getOrganizationID());  // User's organization
     29
             options.setLoginHint(claims.getLoginHint());            // User's email for context
     30


     31
             // Generate authorization URL and redirect to complete authentication
     32
             String authUrl = scalekit
     33
                 .authentication()
     34
                 .getAuthorizationUrl("https://your-app.com/auth/callback", options)
     35
                 .toString();
     36


     37
             response.sendRedirect(authUrl);
     38
             return null;
     39
         }
     40


     41
         return null;
     42
     }
     ```

   This approach provides enhanced security by converting IdP-initiated requests to standard SP-initiated flows, protecting against SAML assertion theft and replay attacks.

   Learn more: [IdP-initiated SSO implementation guide](/sso/guides/idp-init-sso/)

5. ## Get user details from the callback

   [Section titled “Get user details from the callback”](#get-user-details-from-the-callback)

   After successful authentication, Scalekit redirects to your callback URL with an authorization code. Your application exchanges this code for the user’s profile information and session tokens.

   1. Add a callback endpoint in your application (typically `https://your-app.com/auth/callback`)
   2. [Register](/guides/dashboard/redirects/#allowed-callback-urls) it in your Scalekit dashboard > Authentication > Redirect URLS > Allowed Callback URLs

   In authentication flow, Scalekit redirects to your callback URL with an authorization code. Your application exchanges this code for the user’s profile information.

   * Node.js

     Fetch user profile

     ```javascript
     1
     // Extract authentication parameters from the callback request
     2
     const {
     3
       code,
     4
       error,
     5
       error_description,
     6
       idp_initiated_login,
     7
       connection_id,
     8
       relay_state
     9
     } = req.query;
     10


     11
     if (error) {
     12
       // Handle authentication errors returned from the identity provider
     13
     }
     14


     15
     // Recommended: Process IdP-initiated login flows (when users start from their SSO portal)
     16


     17
     const result = await scalekit.authenticateWithCode(code, redirectUri);
     18
     const userEmail = result.user.email;
     19


     20
     // Create a session for the authenticated user and grant appropriate access permissions
     ```

   * Python

     Fetch user profile

     ```py
     1
     # Extract authentication parameters from the callback request
     2
     code = request.args.get('code')
     3
     error = request.args.get('error')
     4
     error_description = request.args.get('error_description')
     5
     idp_initiated_login = request.args.get('idp_initiated_login')
     6
     connection_id = request.args.get('connection_id')
     7
     relay_state = request.args.get('relay_state')
     8


     9
     if error:
     10
         raise Exception(error_description)
     11


     12
     # Recommended: Process IdP-initiated login flows (when users start from their SSO portal)
     13


     14
     result = scalekit.authenticate_with_code(code, '')
     15


     16
     # Access normalized user profile information
     17
     user_email = result.user.email
     18


     19
     # Create a session for the authenticated user and grant appropriate access permissions
     ```

   * Go

     Fetch user profile

     ```go
     1
     // Extract authentication parameters from the callback request
     2
     code := r.URL.Query().Get("code")
     3
     error := r.URL.Query().Get("error")
     4
     errorDescription := r.URL.Query().Get("error_description")
     5
     idpInitiatedLogin := r.URL.Query().Get("idp_initiated_login")
     6
     connectionID := r.URL.Query().Get("connection_id")
     7
     relayState := r.URL.Query().Get("relay_state")
     8


     9
     if error != "" {
     10
       // Handle authentication errors returned from the identity provider
     11
     }
     12


     13
     // Recommended: Process IdP-initiated login flows (when users start from their SSO portal)
     14


     15
     result, err := scalekitClient.AuthenticateWithCode(r.Context(), code, redirectUrl)
     16


     17
     if err != nil {
     18
       // Handle token exchange or validation errors
     19
     }
     20


     21
     // Access normalized user profile information
     22
     userEmail := result.User.Email
     23


     24
     // Create a session for the authenticated user and grant appropriate access permissions
     ```

   * Java

     Fetch user profile

     ```java
     1
     // Extract authentication parameters from the callback request
     2
     String code = request.getParameter("code");
     3
     String error = request.getParameter("error");
     4
     String errorDescription = request.getParameter("error_description");
     5
     String idpInitiatedLogin = request.getParameter("idp_initiated_login");
     6
     String connectionID = request.getParameter("connection_id");
     7
     String relayState = request.getParameter("relay_state");
     8


     9
     if (error != null && !error.isEmpty()) {
     10
         // Handle authentication errors returned from the identity provider
     11
         return;
     12
     }
     13


     14
     // Recommended: Process IdP-initiated login flows (when users start from their SSO portal)
     15


     16
     try {
     17
         AuthenticationResponse result = scalekit.authentication().authenticateWithCode(code, redirectUrl);
     18
         String userEmail = result.getIdTokenClaims().getEmail();
     19


     20
         // Create a session for the authenticated user and grant appropriate access permissions
     21
     } catch (Exception e) {
     22
         // Handle token exchange or validation errors
     23
     }
     ```

   The `result` object

   * Node.js

     Validate tokens

     ```js
     // Validate and decode the ID token from the authentication result
     const idTokenClaims = await scalekit.validateToken(result.idToken);


     // Validate and decode the access token
     const accessTokenClaims = await scalekit.validateToken(result.accessToken);
     ```

   * Python

     Validate tokens

     ```py
      # Validate and decode the ID token from the authentication result
      id_token_claims = scalekit_client.validate_token(result["id_token"])


      # Validate and decode the access token
      access_token_claims = scalekit_client.validate_token(result["access_token"])
     ```

   * Go

     Validate tokens

     ```go
      // Validate and decode the access token (uses JWKS from the client)
      accessTokenClaims, err := scalekitClient.GetAccessTokenClaims(ctx, result.AccessToken)
      if err != nil {
          // handle error
      }
     ```

   * Java

     Validate tokens

     ```java
      // Validate and decode the ID token
      Map idTokenClaims = scalekitClient.validateToken(result.getIdToken());


      // Validate and decode the access token
      Map accessTokenClaims = scalekitClient.validateToken(result.getAccessToken());
     ```

   - Auth result

     ```js
     {
       user: {
         email: 'john@example.com',
         familyName: 'Doe',
         givenName: 'John',
         username: 'john@example.com',
         id: 'conn_70087756662964366;dcc62570-6a5a-4819-b11b-d33d110c7716'
       },
       idToken: 'eyJhbGciOiJSU..bcLQ',
       accessToken: 'eyJhbGciO..',
       expiresIn: 899
     }
     ```

   - ID token (decoded)

     ```js
     1
     {
     2
       amr: [ 'conn_70087756662964366' ], // SSO connection ID
     3
       at_hash: 'yMGIBg7BkmIGgD6_dZPEGQ',
     4
       aud: [ 'skc_70087756327420046' ],
     5
       azp: 'skc_70087756327420046',
     6
       c_hash: '4x7qsXnlRw6dRC6twnuENw',
     7
       client_id: 'skc_70087756327420046',
     8
       email: 'john@example.com',
     9
       exp: 1758952038,
     10
       family_name: 'Doe',
     11
       given_name: 'John',
     12
       iat: 1758692838,
     13
       iss: '',
     14
       oid: 'org_70087756646187150',
     15
       preferred_username: 'john@example.com',
     16
       sid: 'ses_91646612652163629',
     17
       sub: 'conn_70087756662964366;e964d135-35c7-4a13-a3b4-2579a1cdf4e6'
     18
     }
     ```

   - Access token (decoded)

     ```js
      {
       "iss": "",
       "sub": "conn_70087756662964366;dcc62570-6a5a-4819-b11b-d33d110c7716",
       "aud": [
         "skc_70087756327420046"
       ],
       "exp": 1758693916,
       "iat": 1758693016,
       "nbf": 1758693016,
       "client_id": "skc_70087756327420046",
       "jti": "tkn_91646913048216109"
     }
     ```

6. ## Test your SSO integration

   [Section titled “Test your SSO integration”](#test-your-sso-integration)

   Validate your implementation using the **IdP Simulator** and **Test Organization** included in your development environment. Test all three scenarios before deploying to production.

   Your environment includes a pre-configured test organization (found in **Dashboard > Organizations**) with domains like `@example.com` and `@example.org` for testing.

   Pass one of the following connection selectors in your authorization URL:

   * Email address with `@example.com` or `@example.org` domain
   * Test organization’s connection ID
   * Organization ID

   This opens the SSO login page (IdP Simulator) that simulates your customer’s identity provider login experience.

   ![IdP Simulator](/.netlify/images?url=_astro%2F2.1.BEM1Vo-J.png\&w=2646\&h=1652\&dpl=6ae471fc143c9a4c27550463)

   For detailed testing instructions and scenarios, see our [Complete SSO testing guide](/sso/guides/test-sso/)

7. ## Set up SSO with your existing authentication system

   [Section titled “Set up SSO with your existing authentication system”](#set-up-sso-with-your-existing-authentication-system)

   Many applications already use an authentication provider such as Auth0, Firebase, or AWS Cognito. To enable single sign-on (SSO) using Scalekit, configure Scalekit to work with your current authentication provider.

   ### Auth0

   Integrate Scalekit with Auth0 for enterprise SSO

   [Know more →](/guides/integrations/auth-systems/auth0)

   ### Firebase Auth

   Add enterprise authentication to Firebase projects

   [Know more →](/guides/integrations/auth-systems/firebase)

   ### AWS Cognito

   Configure Scalekit with AWS Cognito user pools

   [Know more →](/guides/integrations/auth-systems/aws-cognito)

8. ## Onboard enterprise customers

   [Section titled “Onboard enterprise customers”](#onboard-enterprise-customers)

   Enable SSO for your enterprise customers by creating an organization in Scalekit and providing them access to the Admin Portal. Your customers configure their identity provider settings themselves through a self-service portal.

   **Create an organization** for your customer in [Dashboard > Organizations](https://app.scalekit.com/organizations), then provide Admin Portal access using one of these methods:

   * Shareable link

     Generate a secure link your customer can use to access the Admin Portal:

     generate-portal-link.js

     ```javascript
     // Generate a one-time Admin Portal link for your customer
     const portalLink = await scalekit.organization.generatePortalLink(
       'org_32656XXXXXX0438'  // Your customer's organization ID
     );


     // Share this link with your customer's IT admin via email or messaging
     // Example: '/magicLink/8930509d-68cf-4e2c-8c6d-94d2b5e2db43
     console.log('Admin Portal URL:', portalLink.location);
     ```

     Send this link to your customer’s IT administrator through email, Slack, or your preferred communication channel. They can configure their SSO connection without any developer involvement.

   * Embedded portal

     Embed the Admin Portal directly in your application using an iframe:

     embed-portal.js

     ```javascript
     // Generate a secure portal link at runtime
     const portalLink = await scalekit.organization.generatePortalLink(orgId);


     // Return the link to your frontend to embed in an iframe
     res.json({ portalUrl: portalLink.location });
     ```

     admin-settings.html

     ```html
     
     
     ```

     Customers configure SSO without leaving your application, maintaining a consistent user experience. Listen for UI events from the embedded portal to respond to configuration changes, such as when SSO is enabled or the session expires. See the [Admin portal UI events reference](/reference/admin-portal/ui-events/) for details on handling these events.

     Learn more: [Embedded Admin Portal guide](/authenticate/sso/admin-portal/#embed-the-admin-portal)

   **Enable domain verification** for seamless user experience. Once your customer verifies their domain (e.g., `@megacorp.org`), users can sign in without selecting their organization. Scalekit automatically routes them to the correct identity provider based on their email domain.

   **Pre-check SSO availability** before redirecting users. This prevents failed redirects when a user’s domain doesn’t have SSO configured:

   * Node.js

     check-sso-availability.js

     ```javascript
     1
     // Extract domain from user's email address
     2
     const domain = email.split('@')[1].toLowerCase();  // e.g., "megacorp.org"
     3


     4
     // Check if domain has an active SSO connection
     5
     const { connections } = await scalekit.connection.listConnectionsByDomain(domain);
     6


     7
     if (connections.length > 0) {
     8
       // Domain has SSO configured - redirect to identity provider
     9
       const authUrl = scalekit.getAuthorizationUrl(redirectUri, {
     10
         domainHint: domain  // Automatically routes to correct IdP
     11
       });
     12
       return res.redirect(authUrl);
     13
     } else {
     14
       // No SSO for this domain - show alternative login methods
     15
       return showPasswordlessLogin();
     16
     }
     ```

   * Python

     check_sso_availability.py

     ```python
     1
     # Extract domain from user's email address
     2
     domain = email.split('@')[1].lower()  # e.g., "megacorp.org"
     3


     4
     # Check if domain has an active SSO connection
     5
     connections = scalekit_client.connection.list_connections_by_domain(domain=domain).connections
     6


     7
     if len(connections) > 0:
     8
         # Domain has SSO configured - redirect to identity provider
     9
         options = AuthorizationUrlOptions()
     10
         options.domain_hint = domain  # Automatically routes to correct IdP
     11


     12
         auth_url = scalekit_client.get_authorization_url(
     13
             redirect_uri=redirect_uri,
     14
             options=options
     15
         )
     16
         return redirect(auth_url)
     17
     else:
     18
         # No SSO for this domain - show alternative login methods
     19
         return show_passwordless_login()
     ```

   * Go

     check_sso_availability.go

     ```go
     1
     // Extract domain from user's email address
     2
     parts := strings.Split(email, "@")
     3
     domain := strings.ToLower(parts[1])  // e.g., "megacorp.org"
     4


     5
     // Check if domain has an active SSO connection
     6
     connections, err := scalekitClient.Connections.ListConnectionsByDomain(domain)
     7
     if err != nil {
     8
         // Handle error
     9
         return err
     10
     }
     11


     12
     if len(connections) > 0 {
     13
         // Domain has SSO configured - redirect to identity provider
     14
         options := scalekit.AuthorizationUrlOptions{
     15
             DomainHint: domain,  // Automatically routes to correct IdP
     16
         }
     17


     18
         authUrl, err := scalekitClient.GetAuthorizationUrl(redirectUri, options)
     19
         if err != nil {
     20
             return err
     21
         }
     22


     23
         c.Redirect(http.StatusFound, authUrl.String())
     24
     } else {
     25
         // No SSO for this domain - show alternative login methods
     26
         return showPasswordlessLogin()
     27
     }
     ```

   * Java

     CheckSsoAvailability.java

     ```java
     1
     // Extract domain from user's email address
     2
     String[] parts = email.split("@");
     3
     String domain = parts[1].toLowerCase();  // e.g., "megacorp.org"
     4


     5
     // Check if domain has an active SSO connection
     6
     List connections = scalekitClient
     7
         .connections()
     8
         .listConnectionsByDomain(domain);
     9


     10
     if (connections.size() > 0) {
     11
         // Domain has SSO configured - redirect to identity provider
     12
         AuthorizationUrlOptions options = new AuthorizationUrlOptions();
     13
         options.setDomainHint(domain);  // Automatically routes to correct IdP
     14


     15
         String authUrl = scalekitClient
     16
             .authentication()
     17
             .getAuthorizationUrl(redirectUri, options)
     18
             .toString();
     19


     20
         return new RedirectView(authUrl);
     21
     } else {
     22
         // No SSO for this domain - show alternative login methods
     23
         return showPasswordlessLogin();
     24
     }
     ```

   This check ensures users only see SSO options when available, improving the login experience and reducing confusion.

---
# DOCUMENT BOUNDARY
---

# Admin portal

> Implement Scalekit's self-serve admin portal to let customers configure SSO and SCIM via a shareable link or embedded iframe

The admin portal provides a self-serve interface for customers to configure single sign-on (SSO) and directory sync (SCIM) connections. Scalekit hosts the portal and provides two integration methods: generate a shareable link through the dashboard or programmatically embed the portal in your application using an iframe.

For the broader customer onboarding workflow, see [Onboard enterprise customers](/sso/guides/onboard-enterprise-customers/).

For self-serve management by signed-in users (members, profiles, domains, SSO, SCIM, session policy), redirect to Hosted UI widgets at `/ui/`. See [Hosted UI widgets](/authenticate/manage-users-orgs/hosted-widgets/) for when to use each option. Use the Admin portal for shareable links or iframe embeds aimed at enterprise IT admins.

## Generate shareable portal link

Recommended

Generate a shareable link through the Scalekit dashboard to give customers access to the admin portal. This method requires no code and is ideal for quick setup.

### Create the portal link

1. Log in to the [Scalekit dashboard](https://app.scalekit.com)
2. Go to **Organizations**
3. Select the target organization
4. Click **Generate link** to create a shareable admin portal link

The generated link follows this format:

Portal link example

```http
https://your-app.scalekit.dev/magicLink/2cbe56de-eec4-41d2-abed-90a5b82286c4_p
```

### Link properties

| Property       | Details                                                                         |
| -------------- | ------------------------------------------------------------------------------- |
| **Expiration** | Links expire after 7 days                                                       |
| **Revocation** | Revoke links anytime from the dashboard                                         |
| **Sharing**    | Share via email, Slack, or any preferred channel                                |
| **Security**   | Anyone with the link can view and update the organization’s connection settings |

Security consideration

Treat portal links as sensitive credentials. Anyone with the link can view and modify the organization’s SSO and SCIM configuration.

## Embed the admin portal

API

Embed the admin portal directly in your application using an iframe. This allows customers to configure SSO and SCIM without leaving your app, creating a seamless experience within your settings or admin interface.

The portal link must be generated programmatically on each page load for security. Each generated link is single-use and expires after 1 minute, though once loaded, the session remains active for up to 6 hours.

* Node.js

  ```bash
  npm install @scalekit-sdk/node
  ```

* Python

  ```sh
  pip install scalekit-sdk-python
  ```

* Go

  ```sh
  go get -u github.com/scalekit-inc/scalekit-sdk-go
  ```

* Java

  ```groovy
  /* Gradle users - add the following to your dependencies in build file */
  implementation "com.scalekit:scalekit-sdk-java:2.1.3"
  ```

  ```xml
  
  
      com.scalekit
      scalekit-sdk-java
      2.1.3
  
  ```

### Generate portal link

Use the Scalekit SDK to generate a unique, embeddable admin portal link for an organization. Call this API endpoint each time you render the page containing the iframe.

* Node.js

  Express.js

  ```javascript
  6 collapsed lines
  import { Scalekit } from '@scalekit-sdk/node';


  const scalekit = new Scalekit(
    process.env.SCALEKIT_ENVIRONMENT_URL,
    process.env.SCALEKIT_CLIENT_ID,
    process.env.SCALEKIT_CLIENT_SECRET,
  );


  async function generatePortalLink(organizationId) {
    const link = await scalekit.organization.generatePortalLink(organizationId);
    return link.location; // Use as iframe src
  }
  ```

* Python

  Flask

  ```python
  6 collapsed lines
  from scalekit import Scalekit
  import os


  scalekit_client = Scalekit(
      environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
      client_id=os.environ.get("SCALEKIT_CLIENT_ID"),
      client_secret=os.environ.get("SCALEKIT_CLIENT_SECRET")
  )


  def generate_portal_link(organization_id):
      link = scalekit_client.organization.generate_portal_link(organization_id)
      return link.location  # Use as iframe src
  ```

* Go

  Gin

  ```go
  10 collapsed lines
  1
  import (
  2
      "context"
  3
      "os"
  4


  5
      "github.com/scalekit/sdk-go"
  6
  )
  7


  8
  scalekitClient := scalekit.New(
  9
      os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
  10
      os.Getenv("SCALEKIT_CLIENT_ID"),
  11
      os.Getenv("SCALEKIT_CLIENT_SECRET"),
  12
  )
  13


  14
  func generatePortalLink(organizationID string) (string, error) {
  15
      ctx := context.Background()
  16
      link, err := scalekitClient.Organization().GeneratePortalLink(ctx, organizationID)
  17
      if err != nil {
  18
          return "", err
  19
      }
  20
      return link.Location, nil  // Use as iframe src
  21
  }
  ```

* Java

  Spring Boot

  ```java
  8 collapsed lines
  1
  import com.scalekit.client.Scalekit;
  2
  import com.scalekit.client.models.Link;
  3
  import com.scalekit.client.models.Feature;
  4
  import java.util.Arrays;
  5


  6
  Scalekit scalekitClient = new Scalekit(
  7
      System.getenv("SCALEKIT_ENVIRONMENT_URL"),
  8
      System.getenv("SCALEKIT_CLIENT_ID"),
  9
      System.getenv("SCALEKIT_CLIENT_SECRET")
  10
  );
  11


  12
  public String generatePortalLink(String organizationId) {
  13
      Link portalLink = scalekitClient.organizations()
  14
          .generatePortalLink(organizationId, Arrays.asList(Feature.sso, Feature.dir_sync, Feature.domain_verification));
  15
      return portalLink.getLocation();  // Use as iframe src
  16
  }
  ```

The API returns a JSON object with the portal link. Use the `location` property as the iframe `src`:

API response

```json
{
  "id": "8930509d-68cf-4e2c-8c6d-94d2b5e2db43",
  "location": "https://random-subdomain.scalekit.dev/magicLink/8930509d-68cf-4e2c-8c6d-94d2b5e2db43",
  "expireTime": "2024-10-03T13:35:50.563013Z"
}
```

Embed portal in iframe

```html

```

Embed the portal in your application’s settings or admin section where customers manage authentication configuration.

### Configuration and session

| Setting               | Requirement                                                                                                                                                                |
| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Redirect URI**      | Register the domain that hosts the iframe (your application’s own domain) as an **Allowed callback URL** at **Authentication** > **Redirects** > **Allowed Callback URLs** |
| **iframe attributes** | Include `allow="clipboard-write"` for copy-paste functionality                                                                                                             |
| **Dimensions**        | Minimum recommended height: 600px                                                                                                                                          |
| **Link expiration**   | Generated links expire after 1 minute if not loaded                                                                                                                        |
| **Session duration**  | Portal session remains active for up to 6 hours once loaded                                                                                                                |
| **Single-use**        | Each generated link can only be used once to initialize a session                                                                                                          |

Register the embedding domain

The embedded portal loads only on domains you have registered. The domain of the page that hosts the iframe must be listed as an allowed callback URL (see the table above). If it is missing, the browser blocks the portal with a Content Security Policy (`frame-ancestors`) error and the iframe stays blank.

Generate fresh links

Generate a new portal link on each page load rather than caching the URL. This ensures security and prevents expired link errors.

## Customize the admin portal

Match the admin portal to your brand identity. Configure branding at **Settings** > **Branding**:

| Option           | Description                                               |
| ---------------- | --------------------------------------------------------- |
| **Logo**         | Upload your company logo (displayed in the portal header) |
| **Accent color** | Set the primary color to match your brand palette         |
| **Favicon**      | Provide a custom favicon for browser tabs                 |

Branding scope

Branding changes apply globally to all portal instances (both shareable links and embedded iframes) in your environment.

For additional customization options including custom domains, see the [Custom domain guide](/guides/custom-domain/).

[SSO integrations](/guides/integrations/sso-integrations/)Administrator guides to set up SSO integrations

[Portal events](/reference/admin-portal/ui-events/)Listen to the browser events emitted from the embedded admin portal

---
# DOCUMENT BOUNDARY
---

# Enterprise SSO code samples

> Code samples demonstrating Single Sign-On implementations with Express.js, PHP Symfony, .NET Core, Firebase, AWS Cognito, and Next.js

### [Add SSO to Express.js apps](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/sso-express-example)

[Implement Scalekit SSO in a Node.js Express application. Includes middleware setup for secure session handling](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/sso-express-example)

### [Add SSO to PHP Symfony apps](https://github.com/scalekit-developers/scalekit-php-symfony-sso)

[Call Scalekit OIDC endpoints from Symfony with no PHP SDK. Try handshake-only login or Modular SSO with a local user store](https://github.com/scalekit-developers/scalekit-php-symfony-sso)

### [Add SSO to .NET Core apps](https://github.com/scalekit-inc/dotnet-example-apps)

[Secure .NET Core applications with Scalekit SSO. Demonstrates authentication pipelines and user claims management](https://github.com/scalekit-inc/dotnet-example-apps)

### [Add SSO to Spring Boot apps](https://github.com/scalekit-developers/scalekit-springboot-example)

[Integrate Scalekit SSO with Spring Security. Shows how to configure security filters and protect Java endpoints](https://github.com/scalekit-developers/scalekit-springboot-example)

### [Add SSO to Python FastAPI](https://github.com/scalekit-developers/scalekit-fastapi-example)

[Add enterprise SSO to FastAPI services using Scalekit. Includes async route protection and user session validation](https://github.com/scalekit-developers/scalekit-fastapi-example)

### [Add SSO to Go applications](https://github.com/scalekit-developers/scalekit-go-example)

[Implement Scalekit SSO in Go. Features idiomatically written middleware for securing HTTP handlers](https://github.com/scalekit-developers/scalekit-go-example)

### [Add SSO to Next.js apps](https://github.com/scalekit-developers/scalekit-nextjs-demo)

[Secure Next.js applications with Scalekit. Covers both App Router and Pages Router authentication patterns](https://github.com/scalekit-developers/scalekit-nextjs-demo)

### Scalekit SSO + Your own auth system

### [Connect Firebase Auth with SSO](https://github.com/scalekit-inc/scalekit-firebase-sso)

[Enable Enterprise SSO for Firebase apps using Scalekit. Learn to link Scalekit identities with Firebase Authentication](https://github.com/scalekit-inc/scalekit-firebase-sso)

### [Connect AWS Cognito with SSO](https://github.com/scalekit-inc/scalekit-cognito-sso)

[Add Enterprise SSO to Cognito user pools via Scalekit. Step-by-step guide to federating identity providers](https://github.com/scalekit-inc/scalekit-cognito-sso)

### [Cognito + Scalekit for Next.js](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/cognito-scalekit)

[Integrate Cognito and Scalekit SSO in Next.js. Uses OIDC protocols to secure your full-stack React application](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/cognito-scalekit)

## Admin portal

### [Embed admin portal](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample)

[Embed the Scalekit Admin Portal into your app via **iframe**. Node.js example for generating secure admin sessions](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample)

---
# DOCUMENT BOUNDARY
---

# Developer resources

> Get up and running with SDKs, APIs, and integration tools

Coming soon

---
# DOCUMENT BOUNDARY
---

# Use Scalekit docs in your AI coding agent

> Use Context7 to give your AI coding agent accurate, up-to-date Scalekit documentation so it can help you integrate faster and with fewer errors.

AI coding agents like Claude Code and Cursor work from training data that can be months out of date. When you ask them to help integrate Scalekit, they may reference old APIs, deprecated patterns, or incorrect parameter names — leading to bugs that are hard to trace.

[Context7](https://context7.com) provides two ways to access live, version-accurate documentation:

* **CLI** — query docs directly from your terminal (recommended for most developers)
* **MCP server** — integrates with AI agents for automatic doc injection

Both methods pull the same up-to-date content. Choose CLI for direct control, or MCP server for seamless AI agent integration.

Scalekit’s full developer documentation is indexed on Context7 at [context7.com/scalekit-inc/developer-docs](https://context7.com/scalekit-inc/developer-docs), covering hundreds of pages and thousands of code snippets across SSO, SCIM, MCP auth, agent auth, and connected accounts.

## Get accurate answers about Scalekit

[Section titled “Get accurate answers about Scalekit”](#get-accurate-answers-about-scalekit)

Context7 retrieves relevant documentation from the indexed Scalekit docs and delivers it to you or your agent. The AI then answers using accurate, current content rather than training data.

Context7 provides three main capabilities:

* `library` — resolve library IDs and discover docs
* `docs` — fetch specific documentation sections
* MCP server tools for AI agent integration

1. #### Set up Context7

   [Section titled “Set up Context7”](#set-up-context7)

   Context7 can be set up via CLI or as an MCP server. Choose your method:

   * CLI

     Install the Context7 CLI to query docs directly from your terminal.

     **One-off installation via npx:**

     ```sh
     npx ctx7 --help
     ```

     **Global installation:**

     ```sh
     npm install -g ctx7
     ctx7 --version
     ```

     Requires Node.js 18 or higher.

     The CLI provides three main capabilities:

     * **Fetch docs** — query specific documentation sections
     * **Manage skills** — generate AI agent skills for auto-invocation
     * **Configure MCP** — set up MCP server integration

   * MCP Server

     Context7 is configured as an MCP server in your coding agent. You can also add it directly from [context7.com](https://context7.com). Choose your tool:

     * Claude Code

       Run one of the following commands in your terminal:

       **Local (stdio):**

       ```sh
       claude mcp add --scope user context7 -- npx -y @upstash/context7-mcp
       ```

       **Remote (HTTP):**

       ```sh
       claude mcp add --scope user --transport http context7 https://mcp.context7.com/mcp
       ```

       To verify the server was added:

       ```sh
       claude mcp list
       ```

     * Cursor

       1. Open **Settings > Cursor Settings > MCP** and click **Add New Global MCP Server**.

          Paste one of the following configs:

          **Remote server:**

          ```json
          {
            "mcpServers": {
              "context7": {
                "url": "https://mcp.context7.com/mcp"
              }
            }
          }
          ```

          **Local server:**

          ```json
          {
            "mcpServers": {
              "context7": {
                "command": "npx",
                "args": ["-y", "@upstash/context7-mcp"]
              }
            }
          }
          ```

       2. Restart Cursor.

     * Claude Desktop

       The easiest way is to install Context7 directly from the Claude Desktop interface:

       1. Open Claude Desktop and go to **Customize > Connectors**.
       2. Search for **Context7** and click **Install**.

       Alternatively, configure it manually via **Settings > Developer > Edit Config** and add to `claude_desktop_config.json`:

       ```json
       {
         "mcpServers": {
           "context7": {
             "command": "npx",
             "args": ["-y", "@upstash/context7-mcp"]
           }
         }
       }
       ```

       Restart Claude Desktop after saving.

     * Windsurf

       1. Open **Settings > Developer > Edit Config** and open `windsurf_config.json`.

       2. Add the following config and save:

          ```json
          {
            "mcpServers": {
              "context7": {
                "command": "npx",
                "args": ["-y", "@upstash/context7-mcp"]
              }
            }
          }
          ```

       3. Restart Windsurf.

   * Claude Code

     Run one of the following commands in your terminal:

     **Local (stdio):**

     ```sh
     claude mcp add --scope user context7 -- npx -y @upstash/context7-mcp
     ```

     **Remote (HTTP):**

     ```sh
     claude mcp add --scope user --transport http context7 https://mcp.context7.com/mcp
     ```

     To verify the server was added:

     ```sh
     claude mcp list
     ```

   * Cursor

     1. Open **Settings > Cursor Settings > MCP** and click **Add New Global MCP Server**.

        Paste one of the following configs:

        **Remote server:**

        ```json
        {
          "mcpServers": {
            "context7": {
              "url": "https://mcp.context7.com/mcp"
            }
          }
        }
        ```

        **Local server:**

        ```json
        {
          "mcpServers": {
            "context7": {
              "command": "npx",
              "args": ["-y", "@upstash/context7-mcp"]
            }
          }
        }
        ```

     2. Restart Cursor.

   * Claude Desktop

     The easiest way is to install Context7 directly from the Claude Desktop interface:

     1. Open Claude Desktop and go to **Customize > Connectors**.
     2. Search for **Context7** and click **Install**.

     Alternatively, configure it manually via **Settings > Developer > Edit Config** and add to `claude_desktop_config.json`:

     ```json
     {
       "mcpServers": {
         "context7": {
           "command": "npx",
           "args": ["-y", "@upstash/context7-mcp"]
         }
       }
     }
     ```

     Restart Claude Desktop after saving.

   * Windsurf

     1. Open **Settings > Developer > Edit Config** and open `windsurf_config.json`.

     2. Add the following config and save:

        ```json
        {
          "mcpServers": {
            "context7": {
              "command": "npx",
              "args": ["-y", "@upstash/context7-mcp"]
            }
          }
        }
        ```

     3. Restart Windsurf.

2. #### Query Scalekit docs

   [Section titled “Query Scalekit docs”](#query-scalekit-docs)

   * Using CLI

     Querying Scalekit docs via CLI is a two-step process.

     **Step 1 — Resolve Scalekit library:**

     ```sh
     ctx7 library scalekit "How to set up SSO"
     ctx7 library scalekit "SCIM user provisioning"
     ctx7 library scalekit "MCP authentication setup"
     ```

     Expected result for library selection:

     | Field             | Description                         |
     | ----------------- | ----------------------------------- |
     | Library ID        | `/scalekit-inc/developer-docs`      |
     | Code Snippets     | High (hundreds of indexed examples) |
     | Source Reputation | High                                |
     | Benchmark Score   | Quality score from 0 to 100         |

     **Step 2 — Fetch Scalekit docs:**

     ```sh
     # SSO queries
     ctx7 docs /scalekit-inc/developer-docs "How to set up SSO with Scalekit"
     ctx7 docs /scalekit-inc/developer-docs "Configure SAML for enterprise SSO"


     # SCIM queries
     ctx7 docs /scalekit-inc/developer-docs "How to provision users with SCIM"
     ctx7 docs /scalekit-inc/developer-docs "Set up SCIM for Active Directory"


     # MCP auth queries
     ctx7 docs /scalekit-inc/developer-docs "Add MCP auth to my server"
     ctx7 docs /scalekit-inc/developer-docs "Configure agent authentication"


     # Connected accounts queries
     ctx7 docs /scalekit-inc/developer-docs "Configure connected accounts for GitHub OAuth"
     ctx7 docs /scalekit-inc/developer-docs "Set up Google OAuth integration"


     # JSON output for scripting
     ctx7 docs /scalekit-inc/developer-docs "SSO setup" --json


     # Pipe to other tools
     ctx7 docs /scalekit-inc/developer-docs "SCIM provisioning" | head -50
     ```

     Note

     Library IDs always start with `/`. Running `ctx7 docs scalekit "SSO"` will fail — always use the full ID: `/scalekit-inc/developer-docs`.

   * Using MCP Server

     Once Context7 is running, add **`use context7`** to any prompt where you want current Scalekit documentation injected automatically.

     **General Scalekit queries:**

     ```txt
     How do I set up SSO with Scalekit? use context7
     ```

     ```txt
     Show me how to provision users with SCIM using Scalekit. use context7
     ```

     **Target Scalekit docs directly** using the library path:

     ```txt
     use library /scalekit-inc/developer-docs for how to add MCP auth to my server
     ```

     **Combine with version or feature specificity:**

     ```txt
     How do I configure connected accounts for GitHub OAuth with Scalekit? use context7
     ```

3. #### Auto-invoke Context7 (optional)

   [Section titled “Auto-invoke Context7 (optional)”](#auto-invoke-context7-optional)

   Configure your coding agent to always use Context7 for library and API questions — no need to add “use context7” manually each time.

   * CLI

     Use `ctx7 setup --cli` to configure Context7 for AI coding agents. This installs a `docs` skill that guides the agent to use `ctx7 library` and `ctx7 docs` commands for Scalekit documentation.

     **Setup commands:**

     ```sh
     # Interactive setup (prompts for agent)
     ctx7 setup --cli


     # Direct setup for specific agents
     ctx7 setup --cli --claude       # Claude Code (~/.claude/skills)
     ctx7 setup --cli --cursor       # Cursor (~/.cursor/skills)
     ctx7 setup --cli --universal    # Universal (~/.config/agents/skills)


     # Project-specific setup (default is global)
     ctx7 setup --cli --project


     # Skip confirmation prompts
     ctx7 setup --cli --yes
     ```

     **What gets installed — CLI + Skills mode:**

     | File                   | Purpose                                                               |
     | ---------------------- | --------------------------------------------------------------------- |
     | Agent skills directory | `docs` skill — guides the agent to use `ctx7 library` and `ctx7 docs` |

     When the `docs` skill is installed, your AI agent will automatically use `ctx7` commands to fetch accurate Scalekit documentation when asked about SSO, SCIM, MCP auth, or other Scalekit features.

   * MCP Server

     Configure your coding agent to always use Context7 for library and API questions — no need to add “use context7” manually each time.

     * Claude Code

       Add the following rule to your project’s `CLAUDE.md` file:

       ```md
       Always use Context7 MCP when I need library or API documentation, code generation, or setup and configuration steps.
       ```

       This applies project-wide. For a global rule, add it to `~/.claude/CLAUDE.md`.

     * Cursor

       Open **Settings > Cursor Settings > Rules** and add:

       ```txt
       Always use Context7 MCP when I need library or API documentation, code generation, or setup and configuration steps.
       ```

   * Claude Code

     Add the following rule to your project’s `CLAUDE.md` file:

     ```md
     Always use Context7 MCP when I need library or API documentation, code generation, or setup and configuration steps.
     ```

     This applies project-wide. For a global rule, add it to `~/.claude/CLAUDE.md`.

   * Cursor

     Open **Settings > Cursor Settings > Rules** and add:

     ```txt
     Always use Context7 MCP when I need library or API documentation, code generation, or setup and configuration steps.
     ```

4. #### Increase rate limits with an API key

   [Section titled “Increase rate limits with an API key”](#increase-rate-limits-with-an-api-key)

   The free tier of Context7 has rate limits. For heavier usage or team environments, get a free API key from [context7.com/dashboard](https://context7.com/dashboard) and add it to your configuration.

   * MCP Server

     * Claude Code

       **Local:**

       ```sh
       claude mcp add --scope user context7 -- npx -y @upstash/context7-mcp --api-key YOUR_API_KEY
       ```

       **Remote:**

       ```sh
       claude mcp add --scope user --header "CONTEXT7_API_KEY: YOUR_API_KEY" --transport http context7 https://mcp.context7.com/mcp
       ```

     * Cursor

       **Remote server with API key:**

       ```json
       {
         "mcpServers": {
           "context7": {
             "url": "https://mcp.context7.com/mcp",
             "headers": {
               "CONTEXT7_API_KEY": "YOUR_API_KEY"
             }
           }
         }
       }
       ```

       **Local server with API key:**

       ```json
       {
         "mcpServers": {
           "context7": {
             "command": "npx",
             "args": ["-y", "@upstash/context7-mcp", "--api-key", "YOUR_API_KEY"]
           }
         }
       }
       ```

     * Claude Desktop / Windsurf

       ```json
       {
         "mcpServers": {
           "context7": {
             "command": "npx",
             "args": ["-y", "@upstash/context7-mcp", "--api-key", "YOUR_API_KEY"]
           }
         }
       }
       ```

   * CLI

     **Local:**

     ```sh
     claude mcp add --scope user context7 -- npx -y @upstash/context7-mcp --api-key YOUR_API_KEY
     ```

     **Remote:**

     ```sh
     claude mcp add --scope user --header "CONTEXT7_API_KEY: YOUR_API_KEY" --transport http context7 https://mcp.context7.com/mcp
     ```

   * Claude Code

     **Remote server with API key:**

     ```json
     {
       "mcpServers": {
         "context7": {
           "url": "https://mcp.context7.com/mcp",
           "headers": {
             "CONTEXT7_API_KEY": "YOUR_API_KEY"
           }
         }
       }
     }
     ```

     **Local server with API key:**

     ```json
     {
       "mcpServers": {
         "context7": {
           "command": "npx",
           "args": ["-y", "@upstash/context7-mcp", "--api-key", "YOUR_API_KEY"]
         }
       }
     }
     ```

   * Cursor

     ```json
     {
       "mcpServers": {
         "context7": {
           "command": "npx",
           "args": ["-y", "@upstash/context7-mcp", "--api-key", "YOUR_API_KEY"]
         }
       }
     }
     ```

   * Claude Desktop / Windsurf

     Set an API key via environment variable for higher rate limits:

     ```sh
     # Set API key for current session
     export CONTEXT7_API_KEY=your_key


     # Add to ~/.bashrc or ~/.zshrc for permanent use
     echo 'export CONTEXT7_API_KEY=your_key' >> ~/.bashrc
     ```

     API keys start with `ctx7sk`. If authentication fails with a 401 error, verify the key format matches your method (HTTP header for MCP, environment variable for CLI).

     Note

     Most CLI commands work without authentication. Login (`ctx7 login`) is only required for skill generation and higher rate limits on documentation commands.

Note

For help with common issues including timeouts, module errors, rate limits, and proxy configuration, see the [Context7 troubleshooting guide](https://context7.com/docs/resources/troubleshooting).

---
# DOCUMENT BOUNDARY
---

# Cursor Integration

> Use Scalekit with Cursor via the local installer while the marketplace listing is under review

Use Scalekit with Cursor by running the local installer, enabling the auth plugin you need, and then prompting Cursor to generate the implementation in your existing codebase.

1. ## Install the authstack plugin (recommended)

   Terminal

   ```bash
   npx @scalekit-inc/cli setup
   ```

   For repeated use, install globally:

   Terminal

   ```bash
   npm install -g @scalekit-inc/cli
   scalekit setup
   ```

   The CLI detects Cursor and installs the authstack plugin directly.

2. ## Reload and select plugins

   Restart Cursor (or run **Developer: Reload Window**), then open **Settings > Cursor Settings > Plugins**.

   Enable the Scalekit plugins you need. The shipped skills are `integrate-agentkit`, `implement-saaskit`, `add-mcp-oauth`, `implement-sso`, and `implement-scim`.

   Done: the plugin list shows the skill you need.

   Alternative for other agents

   For agents that read the shared skills directory, such as Cline, Amp and OpenCode, install the Scalekit skills to `~/.agents/skills`:

   ```bash
   npx @scalekit-inc/cli skills install -y
   ```

   To pick individual skills, run `npx skills add scalekit-inc/authstack`.

3. ## Generate the implementation

   Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt from the feature page (or describe what you need in natural language). The installed Scalekit plugins provide the agent with accurate patterns.

   Review generated code

   Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your application’s security requirements.

4. ## Verify the implementation

   After Cursor finishes generating code, confirm all authentication components are in place:

   * The Scalekit plugin appears in **Settings > Cursor Settings > Plugins**
   * Scalekit client initialized with your API credentials (set up a `.env` file with your Scalekit environment variables)
   * Authorization URL generation and callback handler
   * Session or token integration matching your application’s existing patterns

---
# DOCUMENT BOUNDARY
---

# Scalekit MCP Server

> Learn how to use the Scalekit MCP Server to manage your users, organizations, and applications.

Scalekit Model Context Protocol (MCP) server provides comprehensive tools for managing environments, organizations, users, connections, and workspace operations. Built for developers who want to connect their AI tools to Scalekit context and capabilities based on simple natural language queries.

This MCP server enables AI assistants to interact with Scalekit’s identity and access management platform through a standardized set of tools. It provides secure, OAuth-protected access to manage environments, organizations, users, authentication connections, and more.

* Environment management and configuration
* Organization and user management
* Workspace member administration
* OIDC connection setup and management
* MCP server registration and configuration
* Role and scope management
* Admin portal link generation

## Configuration

[Section titled “Configuration”](#configuration)

Connect the Scalekit MCP server to your AI coding tool. Find your tool below and follow the steps — your client will prompt you to sign in via OAuth on first use.

### Claude Code

[Section titled “Claude Code”](#claude-code)

Run this command in your terminal:

```bash
claude mcp add --transport http scalekit https://mcp.scalekit.com/
```

### Claude Desktop

[Section titled “Claude Desktop”](#claude-desktop)

1. Open Claude Desktop
2. Go to **Settings → Connectors**
3. Click **Add custom connector**
4. Enter `Scalekit` as the name and `https://mcp.scalekit.com` as the URL
5. Click **Connect** to authenticate

### VS Code

[Section titled “VS Code”](#vs-code)

Edit `.vscode/mcp.json` in your project (requires VS Code 1.101 or later):

```json
{
  "servers": {
    "scalekit": {
      "type": "http",
      "url": "https://mcp.scalekit.com/"
    }
  }
}
```

### Cursor

[Section titled “Cursor”](#cursor)

Edit `~/.cursor/mcp.json`, or open **Cursor Settings → MCP → Add New Global MCP Server** and paste the config:

```json
{
  "mcpServers": {
    "scalekit": {
      "url": "https://mcp.scalekit.com/"
    }
  }
}
```

### Windsurf

[Section titled “Windsurf”](#windsurf)

Edit `~/.codeium/windsurf/mcp_config.json`:

```json
{
  "mcpServers": {
    "scalekit": {
      "serverUrl": "https://mcp.scalekit.com/"
    }
  }
}
```

### Gemini CLI

[Section titled “Gemini CLI”](#gemini-cli)

Edit `~/.gemini/settings.json`:

```json
{
  "mcpServers": {
    "scalekit": {
      "httpUrl": "https://mcp.scalekit.com/"
    }
  }
}
```

### Codex

[Section titled “Codex”](#codex)

Run this command in your terminal:

```bash
codex mcp add scalekit --url https://mcp.scalekit.com/
```

### OpenCode

[Section titled “OpenCode”](#opencode)

Edit `opencode.json` in your project root:

```json
{
  "mcp": {
    "scalekit": {
      "type": "remote",
      "url": "https://mcp.scalekit.com/",
      "enabled": true
    }
  }
}
```

### Roo Code

[Section titled “Roo Code”](#roo-code)

Add to your MCP configuration:

```json
{
  "mcpServers": {
    "scalekit": {
      "type": "streamable-http",
      "url": "https://mcp.scalekit.com/"
    }
  }
}
```

### Zed

[Section titled “Zed”](#zed)

Add to your Zed `settings.json`:

```json
{
  "context_servers": {
    "scalekit": {
      "url": "https://mcp.scalekit.com/"
    }
  }
}
```

### Kiro

[Section titled “Kiro”](#kiro)

Edit `~/.kiro/settings/mcp.json`:

```json
{
  "mcpServers": {
    "scalekit": {
      "url": "https://mcp.scalekit.com/"
    }
  }
}
```

### Warp

[Section titled “Warp”](#warp)

Go to **Settings → MCP Servers → Add MCP Server** and enter `https://mcp.scalekit.com/`, or add to your Warp MCP config:

```json
{
  "scalekit": {
    "serverUrl": "https://mcp.scalekit.com/"
  }
}
```

### v0 by Vercel

[Section titled “v0 by Vercel”](#v0-by-vercel)

Go to **Prompt Tools → Add MCP** and enter `https://mcp.scalekit.com/`.

Tip

Building your own MCP server? Add OAuth-protected access using [Auth for MCP Servers](/authenticate/mcp/quickstart/).

## GitHub

[Section titled “GitHub”](#github)

The source code for the Scalekit MCP server is available on [GitHub](https://github.com/scalekit-inc/mcp), including a full list of available tools and their descriptions.

* Open an issue if you find a bug or have a question.
* Submit a PR or open an issue to suggest new tools.

---
# DOCUMENT BOUNDARY
---

# OpenAPI Specifications

> Access Scalekit OpenAPI specifications for API documentation and client generation

### [OpenAPI Spec](https://github.com/scalekit-inc/developer-docs/blob/main/public/api/scalekit.scalar.yaml)

[YAMLv3.1.1](https://github.com/scalekit-inc/developer-docs/blob/main/public/api/scalekit.scalar.yaml)

[Download the OpenAPI specification](https://github.com/scalekit-inc/developer-docs/blob/main/public/api/scalekit.scalar.yaml)

### [OpenAPI Spec](https://github.com/scalekit-inc/developer-docs/blob/main/public/api/scalekit.scalar.json)

[JSONv3.1.1](https://github.com/scalekit-inc/developer-docs/blob/main/public/api/scalekit.scalar.json)

[Download the OpenAPI specification](https://github.com/scalekit-inc/developer-docs/blob/main/public/api/scalekit.scalar.json)

---
# DOCUMENT BOUNDARY
---

# Build with AI

> Use AI coding agents to implement Scalekit authentication in minutes

Use the Scalekit CLI to install the authstack plugin for your coding agents. One command detects your tools and sets everything up.

* Recommended

  Terminal

  ```bash
  npx @scalekit-inc/cli setup
  ```

* Global install (for repeated use)

  Terminal

  ```bash
  npm install -g @scalekit-inc/cli
  scalekit setup
  ```

The CLI installs the authstack plugin for Claude Code, Cursor, GitHub Copilot, and Codex. Then load a shipped skill: `integrate-agentkit`, `implement-saaskit`, `add-mcp-oauth`, `implement-sso`, or `implement-scim`.

Done: the command exits 0 and the skill you need is available.

The CLI sends anonymous usage events about whether plugin and skill installs succeed. It never sends project paths, file contents or credentials. To turn them off, set `SCALEKIT_TELEMETRY=0` or `DO_NOT_TRACK=1`. [The CLI’s README](https://github.com/scalekit-inc/cli#telemetry) lists each event and field.

### [Full Stack Auth](/dev-kit/build-with-ai/full-stack-auth/)

### [AgentKit](/agentkit/coding-agents/)

### [MCP Auth](/dev-kit/build-with-ai/mcp-auth/)

### [Modular SSO](/dev-kit/build-with-ai/sso/)

### [Modular SCIM](/dev-kit/build-with-ai/scim/)

## Documentation for AI agents

[Section titled “Documentation for AI agents”](#documentation-for-ai-agents)

Load these files to give your agent full context about Scalekit APIs and integration patterns:

| File                                                       | Contents                                             | When to use                       |
| ---------------------------------------------------------- | ---------------------------------------------------- | --------------------------------- |
| [`/llms.txt`](/llms.txt)                                   | Structured index with routing hints per product area | Most queries — smaller context    |
| [`/llms-full.txt`](/llms-full.txt)                         | Complete documentation for all pages                 | When exhaustive context is needed |
| [`sitemap-0.xml`](https://docs.scalekit.com/sitemap-0.xml) | Full URL list of all documentation pages             | Crawling or indexing all pages    |

---
# DOCUMENT BOUNDARY
---

# Coding agents: Add full-stack auth to your app

> Let your coding agents guide you into implementing Scalekit full-stack authentication in minutes

Install the authstack plugin, then tell your coding agent to add SaaSKit auth to your app.

* Claude Code

  1. ## Install the authstack plugin

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     Select Claude Code when prompted. The CLI installs the authstack plugin for you. Then ask the agent to run the shipped skill `implement-saaskit`.

     Done: the command exits 0 and `implement-saaskit` is available.

     Tool-native alternative (if not using the CLI)

     ```bash
     claude plugin marketplace add scalekit-inc/authstack
     claude plugin install agentkit@authstack
     claude plugin install saaskit@authstack
     ```

     Or run `/plugins` inside Claude to enable via the wizard.

  2. ## Generate authentication implementation

     Copy the following prompt into your coding agent:

     Authentication implementation prompt

     ```md
     Add SaaSKit auth to my app. I need login, sessions, and logout.
     ```

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  3. ## Verify the implementation

     After the coding agent completes, verify that all authentication components are properly configured:

     Check generated files:

     * Scalekit client initialization with environment credentials (you may need to set up a `.env` file with your Scalekit API credentials)
     * Login route that redirects to Scalekit’s authorization endpoint
     * OAuth callback route that exchanges the code for tokens
     * Secure session storage with proper cookie attributes
     * Logout endpoint that clears the session

     The login flow should redirect users to Scalekit’s authorization page, where they authenticate. Your application should then exchange the returned authorization code for tokens, store the session, and redirect the user to the protected area of your app.

* Codex

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     Choose Codex when prompted. The CLI installs the authstack plugin. Then load the shipped skill `implement-saaskit`.

     Done: the command exits 0 and `implement-saaskit` is available.

  2. ## Enable the SaaSKit plugin

     Restart Codex and enable the authstack plugin in the Plugin Directory.

  3. ## Generate the authentication implementation

     Copy the following prompt into Codex:

     Authentication implementation prompt

     ```md
     Add SaaSKit auth to my app. I need login, sessions, and logout.
     ```

     Codex loads the SaaSKit plugin from the authstack plugin, analyzes your existing application structure, generates Scalekit client initialization with environment credentials, creates the login redirect handler, implements the OAuth callback to exchange the authorization code for tokens, and adds secure session storage with a logout endpoint.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  4. ## Verify the implementation

     After Codex completes, verify that all authentication components are properly configured:

     Check generated files:

     * Scalekit client initialization with environment credentials. You may need to set up a `.env` file with your Scalekit API credentials.
     * Login route that redirects to Scalekit’s authorization endpoint
     * OAuth callback route that exchanges the code for tokens
     * Secure session storage with proper cookie attributes
     * Logout endpoint that clears session state

     The login flow should redirect users to Scalekit’s authorization page, where they authenticate. Your application should then exchange the returned authorization code for tokens, store the session, and redirect the user to the protected area of your app.

  When you connect, users authenticate through the OAuth 2.0 flow you configured. Verify that protected routes require a valid session and that the logout endpoint properly clears session state.

* GitHub Copilot CLI

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI installs the authstack plugin for GitHub Copilot. Then ask the agent to run the shipped skill `implement-saaskit`.

     Done: the command exits 0 and `implement-saaskit` is available.

     Tool-native alternative

     ```bash
     copilot plugin marketplace add scalekit-inc/authstack
     copilot plugin install agentkit@authstack
     copilot plugin install saaskit@authstack
     ```

     Use `copilot plugin list` to verify.

  2. ## Generate authentication implementation

     Copy the following command into your terminal:

     Terminal

     ```bash
     copilot "Add SaaSKit auth to my app. I need login, sessions, and logout."
     ```

     GitHub Copilot uses the SaaSKit plugin to analyze your existing application structure, generate Scalekit client initialization code, create the login redirect handler, implement the OAuth callback for token exchange, add secure session storage, and provide a logout endpoint.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  3. ## Verify the implementation

     After GitHub Copilot completes, verify that all authentication components are properly configured:

     Check generated files:

     * Scalekit client initialization with environment credentials (you may need to set up a `.env` file with your Scalekit API credentials)
     * Login route that redirects to Scalekit’s authorization endpoint
     * OAuth callback route that exchanges the code for tokens
     * Secure session storage with proper cookie attributes
     * Logout endpoint that clears the session

     The login flow should redirect users to Scalekit’s authorization page, where they authenticate. Your application should then exchange the returned authorization code for tokens, store the session, and redirect the user to the protected area of your app.

  When you connect, users authenticate through the OAuth 2.0 flow you configured. Verify that protected routes require a valid session and that the logout endpoint properly clears session state.

* Cursor

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI detects Cursor and installs the authstack plugin directly.

  2. ## Reload and select plugins

     Restart Cursor (or run **Developer: Reload Window**), then open **Settings > Cursor Settings > Plugins**.

     Enable the Scalekit plugins you need. The shipped skills are `integrate-agentkit`, `implement-saaskit`, `add-mcp-oauth`, `implement-sso`, and `implement-scim`.

     Done: the plugin list shows the skill you need.

     Alternative for other agents

     For agents that read the shared skills directory, such as Cline, Amp and OpenCode, install the Scalekit skills to `~/.agents/skills`:

     ```bash
     npx @scalekit-inc/cli skills install -y
     ```

     To pick individual skills, run `npx skills add scalekit-inc/authstack`.

  3. ## Generate the implementation

     Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt from the feature page (or describe what you need in natural language). The installed Scalekit plugins provide the agent with accurate patterns.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your application’s security requirements.

  4. ## Verify the implementation

     After Cursor finishes generating code, confirm all authentication components are in place:

     * The Scalekit plugin appears in **Settings > Cursor Settings > Plugins**
     * Scalekit client initialized with your API credentials (set up a `.env` file with your Scalekit environment variables)
     * Authorization URL generation and callback handler
     * Session or token integration matching your application’s existing patterns

* Other agents

  Scalekit skills work with any coding agent that supports the Agent Skills standard, such as Cline, Amp and OpenCode. Install them with the Scalekit CLI (Node.js 20 or later):

  Terminal

  ```bash
  npx @scalekit-inc/cli skills install -y
  ```

  The command installs the Scalekit skills to `~/.agents/skills`, the shared skills directory these agents read. The `-y` flag skips the confirmation prompt, so the command also works when a coding agent runs it.

  You can also install the skills with the Skills CLI. Use it when the Scalekit CLI skips the install because installed Scalekit plugins already include the skills:

  1. ## Install interactively

     Run the command with no flags to be guided through the available skills:

     Terminal

     ```bash
     npx skills add scalekit-inc/authstack
     ```

  2. ## Browse and install a specific skill

     Install the skill for your auth type (for example, MCP OAuth):

     Terminal

     ```bash
     # List all available skills
     npx skills add scalekit-inc/authstack --list


     # Install a specific skill
     npx skills add scalekit-inc/authstack --skill add-mcp-oauth
     ```

  3. ## Invoke the skill Varies by agent

     Each coding agent has its own behavior for invoking skills. In OpenCode, skills are invoked **automatically by the agent based on natural language** — no slash commands required. The agent has a list of available skills and their `description` fields in context. It reads your intent, matches it against those descriptions, and autonomously calls the skill tool to load the relevant `SKILL.md`. A clear, specific `description` in skill frontmatter is what the agent uses to decide which skill to invoke.

     **Flow in practice:**

     * You write a natural language message to the agent
     * The agent checks its context — it already sees `` with names and descriptions
     * If your request matches a skill’s purpose, the agent calls `skill("")` internally
     * The full `SKILL.md` content loads into context and the agent follows those instructions

     If your agent does not automatically pick up skills, you can run a command to load a skill and manually select Scalekit’s skills to load into context. Refer to your favorite coding agent’s documentation for how to invoke skills once they are installed.

  4. ## Install all skills globally

     To add all Scalekit authentication skills to your agents:

     Terminal

     ```bash
     npx skills add scalekit-inc/authstack --all --global
     ```

     This installs all AgentKit and SaaSKit skills.

---
# DOCUMENT BOUNDARY
---

# MCP quickstart with AI coding agents

> Use AI coding agents to add OAuth 2.1 authentication to your MCP servers in minutes

Use AI coding agents like Claude Code, GitHub Copilot CLI, Cursor, and OpenCode to add Scalekit’s OAuth 2.1 authentication to your MCP servers. Configure the agents to analyze your codebase, apply consistent authentication patterns, and generate production-ready code that integrates OAuth 2.1 end-to-end. This reduces implementation time from hours to minutes and follows security best practices.

**Prerequisites**

* A [Scalekit account](https://app.scalekit.com) with MCP server management access
* Basic familiarity with OAuth 2.1 and MCP server architecture
* Terminal access for installing coding agent tools

- Claude Code

  1. ## Install the authstack plugin

     Not yet on Claude Code? Follow the [official quickstart guide](https://code.claude.com/docs/en/quickstart) to install it.

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI installs the authstack plugin for you. Then ask the agent to run the shipped skill `add-mcp-oauth`.

     Done: the command exits 0 and `add-mcp-oauth` is available.

     Alternative: Enable authentication plugins via plugin wizard

     Run the plugin wizard to browse and enable available plugins:

     Claude REPL

     ```bash
     /plugins
     ```

     Navigate through the visual interface to enable the MCP authentication plugin:

     ![Enabling Scalekit MCP authentication plugin in Claude Code](/.netlify/images?url=_astro%2F2.CF1lI92P.gif\&w=1276\&h=720\&dpl=6ae471fc143c9a4c27550463)

     Auto-update recommendations

     Enable auto-updates for authentication plugins to receive security patches and improvements. Scalekit regularly updates plugins based on community feedback and security best practices.

  2. ## Generate authentication implementation

     Use a structured prompt to direct the coding agent. A well-formed prompt ensures the agent generates complete, production-ready authentication code that includes all required security components.

     Copy the following prompt into your coding agent:

     Authentication implementation prompt

     ```md
     Add OAuth 2.1 authentication to my MCP server using Scalekit. Initialize ScalekitClient with environment credentials, implement /.well-known/ metadata endpoint for discovery, and add authentication middleware that validates JWT bearer tokens on all MCP requests. Code only.
     ```

     When you submit this prompt, Claude Code loads the MCP authentication skill from the marketplace -> analyzes your existing MCP server structure -> generates authentication middleware with token validation -> creates the OAuth discovery endpoint -> configures environment variable handling.

     ![Claude Code activating MCP authentication skill](/.netlify/images?url=_astro%2Fskill-activation.CGYr0u-q.png\&w=1121\&h=858\&dpl=6ae471fc143c9a4c27550463)

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  3. ## Verify and test the implementation

     After the coding agent completes, verify that all authentication components are properly configured:

     Check generated files:

     * Authentication middleware with JWT validation
     * Environment variable configuration (`.env.example`)
     * OAuth discovery endpoint (`/.well-known/oauth-authorization-server`)
     * Error handling for invalid or expired tokens

     **Test the authentication flow:**

     * Claude Code

       Claude REPL

       ```md
       Now that your MCP server has authentication integrated, let's verify it's working correctly by testing the flow step by step. First, start your MCP server using npm start (Node.js) or python server.py (Python) and confirm it's running without errors. Next, test the OAuth discovery endpoint by running curl http://localhost:3000/.well-known/oauth-authorization-server to verify your server exposes the correct authorization configuration. Then, verify authentication is enforced by calling curl http://localhost:3000/mcp without credentials—this should return a 401 Unauthorized response, confirming protected endpoints are secured. Finally, test with a valid token by running curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp (replace YOUR_TOKEN with an actual access token from your auth provider) to confirm authenticated requests succeed and return the expected response—if all these steps work as described, your authentication implementation is functioning correctly.
       ```

     * Node.js

       Terminal

       ```bash
       # Start your MCP server
       npm start


       # Test discovery endpoint
       curl http://localhost:3000/.well-known/oauth-authorization-server


       # Test protected endpoint (should return 401)
       curl http://localhost:3000/mcp


       # Test with valid token
       curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp
       ```

     * Python

       Terminal

       ```bash
       # Start your MCP server
       python server.py


       # Test discovery endpoint
       curl http://localhost:3000/.well-known/oauth-authorization-server


       # Test protected endpoint (should return 401)
       curl http://localhost:3000/mcp


       # Test with valid token
       curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp
       ```

     The discovery endpoint should return OAuth configuration metadata. Protected endpoints should reject requests without valid tokens and accept requests with properly scoped access tokens.

- Codex

  Claude REPL

  ```md
  Now that your MCP server has authentication integrated, let's verify it's working correctly by testing the flow step by step. First, start your MCP server using npm start (Node.js) or python server.py (Python) and confirm it's running without errors. Next, test the OAuth discovery endpoint by running curl http://localhost:3000/.well-known/oauth-authorization-server to verify your server exposes the correct authorization configuration. Then, verify authentication is enforced by calling curl http://localhost:3000/mcp without credentials—this should return a 401 Unauthorized response, confirming protected endpoints are secured. Finally, test with a valid token by running curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp (replace YOUR_TOKEN with an actual access token from your auth provider) to confirm authenticated requests succeed and return the expected response—if all these steps work as described, your authentication implementation is functioning correctly.
  ```

- GitHub Copilot CLI

  Terminal

  ```bash
  # Start your MCP server
  npm start


  # Test discovery endpoint
  curl http://localhost:3000/.well-known/oauth-authorization-server


  # Test protected endpoint (should return 401)
  curl http://localhost:3000/mcp


  # Test with valid token
  curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp
  ```

- Cursor

  Terminal

  ```bash
  # Start your MCP server
  python server.py


  # Test discovery endpoint
  curl http://localhost:3000/.well-known/oauth-authorization-server


  # Test protected endpoint (should return 401)
  curl http://localhost:3000/mcp


  # Test with valid token
  curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp
  ```

- Other agents

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     Choose Codex when prompted. The CLI installs the authstack plugin.

     Done: the command exits 0.

  2. ## Enable the MCP OAuth skill

     Restart Codex, then open the Plugin Directory and enable the authstack plugin.

     Load the shipped skill `add-mcp-oauth`. That skill includes the workflows, framework-specific guidance, and references Codex uses to generate OAuth 2.1 protection for remote MCP servers.

     Done: `add-mcp-oauth` is available to Codex.

  3. ## Generate the authentication implementation

     Use a structured prompt to direct Codex. A well-formed prompt helps Codex generate complete, production-ready authentication code that includes all required security components.

     Copy the following prompt into Codex:

     Authentication implementation prompt

     ```md
     Add OAuth 2.1 authentication to my MCP server using Scalekit. Initialize ScalekitClient with environment credentials, implement /.well-known/ metadata endpoint for discovery, and add authentication middleware that validates JWT bearer tokens on all MCP requests. Code only.
     ```

     When you submit this prompt, Codex loads the MCP Auth plugin from the authstack plugin, analyzes your existing MCP server structure, generates authentication middleware with token validation, creates the OAuth discovery endpoint, and configures environment variable handling.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  4. ## Verify and test the implementation

     After Codex completes, verify that all authentication components are properly configured:

     Check generated files:

     * Authentication middleware with JWT validation
     * Environment variable configuration (`.env.example`)
     * OAuth discovery endpoint (`/.well-known/oauth-authorization-server`)
     * Error handling for invalid or expired tokens

     Test the authentication flow:

     Terminal

     ```bash
     # Start your MCP server
     npm start


     # Test discovery endpoint
     curl http://localhost:3000/.well-known/oauth-authorization-server


     # Test protected endpoint (should return 401)
     curl http://localhost:3000/mcp


     # Test with valid token
     curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp
     ```

     The discovery endpoint should return OAuth configuration metadata. Protected endpoints should reject requests without valid tokens and accept requests with properly scoped access tokens.

- Claude Code

  1. ## Install the authstack plugin (recommended)

     Need to install GitHub Copilot CLI? See the [getting started guide](https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-getting-started) — an active GitHub Copilot subscription is required.

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI installs the authstack plugin for GitHub Copilot. Then ask the agent to run the shipped skill `add-mcp-oauth`.

     Done: the command exits 0 and `add-mcp-oauth` is available.

     Tool-native alternative

     ```bash
     copilot plugin marketplace add scalekit-inc/authstack
     copilot plugin install agentkit@authstack
     copilot plugin install saaskit@authstack
     ```

     Verify the plugin is installed

     Confirm the plugin installed successfully:

     Terminal

     ```bash
     copilot plugin list
     ```

  2. ## Generate authentication implementation

     Use a structured prompt to direct GitHub Copilot. A well-formed prompt ensures the agent generates complete, production-ready authentication code that includes all required security components.

     Copy the following command into your terminal:

     Terminal

     ```bash
     copilot "Add OAuth 2.1 authentication to my MCP server using Scalekit. Initialize ScalekitClient with environment credentials, implement /.well-known/ metadata endpoint for discovery, and add authentication middleware that validates JWT bearer tokens on all MCP requests. Code only."
     ```

     GitHub Copilot uses the MCP Auth plugin to analyze your existing MCP server structure, generate authentication middleware with token validation, create the OAuth discovery endpoint, and configure environment variable handling.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  3. ## Verify the implementation

     After GitHub Copilot completes, verify that all authentication components are properly configured:

     Check generated files:

     * Authentication middleware with JWT validation
     * Environment variable configuration (`.env.example`)
     * OAuth discovery endpoint (`/.well-known/oauth-authorization-server`)
     * Error handling for invalid or expired tokens

     Test the authentication flow:

     Terminal

     ```bash
     # Start your MCP server
     npm start


     # Test discovery endpoint
     curl http://localhost:3000/.well-known/oauth-authorization-server


     # Test protected endpoint (should return 401)
     curl http://localhost:3000/mcp


     # Test with valid token
     curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/mcp
     ```

     The discovery endpoint should return OAuth configuration metadata. Protected endpoints should reject requests without valid tokens and accept requests with properly scoped access tokens.

- Node.js

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI detects Cursor and installs the authstack plugin directly.

  2. ## Reload and select plugins

     Restart Cursor (or run **Developer: Reload Window**), then open **Settings > Cursor Settings > Plugins**.

     Enable the Scalekit plugins you need. The shipped skills are `integrate-agentkit`, `implement-saaskit`, `add-mcp-oauth`, `implement-sso`, and `implement-scim`.

     Done: the plugin list shows the skill you need.

     Alternative for other agents

     For agents that read the shared skills directory, such as Cline, Amp and OpenCode, install the Scalekit skills to `~/.agents/skills`:

     ```bash
     npx @scalekit-inc/cli skills install -y
     ```

     To pick individual skills, run `npx skills add scalekit-inc/authstack`.

  3. ## Generate the implementation

     Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt from the feature page (or describe what you need in natural language). The installed Scalekit plugins provide the agent with accurate patterns.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your application’s security requirements.

  4. ## Verify the implementation

     After Cursor finishes generating code, confirm all authentication components are in place:

     * The Scalekit plugin appears in **Settings > Cursor Settings > Plugins**
     * Scalekit client initialized with your API credentials (set up a `.env` file with your Scalekit environment variables)
     * Authorization URL generation and callback handler
     * Session or token integration matching your application’s existing patterns

- Python

  Scalekit skills work with any coding agent that supports the Agent Skills standard, such as Cline, Amp and OpenCode. Install them with the Scalekit CLI (Node.js 20 or later):

  Terminal

  ```bash
  npx @scalekit-inc/cli skills install -y
  ```

  The command installs the Scalekit skills to `~/.agents/skills`, the shared skills directory these agents read. The `-y` flag skips the confirmation prompt, so the command also works when a coding agent runs it.

  You can also install the skills with the Skills CLI. Use it when the Scalekit CLI skips the install because installed Scalekit plugins already include the skills:

  1. ## Install interactively

     Run the command with no flags to be guided through the available skills:

     Terminal

     ```bash
     npx skills add scalekit-inc/authstack
     ```

  2. ## Browse and install a specific skill

     Install the skill for your auth type (for example, MCP OAuth):

     Terminal

     ```bash
     # List all available skills
     npx skills add scalekit-inc/authstack --list


     # Install a specific skill
     npx skills add scalekit-inc/authstack --skill add-mcp-oauth
     ```

  3. ## Invoke the skill Varies by agent

     Each coding agent has its own behavior for invoking skills. In OpenCode, skills are invoked **automatically by the agent based on natural language** — no slash commands required. The agent has a list of available skills and their `description` fields in context. It reads your intent, matches it against those descriptions, and autonomously calls the skill tool to load the relevant `SKILL.md`. A clear, specific `description` in skill frontmatter is what the agent uses to decide which skill to invoke.

     **Flow in practice:**

     * You write a natural language message to the agent
     * The agent checks its context — it already sees `` with names and descriptions
     * If your request matches a skill’s purpose, the agent calls `skill("")` internally
     * The full `SKILL.md` content loads into context and the agent follows those instructions

     If your agent does not automatically pick up skills, you can run a command to load a skill and manually select Scalekit’s skills to load into context. Refer to your favorite coding agent’s documentation for how to invoke skills once they are installed.

  4. ## Install all skills globally

     To add all Scalekit authentication skills to your agents:

     Terminal

     ```bash
     npx skills add scalekit-inc/authstack --all --global
     ```

     This installs all AgentKit and SaaSKit skills.

## Next steps

[Section titled “Next steps”](#next-steps)

Your MCP server now has OAuth 2.1 authentication integrated. Test the implementation with your MCP host to verify the authentication flow works correctly.

### Test with MCP hosts

[Section titled “Test with MCP hosts”](#test-with-mcp-hosts)

Connect your authenticated MCP server to any MCP-compatible host:

* **Claude Desktop or Claude Code**: Configure the MCP server connection in settings
* **Cursor**: Add the MCP server to your workspace configuration
* **Windsurf**: Register the server in your MCP settings
* **Other MCP hosts**: Follow your host’s documentation for connecting authenticated MCP servers

When you connect, the host authenticates using the OAuth 2.1 flow you configured. Verify that protected MCP resources require valid access tokens and that the discovery endpoint provides correct OAuth metadata.

---
# DOCUMENT BOUNDARY
---

# Coding agents: Add SCIM directory sync to your app

> Let your coding agents guide you into adding Scalekit SCIM provisioning to your application in minutes

Use AI coding agents like Claude Code, GitHub Copilot CLI, Cursor, and OpenCode to add Scalekit’s Modular SCIM directory sync to your applications. Configure the agents to analyze your codebase, apply SCIM patterns, and generate production-ready code for user provisioning, deprovisioning, and lifecycle management. This follows security best practices and reduces implementation time from hours to minutes.

* Claude Code

  1. ## Install the authstack plugin

     Not yet on Claude Code? Follow the [official quickstart guide](https://code.claude.com/docs/en/quickstart) to install it.

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI installs the authstack plugin for you. Then ask the agent to run the shipped skill `implement-scim`.

     Done: the command exits 0 and `implement-scim` is available.

     Alternative: Enable SCIM plugins via plugin wizard

     Run the plugin wizard to browse and enable available plugins:

     Claude REPL

     ```bash
     /plugins
     ```

     Navigate through the visual interface to enable the Modular SCIM plugin.

     Auto-update recommendations

     Enable auto-updates for SCIM plugins to receive security patches and improvements. Scalekit regularly updates plugins based on community feedback and security best practices.

  2. ## Generate SCIM implementation

     Use a structured prompt to direct the coding agent. A well-formed prompt ensures the agent generates complete, production-ready SCIM code that includes all required security components.

     Copy the following prompt into your coding agent:

     SCIM implementation prompt

     ```md
     Guide the coding agent to add Scalekit SCIM directory sync to my app — set up the webhook endpoint to receive SCIM events, validate the webhook signature, and handle user provisioning and deprovisioning events to create, update, and delete users in my database. Code only.
     ```

     When you submit this prompt, Claude Code loads the Modular SCIM skill from the marketplace -> analyzes your existing application structure -> generates a webhook endpoint to receive SCIM events from Scalekit -> implements webhook signature validation to prevent unauthorized requests -> creates handlers for user provisioning events (create and update) -> adds deprovisioning logic to delete or deactivate users in your database.

     Review generated code

     Always review AI-generated SCIM code before deployment. Verify that webhook signature validation, event handling logic, and database operations match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  3. ## Verify the implementation

     After the coding agent completes, verify that all SCIM components are properly configured:

     Check generated files:

     * Webhook endpoint that receives SCIM events from Scalekit (you may need to set up a `.env` file with your Scalekit webhook secret)
     * Webhook signature validation to authenticate incoming requests
     * User provisioning handler that creates or updates users in your database
     * Deprovisioning handler that deletes or deactivates users when they are removed from the identity provider

     The SCIM flow should receive webhook events from Scalekit when users are added, updated, or removed in the connected identity provider. Your application should validate each event’s signature, then apply the corresponding change to your user database.

  When directory sync is active, user lifecycle changes in the identity provider propagate automatically to your application. Verify that provisioning events correctly create or update users, and that deprovisioning events properly remove or deactivate accounts.

* Codex

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     Choose Codex when prompted. The CLI installs the authstack plugin.

     Done: the command exits 0.

  2. ## Enable the SCIM skill

     Restart Codex, then open the Plugin Directory and enable the authstack plugin.

     Load the shipped skill `implement-scim`. That skill includes the workflows, references, and prompts Codex uses to generate SCIM provisioning and deprovisioning code for your application.

     Done: `implement-scim` is available to Codex.

  3. ## Generate the SCIM implementation

     Use a structured prompt to direct Codex. A well-formed prompt helps Codex generate complete, production-ready SCIM code that includes all required security components.

     Copy the following prompt into Codex:

     SCIM implementation prompt

     ```md
     Guide the coding agent to add Scalekit SCIM directory sync to my app — set up the webhook endpoint to receive SCIM events, validate the webhook signature, and handle user provisioning and deprovisioning events to create, update, and delete users in my database. Code only.
     ```

     When you submit this prompt, Codex loads the Modular SCIM plugin from the authstack plugin, analyzes your existing application structure, generates a webhook endpoint to receive SCIM events from Scalekit, implements webhook signature validation to prevent unauthorized requests, creates handlers for user provisioning events, and adds deprovisioning logic to delete or deactivate users in your database.

     Review generated code

     Always review AI-generated SCIM code before deployment. Verify that webhook signature validation, event handling logic, and database operations match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  4. ## Verify the implementation

     After Codex completes, verify that all SCIM components are properly configured:

     Check generated files:

     * Webhook endpoint that receives SCIM events from Scalekit. You may need to set up a `.env` file with your Scalekit webhook secret.
     * Webhook signature validation to authenticate incoming requests
     * User provisioning handler that creates or updates users in your database
     * Deprovisioning handler that deletes or deactivates users when they are removed from the identity provider

     The SCIM flow should receive webhook events from Scalekit when users are added, updated, or removed in the connected identity provider. Your application should validate each event’s signature, then apply the corresponding change to your user database.

  When directory sync is active, user lifecycle changes in the identity provider propagate automatically to your application. Verify that provisioning events correctly create or update users, and that deprovisioning events properly remove or deactivate accounts.

* GitHub Copilot CLI

  1. ## Install the authstack plugin (recommended)

     Need to install GitHub Copilot CLI? See the [getting started guide](https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-getting-started) — an active GitHub Copilot subscription is required.

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI installs the authstack plugin for GitHub Copilot. Then ask the agent to run the shipped skill `implement-scim`.

     Done: the command exits 0 and `implement-scim` is available.

     Tool-native alternative

     ```bash
     copilot plugin marketplace add scalekit-inc/authstack
     copilot plugin install agentkit@authstack
     copilot plugin install saaskit@authstack
     ```

     Verify the plugin is installed

     Confirm the plugin installed successfully:

     Terminal

     ```bash
     copilot plugin list
     ```

  2. ## Generate SCIM implementation

     Use a structured prompt to direct GitHub Copilot. A well-formed prompt ensures the agent generates complete, production-ready SCIM code that includes all required security components.

     Copy the following command into your terminal:

     Terminal

     ```bash
     copilot "Add Scalekit SCIM directory sync to my app — set up the webhook endpoint to receive SCIM events, validate the webhook signature, and handle user provisioning and deprovisioning events to create, update, and delete users in my database. Code only."
     ```

     GitHub Copilot uses the Modular SCIM plugin to analyze your existing application structure, generate a webhook endpoint to receive SCIM events from Scalekit, implement webhook signature validation to prevent unauthorized requests, create handlers for user provisioning events (create and update), and add deprovisioning logic to delete or deactivate users in your database.

     Review generated code

     Always review AI-generated SCIM code before deployment. Verify that webhook signature validation, event handling logic, and database operations match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  3. ## Verify the implementation

     After GitHub Copilot completes, verify that all SCIM components are properly configured:

     Check generated files:

     * Webhook endpoint that receives SCIM events from Scalekit (you may need to set up a `.env` file with your Scalekit webhook secret)
     * Webhook signature validation to authenticate incoming requests
     * User provisioning handler that creates or updates users in your database
     * Deprovisioning handler that deletes or deactivates users when they are removed from the identity provider

     The SCIM flow should receive webhook events from Scalekit when users are added, updated, or removed in the connected identity provider. Your application should validate each event’s signature, then apply the corresponding change to your user database.

  When directory sync is active, user lifecycle changes in the identity provider propagate automatically to your application. Verify that provisioning events correctly create or update users, and that deprovisioning events properly remove or deactivate accounts.

* Cursor

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI detects Cursor and installs the authstack plugin directly.

  2. ## Reload and select plugins

     Restart Cursor (or run **Developer: Reload Window**), then open **Settings > Cursor Settings > Plugins**.

     Enable the Scalekit plugins you need. The shipped skills are `integrate-agentkit`, `implement-saaskit`, `add-mcp-oauth`, `implement-sso`, and `implement-scim`.

     Done: the plugin list shows the skill you need.

     Alternative for other agents

     For agents that read the shared skills directory, such as Cline, Amp and OpenCode, install the Scalekit skills to `~/.agents/skills`:

     ```bash
     npx @scalekit-inc/cli skills install -y
     ```

     To pick individual skills, run `npx skills add scalekit-inc/authstack`.

  3. ## Generate the implementation

     Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt from the feature page (or describe what you need in natural language). The installed Scalekit plugins provide the agent with accurate patterns.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your application’s security requirements.

  4. ## Verify the implementation

     After Cursor finishes generating code, confirm all authentication components are in place:

     * The Scalekit plugin appears in **Settings > Cursor Settings > Plugins**
     * Scalekit client initialized with your API credentials (set up a `.env` file with your Scalekit environment variables)
     * Authorization URL generation and callback handler
     * Session or token integration matching your application’s existing patterns

* Other agents

  Scalekit skills work with any coding agent that supports the Agent Skills standard, such as Cline, Amp and OpenCode. Install them with the Scalekit CLI (Node.js 20 or later):

  Terminal

  ```bash
  npx @scalekit-inc/cli skills install -y
  ```

  The command installs the Scalekit skills to `~/.agents/skills`, the shared skills directory these agents read. The `-y` flag skips the confirmation prompt, so the command also works when a coding agent runs it.

  You can also install the skills with the Skills CLI. Use it when the Scalekit CLI skips the install because installed Scalekit plugins already include the skills:

  1. ## Install interactively

     Run the command with no flags to be guided through the available skills:

     Terminal

     ```bash
     npx skills add scalekit-inc/authstack
     ```

  2. ## Browse and install a specific skill

     Install the skill for your auth type (for example, MCP OAuth):

     Terminal

     ```bash
     # List all available skills
     npx skills add scalekit-inc/authstack --list


     # Install a specific skill
     npx skills add scalekit-inc/authstack --skill add-mcp-oauth
     ```

  3. ## Invoke the skill Varies by agent

     Each coding agent has its own behavior for invoking skills. In OpenCode, skills are invoked **automatically by the agent based on natural language** — no slash commands required. The agent has a list of available skills and their `description` fields in context. It reads your intent, matches it against those descriptions, and autonomously calls the skill tool to load the relevant `SKILL.md`. A clear, specific `description` in skill frontmatter is what the agent uses to decide which skill to invoke.

     **Flow in practice:**

     * You write a natural language message to the agent
     * The agent checks its context — it already sees `` with names and descriptions
     * If your request matches a skill’s purpose, the agent calls `skill("")` internally
     * The full `SKILL.md` content loads into context and the agent follows those instructions

     If your agent does not automatically pick up skills, you can run a command to load a skill and manually select Scalekit’s skills to load into context. Refer to your favorite coding agent’s documentation for how to invoke skills once they are installed.

  4. ## Install all skills globally

     To add all Scalekit authentication skills to your agents:

     Terminal

     ```bash
     npx skills add scalekit-inc/authstack --all --global
     ```

     This installs all AgentKit and SaaSKit skills.

---
# DOCUMENT BOUNDARY
---

# Coding agents: Add SSO to your app

> Let your coding agents guide you into adding Scalekit SSO to your existing application in minutes

Use AI coding agents like Claude Code, GitHub Copilot CLI, Cursor, and OpenCode to add Scalekit’s Modular SSO to your existing applications. Configure the agents to analyze your codebase, apply SSO patterns, and generate production-ready code that integrates enterprise identity providers. This follows security best practices while reducing implementation time from hours to minutes.

* Claude Code

  1. ## Install the authstack plugin

     Not yet on Claude Code? Follow the [official quickstart guide](https://code.claude.com/docs/en/quickstart) to install it.

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI installs the authstack plugin for you. Then ask the agent to run the shipped skill `implement-sso`.

     Done: the command exits 0 and `implement-sso` is available.

     Alternative: Enable authentication plugins via plugin wizard

     Run the plugin wizard to browse and enable available plugins:

     Claude REPL

     ```bash
     /plugins
     ```

     Navigate through the visual interface to enable the Modular SSO plugin.

     Auto-update recommendations

     Enable auto-updates for authentication plugins to receive security patches and improvements. Scalekit regularly updates plugins based on community feedback and security best practices.

  2. ## Generate SSO implementation

     Use a structured prompt to direct the coding agent. A well-formed prompt ensures the agent generates complete, production-ready SSO code that includes all required security components.

     Copy the following prompt into your coding agent:

     SSO implementation prompt

     ```md
     Guide the coding agent to add Scalekit SSO to my existing app — initialize ScalekitClient, generate an SSO authorization URL for a given organization, handle the SSO callback to validate and exchange the code for user identity, and integrate the SSO user into my existing session system. Code only.
     ```

     When you submit this prompt, Claude Code loads the Modular SSO skill from the marketplace -> analyzes your existing application structure -> generates Scalekit client initialization with environment credentials -> creates an SSO authorization URL generator for organization-based routing -> implements the SSO callback handler to validate and exchange the code for user identity -> integrates SSO user data into your existing session system.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  3. ## Verify the implementation

     After the coding agent completes, verify that all SSO components are properly configured:

     Check generated files:

     * Scalekit client initialization with environment credentials (you may need to set up a `.env` file with your Scalekit API credentials)
     * SSO authorization URL generation for organization-based routing
     * SSO callback handler that validates the authorization code and retrieves user identity
     * Integration logic that maps SSO user identity into your existing session system

     The SSO flow should redirect users to their organization’s identity provider, where they authenticate. Your application should then receive the callback, validate the code, extract the user’s identity, and create or update the user session accordingly.

  When users authenticate through SSO, your application receives verified identity claims from the identity provider. Verify that the SSO callback correctly maps user identity to your application’s user model and that the session is created with the appropriate access level.

* Codex

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     Choose Codex when prompted. The CLI installs the authstack plugin.

     Done: the command exits 0.

  2. ## Enable the SSO skill

     Restart Codex, then open the Plugin Directory and enable the authstack plugin.

     Load the shipped skill `implement-sso`. That skill includes the workflows, references, and prompts Codex uses to generate SAML and OIDC SSO code for your existing application.

     Done: `implement-sso` is available to Codex.

  3. ## Generate the SSO implementation

     Use a structured prompt to direct Codex. A well-formed prompt helps Codex generate complete, production-ready SSO code that includes all required security components.

     Copy the following prompt into Codex:

     SSO implementation prompt

     ```md
     Guide the coding agent to add Scalekit SSO to my existing app — initialize ScalekitClient, generate an SSO authorization URL for a given organization, handle the SSO callback to validate and exchange the code for user identity, and integrate the SSO user into my existing session system. Code only.
     ```

     When you submit this prompt, Codex loads the Modular SSO plugin from the authstack plugin, analyzes your existing application structure, generates Scalekit client initialization with environment credentials, creates an SSO authorization URL generator for organization-based routing, implements the SSO callback handler to validate and exchange the code for user identity, and integrates SSO user data into your existing session system.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  4. ## Verify the implementation

     After Codex completes, verify that all SSO components are properly configured:

     Check generated files:

     * Scalekit client initialization with environment credentials. You may need to set up a `.env` file with your Scalekit API credentials.
     * SSO authorization URL generation for organization-based routing
     * SSO callback handler that validates the authorization code and retrieves user identity
     * Integration logic that maps SSO user identity into your existing session system

     The SSO flow should redirect users to their organization’s identity provider, where they authenticate. Your application should then receive the callback, validate the code, extract the user’s identity, and create or update the user session accordingly.

  When users authenticate through SSO, your application receives verified identity claims from the identity provider. Verify that the SSO callback correctly maps user identity to your application’s user model and that the session is created with the appropriate access level.

* GitHub Copilot CLI

  1. ## Install the authstack plugin (recommended)

     Need to install GitHub Copilot CLI? See the [getting started guide](https://docs.github.com/en/copilot/how-tos/copilot-cli/cli-getting-started) — an active GitHub Copilot subscription is required.

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI installs the authstack plugin for GitHub Copilot. Then ask the agent to run the shipped skill `implement-sso`.

     Done: the command exits 0 and `implement-sso` is available.

     Tool-native alternative

     ```bash
     copilot plugin marketplace add scalekit-inc/authstack
     copilot plugin install agentkit@authstack
     copilot plugin install saaskit@authstack
     ```

     Verify the plugin is installed

     Confirm the plugin installed successfully:

     Terminal

     ```bash
     copilot plugin list
     ```

  2. ## Generate SSO implementation

     Use a structured prompt to direct GitHub Copilot. A well-formed prompt ensures the agent generates complete, production-ready SSO code that includes all required security components.

     Copy the following command into your terminal:

     Terminal

     ```bash
     copilot "Add Scalekit SSO to my existing app — initialize ScalekitClient, generate an SSO authorization URL for a given organization, handle the SSO callback to validate and exchange the code for user identity, and integrate the SSO user into my existing session system. Code only."
     ```

     GitHub Copilot uses the Modular SSO plugin to analyze your existing application structure, generate Scalekit client initialization code, create an SSO authorization URL generator for organization-based routing, implement the SSO callback handler to validate and exchange the code for user identity, and integrate SSO user data into your existing session system.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your security requirements. The coding agent provides a foundation, but you must ensure it aligns with your application’s specific needs.

  3. ## Verify the implementation

     After GitHub Copilot completes, verify that all SSO components are properly configured:

     Check generated files:

     * Scalekit client initialization with environment credentials (you may need to set up a `.env` file with your Scalekit API credentials)
     * SSO authorization URL generation for organization-based routing
     * SSO callback handler that validates the authorization code and retrieves user identity
     * Integration logic that maps SSO user identity into your existing session system

     The SSO flow should redirect users to their organization’s identity provider, where they authenticate. Your application should then receive the callback, validate the code, extract the user’s identity, and create or update the user session accordingly.

  When users authenticate through SSO, your application receives verified identity claims from the identity provider. Verify that the SSO callback correctly maps user identity to your application’s user model and that the session is created with the appropriate access level.

* Cursor

  1. ## Install the authstack plugin (recommended)

     Terminal

     ```bash
     npx @scalekit-inc/cli setup
     ```

     For repeated use, install globally:

     Terminal

     ```bash
     npm install -g @scalekit-inc/cli
     scalekit setup
     ```

     The CLI detects Cursor and installs the authstack plugin directly.

  2. ## Reload and select plugins

     Restart Cursor (or run **Developer: Reload Window**), then open **Settings > Cursor Settings > Plugins**.

     Enable the Scalekit plugins you need. The shipped skills are `integrate-agentkit`, `implement-saaskit`, `add-mcp-oauth`, `implement-sso`, and `implement-scim`.

     Done: the plugin list shows the skill you need.

     Alternative for other agents

     For agents that read the shared skills directory, such as Cline, Amp and OpenCode, install the Scalekit skills to `~/.agents/skills`:

     ```bash
     npx @scalekit-inc/cli skills install -y
     ```

     To pick individual skills, run `npx skills add scalekit-inc/authstack`.

  3. ## Generate the implementation

     Open Cursor’s chat panel with **Cmd+L** (macOS) or **Ctrl+L** (Windows/Linux) and paste in an implementation prompt from the feature page (or describe what you need in natural language). The installed Scalekit plugins provide the agent with accurate patterns.

     Review generated code

     Always review AI-generated authentication code before deployment. Verify that environment variables, token validation logic, and error handling match your application’s security requirements.

  4. ## Verify the implementation

     After Cursor finishes generating code, confirm all authentication components are in place:

     * The Scalekit plugin appears in **Settings > Cursor Settings > Plugins**
     * Scalekit client initialized with your API credentials (set up a `.env` file with your Scalekit environment variables)
     * Authorization URL generation and callback handler
     * Session or token integration matching your application’s existing patterns

* Other agents

  Scalekit skills work with any coding agent that supports the Agent Skills standard, such as Cline, Amp and OpenCode. Install them with the Scalekit CLI (Node.js 20 or later):

  Terminal

  ```bash
  npx @scalekit-inc/cli skills install -y
  ```

  The command installs the Scalekit skills to `~/.agents/skills`, the shared skills directory these agents read. The `-y` flag skips the confirmation prompt, so the command also works when a coding agent runs it.

  You can also install the skills with the Skills CLI. Use it when the Scalekit CLI skips the install because installed Scalekit plugins already include the skills:

  1. ## Install interactively

     Run the command with no flags to be guided through the available skills:

     Terminal

     ```bash
     npx skills add scalekit-inc/authstack
     ```

  2. ## Browse and install a specific skill

     Install the skill for your auth type (for example, MCP OAuth):

     Terminal

     ```bash
     # List all available skills
     npx skills add scalekit-inc/authstack --list


     # Install a specific skill
     npx skills add scalekit-inc/authstack --skill add-mcp-oauth
     ```

  3. ## Invoke the skill Varies by agent

     Each coding agent has its own behavior for invoking skills. In OpenCode, skills are invoked **automatically by the agent based on natural language** — no slash commands required. The agent has a list of available skills and their `description` fields in context. It reads your intent, matches it against those descriptions, and autonomously calls the skill tool to load the relevant `SKILL.md`. A clear, specific `description` in skill frontmatter is what the agent uses to decide which skill to invoke.

     **Flow in practice:**

     * You write a natural language message to the agent
     * The agent checks its context — it already sees `` with names and descriptions
     * If your request matches a skill’s purpose, the agent calls `skill("")` internally
     * The full `SKILL.md` content loads into context and the agent follows those instructions

     If your agent does not automatically pick up skills, you can run a command to load a skill and manually select Scalekit’s skills to load into context. Refer to your favorite coding agent’s documentation for how to invoke skills once they are installed.

  4. ## Install all skills globally

     To add all Scalekit authentication skills to your agents:

     Terminal

     ```bash
     npx skills add scalekit-inc/authstack --all --global
     ```

     This installs all AgentKit and SaaSKit skills.

---
# DOCUMENT BOUNDARY
---

# Scalekit CLI

> Install and manage the authstack plugin and Scalekit skills for AI coding agents with the Scalekit CLI.

The Scalekit CLI (`@scalekit-inc/cli`) installs the authstack plugin and Scalekit skills for your AI coding agents. It supports Cursor, Claude Code, GitHub Copilot and Codex, plus any agent that reads skills from `~/.agents/skills`, such as Cline, Amp and OpenCode.

The CLI requires **Node.js 20 or later**. Run it with `npx`, or install it globally:

```bash
npx @scalekit-inc/cli setup


# Or install globally for repeated use
npm install -g @scalekit-inc/cli
scalekit setup
```

## Run the setup wizard

[Section titled “Run the setup wizard”](#run-the-setup-wizard)

Running `setup` with no arguments starts an interactive wizard. The wizard marks the agents it detects on your machine, lets you choose which ones to set up, and then offers to install the Scalekit skills.

**The wizard needs an interactive terminal.** Without one, for example when a coding agent or a CI job runs the command, `setup` exits with an error that tells you to add `-y`.

## Run setup from a coding agent or CI

[Section titled “Run setup from a coding agent or CI”](#run-setup-from-a-coding-agent-or-ci)

Coding agents and CI jobs have no terminal to answer prompts. Add `-y` to skip every prompt:

```bash
npx -y @scalekit-inc/cli setup -y          # every detected agent
npx -y @scalekit-inc/cli setup claude -y   # one agent
```

The first `-y` tells `npx` to install the package without asking. The second `-y` tells the CLI to skip its own prompts.

`setup -y` sets up every agent it detects:

* **Agents detected**: the CLI installs the authstack plugin for each detected agent. It then installs the Scalekit skills for detected agents that did not get a plugin, and to `~/.agents/skills` unless a plugin already provides the same skills.
* **No agent detected** (CLI 0.3.24 or later): the CLI installs only the Scalekit skills, to `~/.agents/skills`, and exits with status 0. Earlier versions try to set up every agent instead, so run `npx -y @scalekit-inc/cli@latest`.

Add `--dry-run` to print the commands without running them. A dry run of `setup` never prompts.

## Target a specific tool

[Section titled “Target a specific tool”](#target-a-specific-tool)

Pass the tool name to skip the wizard and install directly:

```bash
scalekit setup cursor
scalekit setup claude      # aliases: claude-code, cc
scalekit setup codex       # alias: opencode
scalekit setup copilot     # aliases: github-copilot, ghcp
```

## Command reference

[Section titled “Command reference”](#command-reference)

Run any command with `npx @scalekit-inc/cli `, or with `scalekit ` (short form: `sk`) after a global install.

| Command                      | What it does                                                                                    |
| ---------------------------- | ----------------------------------------------------------------------------------------------- |
| `setup [stack]`              | Sets up the authstack plugin and Scalekit skills. Omit `[stack]` to run the wizard.             |
| `setup extension `     | Shortcut for `extension install `.                                                        |
| `extension install `   | Installs the authstack plugin for one agent.                                                    |
| `extension update `    | Updates the authstack plugin for one agent.                                                     |
| `extension uninstall ` | Removes the authstack plugin from one agent.                                                    |
| `extension list`             | Lists the supported agents and whether each one is detected.                                    |
| `extension status [stack]`   | Shows whether each agent is detected and whether its plugin is up to date.                      |
| `open [stack]`               | Opens Cursor, Claude Code or Codex with a first prompt filled in. With `-y`, name the stack.    |
| `skills install`             | Installs the Scalekit skills to `~/.agents/skills` and to detected agents that lack the plugin. |
| `update`                     | Updates the Scalekit CLI itself.                                                                |

`extension` also has the short form `ext`. Stack and extension names accept the IDs and aliases in [Supported tools](#supported-tools).

### Flags

[Section titled “Flags”](#flags)

These flags work on every command:

| Flag                      | Description                                                    |
| ------------------------- | -------------------------------------------------------------- |
| `-y`, `--non-interactive` | Skip all prompts and use defaults.                             |
| `--json`                  | Print machine-readable JSON instead of styled output.          |
| `--plain`                 | Turn off colors and styling. The CLI also respects `NO_COLOR`. |

These flags apply to individual commands:

| Flag            | Commands                                                                                                    | Description                                                                                                             |
| --------------- | ----------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `-y`, `--yes`   | `setup`, `extension install`, `extension update`, `extension uninstall`, `open`, `skills install`, `update` | Skip confirmation prompts. Required for `setup`, `open` and `skills install` when no interactive terminal is available. |
| `--dry-run`     | Same as `-y`                                                                                                | Print the commands, or the destination for `open`, without running them.                                                |
| `--skip-skills` | `setup`                                                                                                     | Install the plugins only and skip the Scalekit skills.                                                                  |

### Supported tools

[Section titled “Supported tools”](#supported-tools)

| Tool             | ID        | Aliases                  |
| ---------------- | --------- | ------------------------ |
| Cursor           | `cursor`  |                          |
| Claude Code      | `claude`  | `claude-code`, `cc`      |
| GitHub Copilot   | `copilot` | `github-copilot`, `ghcp` |
| Codex / OpenCode | `codex`   | `opencode`               |

### What gets installed

[Section titled “What gets installed”](#what-gets-installed)

For each tool, the CLI installs two kits from the authstack plugin:

* **AgentKit** - skills for building agents with delegated auth, scoped permissions, and tool calls
* **SaaSKit** - skills for adding SSO, SCIM, MFA, sessions, and API auth to SaaS apps

For Cursor and Codex, the CLI downloads the plugin and copies it to the tool’s local plugin directory. For Claude Code and GitHub Copilot, it runs the tool’s native plugin marketplace commands.

## Turn off telemetry

[Section titled “Turn off telemetry”](#turn-off-telemetry)

The CLI sends anonymous usage events, such as whether a plugin or skill install succeeded. It stores a random ID in `~/.scalekit/anonymous_id` and never sends project paths, file contents or credentials. To turn telemetry off, set either environment variable:

```bash
export DO_NOT_TRACK=1
# or
export SCALEKIT_TELEMETRY=0
```

[The CLI’s README](https://github.com/scalekit-inc/cli#telemetry) lists each event and field.

## Start building

[Section titled “Start building”](#start-building)

After setup, tell your coding agent what you’re building. The authstack plugin routes you to the right skill.

* Claude Code

  ```text
  I want to add agent auth to my project. Help me get started with AgentKit.
  ```

  ```text
  Add enterprise SSO to my Next.js app using SaaSKit.
  ```

* Cursor

  ```text
  I want to add agent auth to my project. Help me get started with AgentKit.
  ```

  ```text
  Add enterprise SSO to my Next.js app using SaaSKit.
  ```

* GitHub Copilot

  ```text
  I want to add agent auth to my project. Help me get started with AgentKit.
  ```

  ```text
  Add enterprise SSO to my Next.js app using SaaSKit.
  ```

* Codex

  ```text
  I want to add agent auth to my project. Help me get started with AgentKit.
  ```

  ```text
  Add enterprise SSO to my Next.js app using SaaSKit.
  ```

The authstack plugin picks up the prompt and routes to the right skill: AgentKit for agent auth, SaaSKit for app auth.

---
# DOCUMENT BOUNDARY
---

# Error handling

> Catch and respond to invalid tokens, bad requests, missing resources, and rate limits on the Scalekit REST API.

Catch and respond to invalid tokens, bad requests, missing resources, and rate limits.

A failed REST call returns an HTTP status and a JSON body. Official SDKs raise typed exceptions for the same failures. Use the body, or the exception fields, to choose a response.

For the endpoint catalog, open the [API reference](/apis/#description/overview). Product catalogs live at [SaaSKit APIs](/saaskit/apis/#description/overview) and [AgentKit APIs](/agentkit/reference/).

## Read the error body

[Section titled “Read the error body”](#read-the-error-body)

| Field                             | Meaning                                                                                                           |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `code`                            | gRPC (remote procedure call) status **number**. This is not the HTTP status. Example: `16` means Unauthenticated. |
| `message`                         | Human-readable reason. Safe for logs. Do not show raw messages to end users.                                      |
| `details[].error_code`            | Stable slug to switch on, such as `UNAUTHENTICATED`.                                                              |
| `details[].validation_error_info` | Present on some `400` responses. Lists field violations (`field`, `description`, `constraint`).                   |
| `details[].tool_error_info`       | Present when a tool call fails. Includes `execution_id`, `tool_error_message`, and `tool_error_code`.             |
| `details[].help_info`             | Optional documentation links when the server attaches them.                                                       |

Scalekit does not return a request ID or a dashboard log URL in this envelope.

401 when the bearer token is missing

```json
{
  "code": 16,
  "message": "Token empty",
  "details": [
    {
      "@type": "type.googleapis.com/scalekit.v1.errdetails.ErrorInfo",
      "error_code": "UNAUTHENTICATED"
    }
  ]
}
```

Switch on `details[].error_code`. Do not switch on `code` alone.

## HTTP status codes

[Section titled “HTTP status codes”](#http-status-codes)

* Codes in the `2xx` range mean success.
* Codes in the `4xx` range mean the request failed given the data you sent. Fix the request. Do not retry the same payload, except for `429`.
* Codes in the `5xx` range mean a server fault. Retry with backoff. Treat the result as unknown until a later call succeeds.

| HTTP          | Typical `error_code`                | Meaning                               |
| ------------- | ----------------------------------- | ------------------------------------- |
| `200` / `201` | n/a                                 | Success                               |
| `400`         | `INVALID_ARGUMENT`, `BAD_REQUEST`   | Invalid request or validation failure |
| `401`         | `UNAUTHENTICATED`                   | Missing or invalid bearer token       |
| `403`         | `PERMISSION_DENIED`, `FORBIDDEN`    | Authenticated, but not allowed        |
| `404`         | `NOT_FOUND`, `RESOURCE_NOT_FOUND`   | Resource does not exist               |
| `409`         | `RESOURCE_ALREADY_EXISTS`           | Duplicate resource                    |
| `429`         | `RATE_LIMITED`, `TOO_MANY_REQUESTS` | Rate limit exceeded                   |
| `500`         | `INTERNAL_ERROR`                    | Server error                          |

## Catch exceptions

[Section titled “Catch exceptions”](#catch-exceptions)

SDKs translate non-success HTTP responses into exceptions. Catch the specific type first, then fall back to the base server exception.

* Node.js

  catch-api-errors.ts

  ```ts
  1
  import {
  2
    ScalekitNotFoundException,
  3
    ScalekitUnauthorizedException,
  4
    ScalekitTooManyRequestsException,
  5
    ScalekitServerException,
  6
  } from '@scalekit-sdk/node'
  7


  8
  try {
  9
    // Your Scalekit SDK call
  10
    await scalekit.organization.listOrganization({ pageSize: 30 })
  11
  } catch (err) {
  12
    if (err instanceof ScalekitUnauthorizedException) {
  13
      // Get a new access token. Do not retry the same bearer token.
  14
    } else if (err instanceof ScalekitNotFoundException) {
  15
      // Create the resource or return not-found to the caller.
  16
    } else if (err instanceof ScalekitTooManyRequestsException) {
  17
      // Back off. Read err.errorCode: RATE_LIMITED vs TOOL_ERROR.
  18
    } else if (err instanceof ScalekitServerException) {
  19
      // Log err.message and err.errorCode. Retry only on 5xx or 429.
  20
    } else {
  21
      throw err
  22
    }
  23
  }
  ```

* Python

  catch_api_errors.py

  ```py
  1
  from scalekit.common.exceptions import (
  2
      ScalekitNotFoundException,
  3
      ScalekitUnauthorizedException,
  4
      ScalekitTooManyRequestsException,
  5
      ScalekitServerException,
  6
  )
  7


  8
  try:
  9
      # Your Scalekit SDK call
  10
      scalekit_client.organization.list_organizations(page_size=30)
  11
  except ScalekitUnauthorizedException:
  12
      # Get a new access token. Do not retry the same bearer token.
  13
      pass
  14
  except ScalekitNotFoundException:
  15
      # Create the resource or return not-found to the caller.
  16
      pass
  17
  except ScalekitTooManyRequestsException as e:
  18
      # Back off. Read e.error_code: RATE_LIMITED vs TOOL_ERROR.
  19
      print(e.error_code, e.message)
  20
  except ScalekitServerException as e:
  21
      # Log e.message and e.error_code. Retry only on 5xx or 429.
  22
      print(e.error_code, e.http_status)
  ```

* Go

  catch_api_errors.go

  ```go
  _, err := scalekitClient.Organization().ListOrganization(ctx, &scalekit.ListOrganizationOptions{
      PageSize: 30,
  })
  if err != nil {
      // Inspect the error. Retry only on HTTP 429 or 5xx.
      // Do not retry the same payload on 400, 401, 403, 404, or 409.
      log.Printf("scalekit API error: %v", err)
      return err
  }
  ```

* Java

  CatchApiErrors.java

  ```java
  try {
      scalekitClient.organizations().listOrganizations(30, "");
  } catch (ScalekitException error) {
      // Inspect the error. Retry only on HTTP 429 or 5xx.
      // Do not retry the same payload on 400, 401, 403, 404, or 409.
      System.err.println(error.getMessage());
  }
  ```

AgentKit’s error body, including the details of a failed tool call, is described in [Errors and rate limits](/agentkit/reference/errors/) in the AgentKit API reference.

## Choose a response

[Section titled “Choose a response”](#choose-a-response)

| `error_code`                         | Problem                                                                                                  | What to do                                                                                                                      |
| ------------------------------------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| `UNAUTHENTICATED`                    | Missing header (`Token empty`), bad `Bearer` value (`Invalid Token`), or expired token (`token expired`) | Request a new token. Do not retry the same token. See [Authenticate with the Scalekit API](/guides/authenticate-scalekit-api/). |
| `INVALID_ARGUMENT` / `BAD_REQUEST`   | The request is malformed or fails validation                                                             | Fix the fields. Do not retry the same payload.                                                                                  |
| `PERMISSION_DENIED` / `FORBIDDEN`    | The caller is authenticated but not allowed                                                              | Change roles, scopes, or the target resource.                                                                                   |
| `NOT_FOUND` / `RESOURCE_NOT_FOUND`   | The resource does not exist                                                                              | Create it, or return not-found to your user.                                                                                    |
| `RESOURCE_ALREADY_EXISTS`            | A create conflicts with an existing resource                                                             | Use the existing ID, or pick a new unique value.                                                                                |
| `RATE_LIMITED` / `TOO_MANY_REQUESTS` | A Scalekit service rejected the call                                                                     | Back off, then retry. See [API rate limits](/reference/rate-limits/).                                                           |
| `TOOL_ERROR`                         | A tool call failed                                                                                       | Read `tool_error_info.tool_error_code`. An inner `RATE_LIMITED` is the upstream provider.                                       |
| `INTERNAL_ERROR`                     | A server fault                                                                                           | Retry with backoff. Treat the result as unknown until a later call succeeds.                                                    |

`/api/v1` accepts a bearer access token from client credentials. It does not accept an API key header.

## Validation errors

[Section titled “Validation errors”](#validation-errors)

Some `400` responses include `validation_error_info.field_violations`. Each violation names the field, a description, and the constraint that failed. The JSON below is an example of that shape.

Example validation error shape

```json
1
{
2
  "code": 3,
3
  "message": "Validation error",
4
  "details": [
5
    {
6
      "@type": "type.googleapis.com/scalekit.v1.errdetails.ErrorInfo",
7
      "error_code": "INVALID_ARGUMENT",
8
      "validation_error_info": {
9
        "field_violations": [
10
          {
11
            "field": "page_size",
12
            "description": "value must be less than or equal to 30",
13
            "constraint": "lte"
14
          }
15
        ]
16
      }
17
    }
18
  ]
19
}
```

## Tool errors

[Section titled “Tool errors”](#tool-errors)

Tool execution sets the **outer** `error_code` to `TOOL_ERROR`. The inner slug is `tool_error_info.tool_error_code`. An upstream provider `429` still returns HTTP `429`.

Upstream provider rate limit on a tool call

```json
{
  "code": 8,
  "message": "tool execution failed - rate limited",
  "details": [
    {
      "@type": "type.googleapis.com/scalekit.v1.errdetails.ErrorInfo",
      "error_code": "TOOL_ERROR",
      "tool_error_info": {
        "execution_id": "exec_123",
        "tool_error_message": "rate limit",
        "tool_error_code": "RATE_LIMITED"
      }
    }
  ]
}
```

Read the outer code first. Then read `tool_error_info.tool_error_code` when the outer code is `TOOL_ERROR`.

## Handle 429s

[Section titled “Handle 429s”](#handle-429s)

When a service exceeds its limit, Scalekit returns HTTP `429`. The response body uses the same error envelope.

| Outer `error_code`                                       | Meaning                                                                                    |
| -------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `RATE_LIMITED`                                           | A Scalekit service rejected the call                                                       |
| `TOO_MANY_REQUESTS`                                      | Email, one-time password (OTP), or time-based one-time password (TOTP) traffic hit a limit |
| `TOOL_ERROR` with inner `tool_error_code` `RATE_LIMITED` | The upstream provider rate-limited the tool call                                           |

Back off and retry with exponential backoff. Do not retry immediately.

Scalekit does not send a `Retry-After` header on 429 responses. Limits vary by service and account. For 429 handling in the SDKs, see [API rate limits](/reference/rate-limits/).

## Fix common 400s

[Section titled “Fix common 400s”](#fix-common-400s)

Some `400` `INVALID_ARGUMENT` responses come from list and lookup URLs.

| Case                                | HTTP  | `error_code`       | Message                                  |
| ----------------------------------- | ----- | ------------------ | ---------------------------------------- |
| Bad `page_token`                    | `400` | `INVALID_ARGUMENT` | `invalid page token` or `Invalid cursor` |
| `:external` path plus an `id` query | `400` | `INVALID_ARGUMENT` | `ExternalId is required`                 |

Fix the URL or the query. Do not retry the same request.

For list parameters, see the operation in the [API reference](/apis/#description/overview). For external IDs, see [organization identifiers](/guides/external-ids-and-metadata/).

---
# DOCUMENT BOUNDARY
---

# Manage team members and roles

> Invite teammates to the Scalekit dashboard, give each one the Admin, Developer, Member or No Access role, and override a role for one environment.

Invite your team to the Scalekit dashboard and give each person a role that matches their work. Roles control who can view or change your SSO connections, directories, organizations and other settings. A role applies to every environment in the workspace unless you override it for one environment.

Using AgentKit? [AgentKit team members and roles](/agentkit/team-members/) lists what each role can do in AgentKit.

## Before you start

[Section titled “Before you start”](#before-you-start)

* You need the **Admin** role to invite people or change roles. Other roles can see the team but can’t change it.

## Roles

[Section titled “Roles”](#roles)

| Role          | For                                                                           |
| ------------- | ----------------------------------------------------------------------------- |
| **Admin**     | Full access to everything, including members, roles and billing.              |
| **Developer** | Build and configure your integration. Can’t manage members, roles or billing. |
| **Member**    | Read-only access to settings.                                                 |
| **No Access** | Can’t open the dashboard. Use it to suspend someone without removing them.    |

What each role can do for modular SSO and SCIM:

| Area                                                               | Admin | Developer | Member |
| ------------------------------------------------------------------ | ----- | --------- | ------ |
| SSO connections and domains: view                                  | Yes   | Yes       | Yes    |
| SSO connections and domains: create, edit and delete               | Yes   | Yes       | No     |
| Directories (SCIM): view, including synced users and groups        | Yes   | Yes       | Yes    |
| Directories (SCIM): create, edit and delete, including SCIM tokens | Yes   | Yes       | No     |
| Organizations: view                                                | Yes   | Yes       | Yes    |
| Organizations: create and edit                                     | Yes   | Yes       | No     |
| Organizations: delete                                              | Yes   | No        | No     |
| Admin portal links: generate and revoke                            | Yes   | No        | No     |
| Auth logs                                                          | Yes   | Yes       | No     |
| API credentials: view and generate secrets                         | Yes   | Yes       | No     |
| Webhooks: view                                                     | Yes   | Yes       | Yes    |
| Webhooks: create and edit                                          | Yes   | Yes       | No     |
| Billing                                                            | Yes   | No        | No     |
| Team, roles and environment access: view                           | Yes   | Yes       | Yes    |
| Team, roles and environment access: change                         | Yes   | No        | No     |

**No Access** can do none of these.

## Invite a teammate

[Section titled “Invite a teammate”](#invite-a-teammate)

Only people you invite can join your workspace.

1. Open **Workspace** > **Team Members** and select **Invite**.

2. Enter their **Email**, choose a **Role**, and select **Invite**.

The person appears in **Team Members** with the role you chose.

## Change a role

[Section titled “Change a role”](#change-a-role)

In **Team Members**, open the member’s menu and select **Edit role**. Choose a new **Workspace role** and select **Save**.

### Give a different role in one environment

[Section titled “Give a different role in one environment”](#give-a-different-role-in-one-environment)

Under **Environment access** in the same drawer, select **Set override** next to an environment and choose the role for that environment only. For example, make someone an Admin in Development and a Member in Production. **Reset to Default** removes the override.

## Remove a teammate

[Section titled “Remove a teammate”](#remove-a-teammate)

In **Team Members**, open the member’s menu, select **Remove Member**, and confirm with **Delete**. To block access but keep the person’s record, give them **No Access** instead.

## Create a custom role

[Section titled “Create a custom role”](#create-a-custom-role)

When the four roles don’t fit, open **Workspace** > **Roles** and select **Create role**. Give it a **Role name** and **Description**, then choose its **Permissions**, or start from a preset such as **Support** or **Billing Owner**. You can only grant permissions your own role has.

Custom roles appear in the **Role** list when you invite or edit a member.

## Check it worked

[Section titled “Check it worked”](#check-it-worked)

Ask the teammate to sign in to the dashboard. In **Team Members**, their row shows the role you gave them. Open **Edit role** to see their **Effective role** in each environment.

---
# DOCUMENT BOUNDARY
---

# Set up AI-assisted development

> Learn how to use AI assisted setup to create a new project in Scalekit

Scalekit provides LLM-friendly capabilities that speed up implementation and guide you through integration steps. Use this guide to configure your preferred AI tools with first-class context awareness of the Scalekit platform.

## Configure code editors for Scalekit documentation

[Section titled “Configure code editors for Scalekit documentation”](#configure-code-editors-for-scalekit-documentation)

In-code editor chat features are powered by models that understand your codebase and project context. These models search the web for relevant information to help you. However, they may not always have the latest information. Follow the instructions below to configure your code editors to explicitly index for up-to-date information.

### Set up Cursor

[Section titled “Set up Cursor”](#set-up-cursor)

[Play](https://youtube.com/watch?v=oMMG1k_9fmU)

To enable Cursor to access up-to-date Scalekit documentation:

1. Open Cursor settings (Cmd/Ctrl + ,)
2. Navigate to **Indexing & Docs** section
3. Click on **Add**
4. Add `https://docs.scalekit.com/llms-full.txt` to the indexable URLs
5. Click on **Save**

Once configured, use `@Scalekit Docs` in your chat to ask questions about Scalekit features, APIs, and integration guides. Cursor will search the latest documentation to provide accurate, up-to-date answers.

### Use Windsurf

[Section titled “Use Windsurf”](#use-windsurf)

![Windsurf editor with the Cascade chat panel answering a question using indexed Scalekit documentation](/.netlify/images?url=_astro%2Fwindsurf.CfsQQlGb.png\&w=1357\&h=818\&dpl=6ae471fc143c9a4c27550463)

Windsurf enables `@docs` mentions within the Cascade chat to search for the best answers to your questions.

* Full Documentation

  ```plaintext
  @docs:https://docs.scalekit.com/llms-full.txt
  
  ```

  Costs more tokens.

* Specific Section

  ```plaintext
  @docs:https://docs.scalekit.com/your-specific-section-or-file
  
  ```

  Costs less tokens.

* Let AI decide

  ```plaintext
  @docs:https://docs.scalekit.com/llms.txt
  
  ```

  Costs tokens as per the model decisions.

## Use AI assistants

[Section titled “Use AI assistants”](#use-ai-assistants)

Assistants like **Anthropic Claude**, **Ollama**, **Google Gemini**, **Vercel v0**, **OpenAI’s ChatGPT**, or your own models can help you with Scalekit projects.

[Play](https://youtube.com/watch?v=ZDAI32I6s-I)

Need help with a specific AI tool?

Don’t see instructions for your favorite AI assistant? We’d love to add support for more tools! [Raise an issue](https://github.com/scalekit-inc/developer-docs/issues) on our GitHub repository and let us know which AI tool you’d like us to document.

---
# DOCUMENT BOUNDARY
---

# SDKs

> Ready-to-use SDKs for Node.js, Python, Go, and Java to integrate Scalekit into your app

* [Node.jsv2.21.0](/saaskit/sdks/node/)

  [npm install @scalekit-sdk/node](/saaskit/sdks/node/)

  [Full-featured, TypeScript-friendly SDK for modern Node.js based applications](/saaskit/sdks/node/)

  [Updated 2 days agoInstall→](/saaskit/sdks/node/)
* [Expressv2.21.0](/saaskit/sdks/express/)

  [npm install @scalekit-sdk/node express](/saaskit/sdks/express/)

  [Session middleware for Express. Hosted login, encrypted cookie, token refresh.](/saaskit/sdks/express/)

  [Updated 2 days agoInstall→](/saaskit/sdks/express/)
* [Next.jsv2.21.0](/saaskit/sdks/nextjs/)

  [npm install @scalekit-sdk/node](/saaskit/sdks/nextjs/)

  [Session middleware for the Next.js App Router, including Edge Runtime.](/saaskit/sdks/nextjs/)

  [Updated 2 days agoInstall→](/saaskit/sdks/nextjs/)
* [Pythonv2.23.0](/saaskit/sdks/python/)

  [pip install scalekit-sdk-python](/saaskit/sdks/python/)

  [Async-first design with complete type hints and Pydantic validation](/saaskit/sdks/python/)

  [Updated 1 day agoInstall→](/saaskit/sdks/python/)
* [Flaskv2.23.0](/saaskit/sdks/flask/)

  [pip install "scalekit-sdk-python\[flask\]"](/saaskit/sdks/flask/)

  [Session middleware for Flask. Hosted login, encrypted cookie, token refresh.](/saaskit/sdks/flask/)

  [Updated 1 day agoInstall→](/saaskit/sdks/flask/)
* [FastAPIv2.23.0](/saaskit/sdks/fastapi/)

  [pip install "scalekit-sdk-python\[fastapi\]"](/saaskit/sdks/fastapi/)

  [Session middleware for FastAPI. Protect routes with Depends().](/saaskit/sdks/fastapi/)

  [Updated 1 day agoInstall→](/saaskit/sdks/fastapi/)
* [Djangov2.23.0](/saaskit/sdks/django/)

  [pip install "scalekit-sdk-python\[django\]"](/saaskit/sdks/django/)

  [Session middleware for Django. Settings-based config and login_required.](/saaskit/sdks/django/)

  [Updated 1 day agoInstall→](/saaskit/sdks/django/)
* [Gov2.9.0](/saaskit/sdks/go/)

  [go get github.com/scalekit-inc/scalekit-sdk-go](/saaskit/sdks/go/)

  [Zero-dependency, idiomatic Go SDK for high-performance services](/saaskit/sdks/go/)

  [Updated 3 weeks agoInstall→](/saaskit/sdks/go/)
* [Javav2.5.0](/saaskit/sdks/java/)

  [Maven Central: com.scalekit:scalekit-sdk-java](/saaskit/sdks/java/)

  [Enterprise-ready SDK with seamless Spring Boot integration](/saaskit/sdks/java/)

  [Updated 3 weeks agoInstall→](/saaskit/sdks/java/)
* [Expo](/sdks/expo/)

  [npx expo install @scalekit-sdk/expo](/sdks/expo/)

  [Official Expo SDK with React Hooks for enterprise-ready mobile authentication](/sdks/expo/)

  [Install→](/sdks/expo/)
* [iOS](/sdks/ios/)

  [Swift Package Manager: ScalekitSDK](/sdks/ios/)

  [Native iOS SDK for enterprise-ready mobile authentication via AppAuth](/sdks/ios/)

  [Install→](/sdks/ios/)

---
# DOCUMENT BOUNDARY
---

# Dryrun

> Try your authentication flows locally before any integration code is written

Use `npx @scalekit-sdk/dryrun` when you want to confirm your Scalekit authentication configuration works end-to-end before implementing auth integration into your app.

Dryrun command executes a complete authentication flow locally - spinning up a server, opening your browser, and displaying the authenticated user’s profile and tokens, so you can catch configuration errors early.

Works with Full Stack Authentication and Modular SSO.

## Prerequisites

[Section titled “Prerequisites”](#prerequisites)

Before running Dryrun, ensure you have:

* **Node.js 20 or higher** installed locally.
* **A Scalekit environment** with an OAuth client configured.
* **A redirect URI** (`http://localhost:12456/auth/callback`) added in the Scalekit Dashboard under **Authentication > Redirect URIs**.

## Run Dryrun

[Section titled “Run Dryrun”](#run-dryrun)

From any directory:

Terminal

```bash
# Refer to prerequisites before running the command
npx @scalekit-sdk/dryrun \
  --env_url= \
  --client_id= \
  [--mode=] \
  [--organization_id=]
```

| Option              | Description                                                                         |
| ------------------- | ----------------------------------------------------------------------------------- |
| `--env_url`         | Scalekit environment URL, for example `https://env-abc123.scalekit.cloud`. Required |
| `--client_id`       | OAuth client ID from the Scalekit Dashboard (starts with `skc_`). Required          |
| `--mode`            | `fsa` for full-stack auth, `sso` for SSO. Defaults to `fsa`. Optional               |
| `--organization_id` | Organization ID to authenticate against when `--mode=sso`. Required (SSO only)      |
| `--help`            | Show CLI usage help. Optional                                                       |

Get your credentials

Find your environment URL and client ID in **Dashboard > Developers > Settings > API Credentials**.

Local testing only

Dryrun is designed for **local testing only**:

* It runs entirely on `localhost` and does not expose any public endpoints.
* It does not persist tokens or credentials after the process exits.
* The CLI stops when you press `Ctrl+C`, which shuts down the local server.

Use this tool only in trusted local environments and never expose the local callback URL to the internet.

## Review authentication results

[Section titled “Review authentication results”](#review-authentication-results)

After successful authentication, the browser shows a local dashboard with:

* **User profile**: Name, email, avatar (when available).
* **ID token claims**: All claims returned in the ID token.
* **Token details**: A view of the raw token response.

![User profile details screenshot](/.netlify/images?url=_astro%2Fuser-profile-details.C55W6Ini.png\&w=2922\&h=1854\&dpl=6ae471fc143c9a4c27550463)

Use this view to confirm:

* The correct user is returned for your test login.
* Claims such as `email`, `sub`, and any custom claims are present as expected.
* The flow works for both `fsa` and `sso` modes when configured.

## Common error scenarios

[Section titled “Common error scenarios”](#common-error-scenarios)

How do I fix redirect_uri mismatch errors?

If you see a `redirect_uri mismatch` error:

* Verify that `http://localhost:12456/auth/callback` is added in the Scalekit Dashboard under **Authentication > Redirect URIs**.
* Confirm that you spelled the URI exactly, including the port and path.

How do I fix invalid client_id errors?

If the CLI reports an invalid client ID:

* Copy the client ID directly from the dashboard to avoid typos.
* Make sure you are using a client from the same environment as `--env_url`.

How do I resolve port conflicts?

If port `12456` is already in use:

* Stop any process that is already listening on port `12456`.
* Close other local tools or frameworks that use `http://localhost:12456` and try again.

How do I fix organization issues in SSO mode?

If you see errors related to `--organization_id`:

* Confirm that the organization exists in your Scalekit environment.
* Verify that SSO is configured for that organization in the dashboard.
* Ensure you are using the correct `org_...` identifier.

---
# DOCUMENT BOUNDARY
---

# SSO simulator

> Test SSO flows end to end using Scalekit’s built-in IdP simulator and a pre-configured test organization.

Scalekit provides an **SSO simulator** so you can test SSO flows before you connect to real enterprise identity providers. You use it when you are implementing SSO with Scalekit and want to verify your application’s behavior end to end.

Without the simulator, you often need to configure multiple providers—such as Microsoft Entra ID, PingIdentity, and Okta—and create test tenants and users just to prove that your SSO flow works. Instead, the SSO simulator lets you trigger the authentication flow with test email domains like `@example.com` and verify how your application handles successful logins and failures, without doing any external IdP configuration.

Before you use the SSO simulator, make sure you have:

* SSO flow integrated in your app with Scalekit. For example, you have completed setup that generates an authorization URL and handles the callback either with [Modular SSO](/authenticate/sso/add-modular-sso) or [Full stack Authentication](/authenticate/auth-methods/enterprise-sso).
* Access to the [Scalekit Dashboard](https://app.scalekit.com) for viewing organizations and connection details.

Your development environment includes a **Test Organization** that has connection already setup to the SSO simulator. This organization is safe to use for SSO testing and does not affect real customers.

1. **Locate the test organization**

   Open **Dashboard → Organizations** and look for an entry named **Test Organization**.

   The details page shows the test organization’s identifier (for example, `org_32656XXXXXX0438`) and any connected SSO integrations.

   ![Test Organization](/.netlify/images?url=_astro%2F2.CCYEcEtj.png\&w=2786\&h=1746\&dpl=6ae471fc143c9a4c27550463)

2. **Copy the organization ID**

   From the **Test Organization** details page, copy the **Organization ID**. You pass this value to the SDK when you generate an SSO authorization URL.

   * Node.js

     Express.js

     ```javascript
     const options = {
       organizationId: 'org_32656XXXXXX0438',
     }


     const authorizationUrl = await scalekit.getAuthorizationUrl(
       'https://your-app.example.com/auth/callback',
       options,
     )
     ```

   * Python

     Flask

     ```python
     options = {
         "organizationId": "org_32656XXXXXX0438",
     }


     authorization_url = scalekit_client.get_authorization_url(
         "https://your-app.example.com/auth/callback",
         options,
     )
     ```

   * Go

     Gin

     ```go
     options := scalekit.AuthorizationUrlOptions{
       OrganizationId: "org_32656XXXXXX0438",
     }


     authorizationURL, err := scalekitClient.GetAuthorizationUrl(
       "https://your-app.example.com/auth/callback",
       options,
     )
     ```

   * Java

     Spring Boot

     ```java
     AuthorizationUrlOptions options = new AuthorizationUrlOptions();
     options.setOrganizationId("org_32656XXXXXX0438");


     URI authorizationUrl = scalekitClient
       .authentication()
       .getAuthorizationUrl("https://your-app.example.com/auth/callback", options);
     ```

   * Direct URL (no SDK)

     Authorization URL

     ```sh
     /oauth/authorize?
       response_type=code&
       client_id=&
       redirect_uri=&
       scope=openid%20profile%20email&
       organization_id=org_32656XXXXXX0438
     ```

     example email addresses

     In developer environments, SSO simulator can be accessed by passing an `example.org` or an `example.com` email addresses. This is useful for starting a SSO simulator

3. **Simulate a SSO login**

   Generated authorization URL redirects the users to SSO simulator.

   1. Select **User login via SSO** from the dropdown menu
   2. Enter test user details (email, name, etc.) to simulate authentication
   3. Click **Submit** to complete the simulation

   ![SSO Simulator form](/.netlify/images?url=_astro%2F2.1.BEM1Vo-J.png\&w=2646\&h=1652\&dpl=6ae471fc143c9a4c27550463)

   After submitting the form, your application receives an `idToken` containing the user details you entered:

   ![ID token response](/.netlify/images?url=_astro%2F2.2.tePTMu6U.png\&w=2182\&h=1146\&dpl=6ae471fc143c9a4c27550463)

Custom user attributes

To test custom attributes from the SSO Simulator, first register them at **Dashboard > Development > Single Sign-On > Custom Attributes**.

### Full stack authentication vs modular SSO

[Section titled “Full stack authentication vs modular SSO”](#full-stack-authentication-vs-modular-sso)

How you reach the SSO simulator depends on how you use Scalekit:

* **Modular SSO:** You can route users to the SSO simulator by including `login_hint=name@example.com` (or `organization_id=`) in the authorization URL. You are not limited to passing only the organization ID.
* **Full stack authentication:** You do not need to pass any parameters when creating the authorization URL. Redirect users to Scalekit’s hosted login page; when they enter an email with a domain such as `example.com` or `example.org`, the login screen automatically sends them to the SSO simulator.

---
# DOCUMENT BOUNDARY
---

# Use Scalekit credentials

> Use Scalekit-managed test accounts to validate social logins and agent tool connections without configuring your own provider credentials.

Scalekit provides development environments that let you test your authentication flows end to end. Flows that depend on third-party providers, such as social logins with Google or tool connections like HubSpot, normally require you to configure your own OAuth apps and test with real user accounts.

Configuring each provider and managing test accounts is time-consuming. Scalekit credentials let you use provider-specific test accounts that Scalekit manages for you, so you can skip most of the provider setup and focus on your application logic.

![Scalekit-managed credentials: Scalekit holds the OAuth apps for providers such as Google and HubSpot and connects them to your app](/d2/docs/dev-kit/tools/use-scalekit-credentials-0.svg)

Scalekit manages the OAuth apps and test accounts for supported providers. When you enable Scalekit credentials for a connection, Scalekit:

* Uses its own client IDs, secrets, and test accounts for that provider
* Handles the provider-side login or authorization on your behalf
* Returns tokens and user data to your application as if a real user had completed the flow

Your application receives the same type of responses it would receive from a fully configured production integration, but without requiring you to manage provider configuration during development.

## Use Scalekit credentials for agent tool connections

[Section titled “Use Scalekit credentials for agent tool connections”](#use-scalekit-credentials-for-agent-tool-connections)

To use Scalekit credentials for agent tool connections:

* Open **Scalekit Dashboard → Agent tool connections**
* Choose a tool connection (for example, HubSpot)
* Select **Use Scalekit credentials**

The next tool invocation for that connection automatically uses the Scalekit-managed credentials and lets you make tool calls without configuring your own OAuth app or test account.

## Use Scalekit credentials for social connections

[Section titled “Use Scalekit credentials for social connections”](#use-scalekit-credentials-for-social-connections)

To use Scalekit credentials for social login providers:

* Open **Scalekit Dashboard → Authentication → Auth methods → Social login**
* Choose a social provider (for example, Google or Microsoft)
* Select **Use Scalekit credentials**

The next social login for that provider automatically uses the Scalekit-managed credentials and lets you complete login flows without maintaining separate test identities or local OAuth configurations.

![Scalekit dashboard Social Login settings for Google with "Use Scalekit credentials" selected under OAuth Configuration](/.netlify/images?url=_astro%2F01.BGnueJDk.png\&w=1970\&h=915\&dpl=6ae471fc143c9a4c27550463)

Request additional providers

If you need a provider that is not yet available with Scalekit credentials, we add new providers for you. [Reach out to us!](/support/contact-us/)

---
# DOCUMENT BOUNDARY
---

# SCIM provisioning code samples

> Code samples demonstrating SCIM provisioning examples and integration patterns for user and group management

### [Handle SCIM webhooks](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/webhook-events)

[Process SCIM directory updates in Next.js. Example shows how to verify webhook signatures and sync user data](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/webhook-events)

### [Embed admin portal](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample)

[Securely embed the Scalekit Admin Portal via iframe. Node.js example for managing directory sync and organizational settings](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample)

---
# DOCUMENT BOUNDARY
---

# Automatically assign roles

> Automatically assign roles to users in your application by mapping directory provider groups to application roles using Scalekit

Manually assigning roles to users in your application consumes time and creates room for errors for your customers (usually, administrators). Scalekit monitors role changes in connected directories and notifies your application through webhooks. You use the event payload to keep user roles in your application in sync with directory groups in near real time.

## How group-based role assignment works

[Section titled “How group-based role assignment works”](#how-group-based-role-assignment-works)

Organization administrators commonly manage varying access levels by grouping users in their directory. For example, to manage access levels to GitHub, they create groups for each role and assign users to those groups. In this case a **Maintainer** group includes all the users who should have maintainer access to the repository.

![Group-based role assignment: directory groups such as Engineering and Managers map through Scalekit to roles such as Editor and Admin in your app](/d2/docs/directory/guides/group-based-role-assignment-0.svg)

This enables your application to take necessary actions such as creating or modifying user roles as directed by the organization’s administrators.

Note

Scalekit delivers **normalized** information regardless of which directory provider your customers use. This eliminates the need for you to transform data across different providers. Users can belong to multiple groups and may receive multiple roles in your application, depending on how you handle roles.

When an administrator deletes a group from the directory, Scalekit removes that group from every member’s group list and recalculates each member’s role from their remaining group memberships. This happens even for users who belong to multiple mapped groups, so deleting one group never leaves a stale role or membership behind for a user who is still a member of other groups.

## Set up automatic role assignment

[Section titled “Set up automatic role assignment”](#set-up-automatic-role-assignment)

To enable administrators to map directory groups to roles in your app, complete these steps:

1. Open the Scalekit dashboard.
2. Go to **Roles & Permissions**.
3. Use the **Roles** and **Permissions** sections to configure your application’s authorization model.
4. Register your app’s roles and permissions so Scalekit can reference them in mappings and webhook events.

Select **Add role** to create a new role.

Choose clear display names and descriptions for your roles. This helps customers understand and align the roles with the access levels they already maintain in their directory.

![Scalekit roles configuration page showing list of application roles](/.netlify/images?url=_astro%2Fadd-role-page.CgXDutLo.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

The roles page lists a couple of sample roles by default. You can edit or remove these and add new roles that match your application’s authorization model.

![Scalekit roles list showing default and custom roles](/.netlify/images?url=_astro%2Fdefault-roles.DC1cIuRF.png\&w=3200\&h=1612\&dpl=6ae471fc143c9a4c27550463)

Specify the default roles your app wants to assign to the organization creator and to members who belong to the same organization. All added roles are available for you to select as default roles.

![Scalekit default roles configuration for creators and members](/.netlify/images?url=_astro%2Fdefault-roles.DC1cIuRF.png\&w=3200\&h=1612\&dpl=6ae471fc143c9a4c27550463)

### Connect organization groups to app roles

[Section titled “Connect organization groups to app roles”](#connect-organization-groups-to-app-roles)

After you create roles, they represent the roles in your app that you want directory groups to control. Users receive role assignments in your app based on the groups they belong to in their directory.

You can set up this mapping in two ways:

1. Configure mappings in the Scalekit dashboard on behalf of organization administrators. Select the organization and go to the **SCIM provisioning** tab.
2. Share the [admin portal link](/directory/admin-portal/#generate-shareable-portal-link) with organization administrators so they can configure the mappings themselves.

Scalekit automatically displays mapping options in both the Scalekit dashboard and the admin portal. This allows administrators to connect organization groups to app roles without custom logic in your application.

![Mapping directory groups to application roles in Scalekit](/.netlify/images?url=_astro%2F2.CqGIp9Zu.png\&w=2010\&h=1092\&dpl=6ae471fc143c9a4c27550463)

## Handle role update events

[Section titled “Handle role update events”](#handle-role-update-events)

Scalekit continuously monitors updates from your customers’ directory providers and sends event payloads to your application through a registered webhook endpoint. To set up these endpoints and manage subscriptions, use the **Webhooks** option in the Scalekit dashboard.

Listen for the `organization.directory.user_updated` event to determine a user’s roles from the payload. Scalekit automatically includes role information that is relevant to your app, based on the roles you configured in the Scalekit dashboard.

* Node.js

  Create a webhook endpoint for role updates

  ```javascript
  1
  // Webhook endpoint to receive directory role updates
  2
  app.post('/webhook', async (req, res) => {
  3
    // Extract event data from the webhook payload
  4
    const event = req.body;
  5
    const { email, roles } = event.data;
  6


  7
    console.log('Received directory role update for:', email);
  8


  9
    // Extract role_name from the roles array, if present
  10
    const roleName = Array.isArray(roles) && roles.length > 0 ? roles[0].role_name : null;
  11
    console.log('Role name received:', roleName);
  12


  13
    // Business logic: update user role and permissions in your app
  14
    if (roleName) {
  15
      await assignRole(roleName, email);
  16
      console.log('Updated access for user:', email);
  17
    }
  18


  19
    res.status(201).json({
  20
      message: 'Role processed',
  21
    });
  22
  });
  ```

* Python

  Create a webhook endpoint for role updates

  ```python
  1
  import json
  2
  from fastapi import FastAPI, Request
  3
  from fastapi.responses import JSONResponse
  4


  5
  app = FastAPI()
  6


  7


  8
  @app.post("/webhook")
  9
  async def api_webhook(request: Request):
  10
      # Parse request body from the webhook payload
  11
      body = await request.body()
  12
      payload = json.loads(body.decode())
  13


  14
      # Extract user data
  15
      user_roles = payload["data"].get("roles", [])
  16
      user_email = payload["data"].get("email")
  17


  18
      print("User roles:", user_roles)
  19
      print("User email:", user_email)
  20


  21
      # Business logic: assign role in your app
  22
      if user_roles and user_email:
  23
          await assign_role(user_roles[0], user_email)
  24


  25
      return JSONResponse(
  26
          status_code=201,
  27
          content={"message": "Role processed"},
  28
      )
  ```

* Java

  Create a webhook endpoint for role updates

  ```java
  1
  @PostMapping("/webhook")
  2
  public ResponseEntity> webhook(@RequestBody String body, @RequestHeader Map headers) {
  3
    ObjectMapper mapper = new ObjectMapper();
  4


  5
    try {
  6
      JsonNode node = mapper.readTree(body);
  7
      JsonNode roles = node.get("data").get("roles");
  8
      String email = node.get("data").get("email").asText();
  9


  10
      System.out.println("Roles: " + roles);
  11
      System.out.println("Email: " + email);
  12


  13
      // TODO: Add role to user in your application
  14


  15
      Map responseBody = new HashMap<>();
  16
      responseBody.put("message", "Role processed");
  17
      return ResponseEntity.status(HttpStatus.CREATED).body(responseBody);
  18
    } catch (IOException e) {
  19
      return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
  20
    }
  21
  }
  ```

* Go

  Create a webhook endpoint for role updates

  ```go
  1
  mux.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) {
  2
      // Read request body from the webhook payload
  3
      bodyBytes, err := io.ReadAll(r.Body)
  4
      if err != nil {
  5
          http.Error(w, err.Error(), http.StatusBadRequest)
  6
          return
  7
      }
  8


  9
      // Parse webhook payload
  10
      var body struct {
  11
          Data map[string]interface{} `json:"data"`
  12
      }
  13


  14
      if err := json.Unmarshal(bodyBytes, &body); err != nil {
  15
          http.Error(w, err.Error(), http.StatusBadRequest)
  16
          return
  17
      }
  18


  19
      // Extract user data
  20
      roles, _ := body.Data["roles"]
  21
      email, _ := body.Data["email"]
  22


  23
      fmt.Println("Roles:", roles)
  24
      fmt.Println("Email:", email)
  25


  26
      w.WriteHeader(http.StatusCreated)
  27
      _, _ = w.Write([]byte(`{"message":"Role processed"}`))
  28
  })
  ```

Refer to the list of [directory webhook events](/directory/reference/directory-events/) you can subscribe to for more event types.

---
# DOCUMENT BOUNDARY
---

# Production readiness checklist

> A focused checklist for launching your Scalekit SCIM provisioning integration, based on core enterprise authentication launch checks.

As you prepare to launch SCIM provisioning to production, you should confirm that your configuration satisfies the SCIM-specific items from the authentication launch checklist.

This page extracts the SCIM provisioning items from the main authentication [production readiness checklist](/authenticate/launch-checklist/) and organizes them for your directory rollout.

**Verify production environment configuration**

Confirm that your environment URL (`SCALEKIT_ENVIRONMENT_URL`), client ID (`SCALEKIT_CLIENT_ID`), and client secret (`SCALEKIT_CLIENT_SECRET`) are correctly configured for your production environment and match your production Scalekit dashboard settings.

**Configure SCIM webhook endpoints**

Configure webhook endpoints to receive SCIM events in your production environment, and ensure they use HTTPS and correct domain configuration.

**Verify webhook security with signature validation**

Implement signature validation for incoming SCIM webhooks so only Scalekit can trigger provisioning changes. See [webhook best practices](/guides/webhooks-best-practices/) for guidance.

**Test user provisioning, updates, and deprovisioning**

Test user provisioning flows (create), deprovisioning flows (deactivate or delete), and user profile updates to ensure your application responds correctly to each event type.

**Validate group-based role assignment**

Set up group-based role assignment and synchronization, and verify that group membership changes in the identity provider correctly map to roles and permissions in your application.

**Handle duplicate and invalid data scenarios**

Test error scenarios such as duplicate users, conflicting identifiers, and invalid data payloads so your integration fails safely and surfaces actionable errors.

**Align SCIM with user and organization models**

Confirm that your SCIM implementation matches your user and organization data model, including how you represent organizations, teams, and role assignments in your system.

**Finalize admin portal configuration for directory admins**

Ensure directory admins can configure SCIM connections in the admin portal, and that your branding and access controls are correct for enterprise customers.

---
# DOCUMENT BOUNDARY
---

# Onboard enterprise customers

> Complete workflow for enabling SCIM provisioning and self-serve directory sync configuration for your enterprise customers

Enterprise provisioning with SCIM enables you to automatically create, update, and deactivate users in your application based on changes in your customers’ directory providers such as Okta, Microsoft Entra ID, or Google Workspace. This gives enterprise customers centralized user lifecycle management while reducing manual administration and access drift.

![How Scalekit connects your application to enterprise directories and identity providers](/.netlify/images?url=_astro%2Fhow-scalekit-connects.CrZX8E30.png\&w=5776\&h=1924\&dpl=6ae471fc143c9a4c27550463)

This guide walks you through the complete workflow for onboarding enterprise customers with SCIM provisioning. You’ll learn how to create organizations, provide admin portal access, enable directory sync, and verify that provisioning works end to end.

Before onboarding enterprise customers with provisioning, ensure you have completed the [SCIM quickstart](/directory/scim/quickstart/) to set up basic directory sync in your application.

## Table of contents

* [Create organization](#create-organization)
* [Provide admin portal access](#provide-admin-portal-access)
* [Customer configures SCIM provisioning](#customer-configures-scim-provisioning)
* [Verify provisioning and run test sync](#verify-provisioning-and-run-test-sync)



1. ## Create organization

   Create an organization in Scalekit to represent your enterprise customer:

   * Log in to the [Scalekit dashboard](https://app.scalekit.com)
   * Navigate to **Dashboard > Organizations**
   * Click **Create Organization**
   * Enter the organization name and relevant details
   * Save the organization

   Each organization in Scalekit represents one of your enterprise customers and can have its own directory sync settings, SSO configuration, and domain associations.

2. ## Provide admin portal access

   Give your customer’s IT administrator access to the self-serve admin portal to configure their directory and SCIM connection. Scalekit provides two integration methods:

   **Option 1: Share a no-code link** Quick setup

   Generate and share a link to the admin portal:

   * Select the organization from **Dashboard > Organizations**
   * Click **Generate link** in the organization overview
   * Share the link with your customer’s IT admin via email, Slack, or your preferred channel

   The link remains valid for 7 days and can be revoked anytime from the dashboard.

   **Link properties:**

   | Property       | Details                                                                         |
   | -------------- | ------------------------------------------------------------------------------- |
   | **Expiration** | Links expire after 7 days                                                       |
   | **Revocation** | Revoke links anytime from the dashboard                                         |
   | **Sharing**    | Share via email, Slack, or any preferred channel                                |
   | **Security**   | Anyone with the link can view and update the organization’s connection settings |

   The generated link follows this format:

   Portal link example

   ```http
   https://your-app.scalekit.dev/magicLink/2cbe56de-eec4-41d2-abed-90a5b82286c4_p
   ```

   Security consideration

   Treat portal links as sensitive credentials. Anyone with the link can view and modify the organization’s SSO and SCIM configuration.

   **Option 2: Embed the portal** Seamless experience

   Embed the admin portal directly in your application so customers can configure SCIM provisioning and SSO without leaving your interface. The portal link must be generated programmatically on each page load for security. Each generated link is single-use and expires after 1 minute, though once loaded, the session remains active for up to 6 hours.

   * Node.js

     ```bash
     npm install @scalekit-sdk/node
     ```

   * Python

     ```sh
     pip install scalekit-sdk-python
     ```

   * Go

     ```sh
     go get -u github.com/scalekit-inc/scalekit-sdk-go
     ```

   * Java

     ```groovy
     /* Gradle users - add the following to your dependencies in build file */
     implementation "com.scalekit:scalekit-sdk-java:2.1.3"
     ```

     ```xml
     
     
         com.scalekit
         scalekit-sdk-java
         2.1.3
     
     ```

   ### Generate portal link

   Use the Scalekit SDK to generate a unique, embeddable admin portal link for an organization. Call this API endpoint each time you render the page containing the iframe:

   * Node.js

     Express.js

     ```javascript
     6 collapsed lines
     import { Scalekit } from '@scalekit-sdk/node';


     const scalekit = new Scalekit(
       process.env.SCALEKIT_ENVIRONMENT_URL,
       process.env.SCALEKIT_CLIENT_ID,
       process.env.SCALEKIT_CLIENT_SECRET,
     );


     async function generatePortalLink(organizationId) {
       const link = await scalekit.organization.generatePortalLink(organizationId);
       return link.location; // Use as iframe src
     }
     ```

   * Python

     Flask

     ```python
     6 collapsed lines
     from scalekit import Scalekit
     import os


     scalekit_client = Scalekit(
         environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"),
         client_id=os.environ.get("SCALEKIT_CLIENT_ID"),
         client_secret=os.environ.get("SCALEKIT_CLIENT_SECRET")
     )


     def generate_portal_link(organization_id):
         link = scalekit_client.organization.generate_portal_link(organization_id)
         return link.location  # Use as iframe src
     ```

   * Go

     Gin

     ```go
     10 collapsed lines
     1
     import (
     2
         "context"
     3
         "os"
     4


     5
         "github.com/scalekit/sdk-go"
     6
     )
     7


     8
     scalekitClient := scalekit.New(
     9
         os.Getenv("SCALEKIT_ENVIRONMENT_URL"),
     10
         os.Getenv("SCALEKIT_CLIENT_ID"),
     11
         os.Getenv("SCALEKIT_CLIENT_SECRET"),
     12
     )
     13


     14
     func generatePortalLink(organizationID string) (string, error) {
     15
         ctx := context.Background()
     16
         link, err := scalekitClient.Organization().GeneratePortalLink(ctx, organizationID)
     17
         if err != nil {
     18
             return "", err
     19
         }
     20
         return link.Location, nil  // Use as iframe src
     21
     }
     ```

   * Java

     Spring Boot

     ```java
     8 collapsed lines
     1
     import com.scalekit.client.Scalekit;
     2
     import com.scalekit.client.models.Link;
     3
     import com.scalekit.client.models.Feature;
     4
     import java.util.Arrays;
     5


     6
     Scalekit scalekitClient = new Scalekit(
     7
         System.getenv("SCALEKIT_ENVIRONMENT_URL"),
     8
         System.getenv("SCALEKIT_CLIENT_ID"),
     9
         System.getenv("SCALEKIT_CLIENT_SECRET")
     10
     );
     11


     12
     public String generatePortalLink(String organizationId) {
     13
         Link portalLink = scalekitClient.organizations()
     14
             .generatePortalLink(organizationId, Arrays.asList(Feature.sso, Feature.dir_sync));
     15
         return portalLink.getLocation();  // Use as iframe src
     16
     }
     ```

   The API returns a JSON object with the portal link. Use the `location` property as the iframe `src`:

   API response

   ```json
   {
     "id": "8930509d-68cf-4e2c-8c6d-94d2b5e2db43",
     "location": "https://random-subdomain.scalekit.dev/magicLink/8930509d-68cf-4e2c-8c6d-94d2b5e2db43",
     "expireTime": "2024-10-03T13:35:50.563013Z"
   }
   ```

   Embed portal in iframe

   ```html
   
   ```

   Embed the portal in your application’s settings or admin section where customers manage authentication configuration.

   Listen for UI events from the embedded portal to respond to configuration changes, such as when directory sync is enabled, provisioning is tested, or the session expires. See the [Admin portal UI events reference](/reference/admin-portal/ui-events/) for details on handling these events.

   ### Configuration and session

   | Setting               | Requirement                                                                   |
   | --------------------- | ----------------------------------------------------------------------------- |
   | **Redirect URI**      | Add your application domain at **Dashboard > Developers > API Configuration** |
   | **iframe attributes** | Include `allow="clipboard-write"` for copy-paste functionality                |
   | **Dimensions**        | Minimum recommended height: 600px                                             |
   | **Link expiration**   | Generated links expire after 1 minute if not loaded                           |
   | **Session duration**  | Portal session remains active for up to 6 hours once loaded                   |
   | **Single-use**        | Each generated link can only be used once to initialize a session             |

   Generate fresh links

   Generate a new portal link on each page load rather than caching the URL. This ensures security and prevents expired link errors.

3. ## Customer configures SCIM provisioning

   After receiving admin portal access, your customer’s IT administrator:

   * Opens the admin portal (via shared link or embedded iframe)
   * Selects their directory integration (Okta, Microsoft Entra ID, Google Workspace, etc.)
   * Follows the provider-specific SCIM or directory sync setup guide
   * Enters the required configuration (SCIM endpoint URL, access token, and any required headers)
   * Tests user provisioning from their directory to your application
   * Activates the SCIM connection

   Once configured, the directory sync or SCIM connection appears as active in your organization’s settings.

   SCIM configuration guides

   Share the appropriate [SCIM integration guide](/guides/integrations/scim-integrations/) with your customer’s IT team to help them configure their directory correctly.

4. ## Verify provisioning and run test sync

   After SCIM provisioning is configured, verify that user and group changes flow correctly from the customer’s directory into your application. This ensures your enterprise onboarding is reliable before rolling out broadly.

   To verify provisioning:

   * Create a test user in the customer’s directory and assign them to the appropriate groups or applications
   * Confirm that the user appears in your application’s organization with the expected attributes (name, email, roles, and status)
   * Update the user’s attributes or group memberships in the directory and verify that changes propagate to your application
   * Deactivate or delete the test user in the directory and ensure their access is revoked in your application

   Home realm discovery and SSO (optional)

   You can optionally combine SCIM provisioning with SSO and domain verification so that users are both automatically provisioned and routed to the correct identity provider at sign-in. See the SSO onboarding guides if you want to add SSO on top of SCIM.

## Customize the admin portal

Match the admin portal to your brand identity. Configure branding at **Dashboard > Settings > Branding**:

| Option           | Description                                               |
| ---------------- | --------------------------------------------------------- |
| **Logo**         | Upload your company logo (displayed in the portal header) |
| **Accent color** | Set the primary color to match your brand palette         |
| **Favicon**      | Provide a custom favicon for browser tabs                 |

Branding scope

Branding changes apply globally to all portal instances (both shareable links and embedded iframes) in your environment.

For additional customization options including custom domains, see the [Custom domain guide](/guides/custom-domain/).

---
# DOCUMENT BOUNDARY
---

# Review SCIM protocol

> Learn about core components, resources, schemas, and real-world implementation scenarios for identity management across cloud applications through SCIM

System for Cross-domain Identity Management (SCIM) is an [open standard API specification](https://datatracker.ietf.org/doc/html/rfc7643#section-2) designed to manage identities across cloud applications easily and scalably. The specification suite builds upon experience with existing schemas and deployments, emphasizing:

* Simplicity of development and integration
* Application of existing authentication, authorization, and privacy models

Its intent is to reduce the cost and complexity of user management operations by providing:

* A common user schema
* An extension model; e.g., enterprise user
* Binding documents to provide patterns for exchanging this schema using HTTP

## SCIM protocol: Key components

[Section titled “SCIM protocol: Key components”](#scim-protocol-key-components)

SCIM is a HTTP based protocol and uses structured [JSON](https://datatracker.ietf.org/doc/html/rfc7159) payloads to exchange resource information between the SCIM client and service provider. To identify the SCIM protocol resources, the `application/scim+json` media type is used.

### SCIM service provider

[Section titled “SCIM service provider”](#scim-service-provider)

SCIM service provider is any business application that provisions users and groups by synchronizing the changes made in a SCIM client, including creates, updates, and deletes. The synchronization enables end users to have seamless access to the business application for which they’re assigned, with up-to-date profiles and permissions.

Scalekit acts as the SCIM service provider on your behalf and integrates with your customer’s identity providers or directory providers (e.g. Okta, Azure AD, Google Workspace, etc.) to provision users and groups.

### SCIM client

[Section titled “SCIM client”](#scim-client)

SCIM client facilitates provisioning, or managing user lifecycle events, through SCIM endpoints exposed by the SCIM service provider. Identity providers and HRMS act as very popular SCIM clients as they are treated as the source of truth for user identity data.

Some of the most common SCIM clients are [Okta](https://www.okta.com), [Microsoft Entra ID (aka Azure AD)](https://www.microsoft.com/en-in/security/business/identity-access/microsoft-entra-id).

### SCIM endpoints

[Section titled “SCIM endpoints”](#scim-endpoints)

SCIM endpoints are the entry points to the SCIM API. They are the endpoints that the SCIM client will call to provision users and groups.

The following are the most popular SCIM endpoints that any SCIM service provider should support:

* `/Users`
* `/Groups`

### SCIM methods

[Section titled “SCIM methods”](#scim-methods)

As SCIM is built on top of REST, SCIM methods are the HTTP methods that are used to perform CRUD operations on the SCIM resources. The following are the most common SCIM methods:

* GET
* POST
* PUT
* PATCH
* DELETE

### SCIM authentication

[Section titled “SCIM authentication”](#scim-authentication)

SCIM uses OAuth 2.0 bearer token authentication to authenticate requests to the SCIM API. The token is a string that is used to authenticate the SCIM API requests to the SCIM service provider. The token is passed in the HTTP Authorization header using the Bearer scheme.

## SCIM resources

[Section titled “SCIM resources”](#scim-resources)

SCIM resources are the core building blocks of the SCIM protocol. They represent entities such as users, groups, and organizational units. Each resource has a set of attributes that describe the entity.

While SCIM user resource has the basic attributes of a user like email address, phone number, and name, it is extensible by defining new JSON schemas that a service provider can choose to implement. An enterprise user is an example of a SCIM user extension resource. Enterprise user resource has attributes such as employee number, department, and manager which are valuable for enterprise implementation of user management using SCIM v2.

Example SCIM user representation

```json
1
{
2
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
3
  "userName": "bjensen",
4
  "name": {
5
    "givenName": "Barbara",
6
    "familyName": "Jensen"
7
  },
8
  "emails": [
9
    {
10
      "value": "bjensen@example.com",
11
      "type": "work",
12
      "primary": true
13
    }
14
  ],
15
  "entitlements": [
16
    {
17
      "value": "Employee",
18
      "type": "role"
19
    }
20
  ]
21
}
```

### SCIM schema

[Section titled “SCIM schema”](#scim-schema)

SCIM schema is the core of the SCIM protocol. It is a JSON schema that defines the structure of the SCIM resources. The following are the most common SCIM schemas:

* [Core SCIM user schema](https://datatracker.ietf.org/doc/html/rfc7643#section-4.1)
* [Enterprise user schema](https://datatracker.ietf.org/doc/html/rfc7643#section-4.3)
* [Group schema](https://datatracker.ietf.org/doc/html/rfc7643#section-4.2)

## Putting everything together

[Section titled “Putting everything together”](#putting-everything-together)

Now that you have a high level understanding of the SCIM protocol and different components involved, let’s put everything together to take a scenario of how SCIM protocol facilitates user provisioning from an identity provider to a SCIM service provider like Scalekit.

### Scenario: New employee onboarding

[Section titled “Scenario: New employee onboarding”](#scenario-new-employee-onboarding)

1. ACME Inc. hires a new employee, John Doe.
2. ACME Inc. adds John Doe to their Okta directory.
3. Okta send a SCIM `POST /Users` request to a pre-registered SCIM service provider (your B2B application) with John Doe’s information as per the SCIM protocol.
4. You authenticate the request using the OAuth 2.0 bearer token authentication & validate the request payload.
5. You provision John Doe as a new user in your B2B application using the user payload.

### Scenario: Employee termination

[Section titled “Scenario: Employee termination”](#scenario-employee-termination)

1. ACME Inc. terminates John Doe’s employment.
2. ACME Inc. removes John Doe from their Okta directory.
3. Okta send a SCIM `DELETE /Users/john.doe` request to a pre-registered SCIM service provider (your B2B application) as per the SCIM protocol.
4. You authenticate the request using the OAuth 2.0 bearer token authentication & validate the request payload.
5. You deactivate John Doe as an existing user in your B2B application using the user payload.

### Scenario: Employee transfer

[Section titled “Scenario: Employee transfer”](#scenario-employee-transfer)

1. ACME Inc. transfers John Doe to a different department.
2. ACME Inc. updates John Doe’s information in their Okta directory.
3. Okta send a SCIM `PATCH /Users/john.doe` request to a pre-registered SCIM service provider (your B2B application) as per the SCIM protocol.
4. You authenticate the request using the OAuth 2.0 bearer token authentication & validate the request payload.
5. You update John Doe’s information in your B2B application using the user payload.

SCIM create user request

```http
1
POST /Users HTTP/1.1
2
Host: yourapp.scalekit.com/directory/dir_12442/scim/v2
3
Accept: application/scim+json
4
Content-Type: application/scim+json
5
Authorization: Bearer YOUR_SCIM_API_TOKEN
6


7
{
8
    "schemas":["urn:ietf:params:scim:schemas:core:2.0:User"],
9
    "userName":"bjensen",
10
    "externalId":"bjensen",
11
    "name":{
12
        "formatted":"Ms. Barbara J Jensen III",
13
        "familyName":"Jensen",
14
        "givenName":"Barbara"
15
    }
16
}
```

## Scalekit’s SCIM implementation

[Section titled “Scalekit’s SCIM implementation”](#scalekits-scim-implementation)

Scalekit’s SCIM implementation is built upon the principles of simplicity, security, and scalability. It provides a normalized implementation of the SCIM protocol across different identity providers & directory providers. This allows you to focus on integrating with Scalekit’s API & leave the complexities of SCIM protocol implementation to us. While not all directory providers implement SCIM or support all SCIM features, Scalekit aims to abstract these complexities & provide a seamless experience for provisioning users and groups.

### Webhooks

[Section titled “Webhooks”](#webhooks)

Scalekit supports webhooks as a mechanism to send real-time updates to your application about user provisioning and deprovisioning events to your application as and when there are changes detected in your customer’s SCIM compliant directory providers.

We also normalize the webhook payloads across different directory providers to ensure that you can focus on building your application without having to worry about the nuances of each directory provider’s SCIM implementations.

Tip

Refer to the [webhooks guide](/authenticate/implement-workflows/implement-webhooks/) and [directory webhook events](/directory/reference/directory-events/) to learn more on how you can use webhooks to listen for changes in the directory and update the user’s roles in your application.

---
# DOCUMENT BOUNDARY
---

# Understanding SCIM Provisioning

> The business case for implementing SCIM

Scaling organizations utilize a growing array of applications to support their employees’ productivity. To efficiently and securely manage access to these applications, organization administrators employ Directory Providers. These providers automate crucial workflows, such as granting access to new employees or revoking access for departing staff.

Directory providers, like Entra ID (formerly Azure Active Directory), serve as the authoritative source for user information and access rights. Organizations expect your application to accommodate their directory provider requirements. Consequently, you must design systems capable of interfacing with various directory providers used by their customers.

Scalekit serves as an intermediary component in your B2B application architecture, providing a streamlined interface to access user information programmatically and in real-time.

![User onboarding flow across your app, Scalekit, and directory providers](/.netlify/images?url=_astro%2Fbasics.BBrrKGoZ.png\&w=4260\&h=2200\&dpl=6ae471fc143c9a4c27550463)

This solution allows your application to:

1. Automatically determine user roles (e.g., admin, member)
2. Retrieve user access permissions
3. Tailor the user experience accordingly and securely

By integrating Scalekit, you can meet enterprise requirements without diverting focus from your core product development. This approach significantly reduces the engineering effort and time typically required to implement compatibility with various directory providers.

Explore the compelling reasons to implement SCIM Provisioning in your B2B SaaS app:

Tip

* Automates user lifecycle management, eliminating the need for manual user creation, updates, and deletions. This reduces administrative overhead and the potential for human error.
* Enhances security by ensuring prompt revocation of user access when employees leave an organization.
* Improves user experience by allowing new employees to gain immediate access to necessary applications without waiting for manual account creation. This leads to a smoother onboarding process.
* Reduces IT workload by eliminating the need for IT administrators to manually manage user accounts across multiple systems. This frees up time for more strategic tasks.
* Ensures user information consistency across the identity provider (IdP) and the B2B application, reducing discrepancies and potential security risks.
* Scales to handle increased user numbers as organizations grow, without requiring additional manual effort.
* Helps organizations meet various compliance requirements related to user access and data protection by maintaining accurate and up-to-date user records.
* Allows for mapping of custom attributes via SCIM, enabling B2B applications to sync specialized user data that may be unique to their use case.

Implementing SCIM allows you to offer a more attractive, enterprise-grade solution.

## Next steps

[Section titled “Next steps”](#next-steps)

Now that you understand the importance of directories and how implementing SCIM Provisioning can step up your app to enterprise-grade status, it’s time to put this knowledge into action. Here are some suggested next steps:

1. Dive into our [Quickstart](/directory/scim/quickstart/) guide to learn how to set up SCIM Provisioning for your app. This practical guide will walk you through the implementation process step-by-step.
2. Start small by simulating directory events. This hands-on approach allows you to test and familiarize yourself with the system without affecting live data.
3. Explore our sample apps to picture all the moving components in a typical app.

Note

Take it one step at a time, and don’t hesitate to refer back to our documentation as you progress. Your efforts will result in a more secure, efficient, and attractive solution for your enterprise customers. Happy syncing!

---
# DOCUMENT BOUNDARY
---

# Directory events

> Explore webhook events related to directory operations in Scalekit, including user and group creation, updates, and deletions.

This page documents the webhook events Scalekit sends when an organization’s directory, its users or its groups change.

## Table of contents

* [Event envelope](#event-envelope)

* [Directory connection events](#directory-connection-events)

  * [`organization.directory_created`](#organizationdirectory_created)
  * [`organization.directory_enabled`](#organizationdirectory_enabled)
  * [`organization.directory_disabled`](#organizationdirectory_disabled)
  * [`organization.directory_deleted`](#organizationdirectory_deleted)

* [Directory user events](#directory-user-events)

  * [`organization.directory.user_created`](#organizationdirectoryuser_created)
  * [`organization.directory.user_updated`](#organizationdirectoryuser_updated)
  * [`organization.directory.user_deleted`](#organizationdirectoryuser_deleted)

* [Directory group events](#directory-group-events)

  * [`organization.directory.group_created`](#organizationdirectorygroup_created)
  * [`organization.directory.group_updated`](#organizationdirectorygroup_updated)
  * [`organization.directory.group_deleted`](#organizationdirectorygroup_deleted)

***

## Event envelope

Every directory event has the same top-level fields. The event-specific fields are in `data`. Keys can arrive in any order, so parse the body as JSON rather than matching it as text.

| Field             | Type   | Description                                                                  |
| ----------------- | ------ | ---------------------------------------------------------------------------- |
| `spec_version`    | string | Webhook payload version. Currently `"1"`                                     |
| `id`              | string | Unique event ID, prefixed with `evt_`. Use it to ignore duplicate deliveries |
| `type`            | string | Event type, such as `organization.directory.user_created`                    |
| `occurred_at`     | string | When the event occurred, in RFC 3339 format                                  |
| `environment_id`  | string | Environment the event belongs to                                             |
| `organization_id` | string | Organization the directory belongs to                                        |
| `object`          | string | `Directory`, `DirectoryUser` or `DirectoryGroup`                             |
| `data`            | object | The directory, user or group. See each event below                           |

## Directory connection events

### `organization.directory_created`

Sent when a directory is created for an organization. A SCIM or LDAP directory is enabled on creation, so `organization.directory_enabled` follows. A POLL directory is created disabled and sends only this event. `data` has four fields only.

organization.directory_created

```json
{
  "spec_version": "1",
  "id": "evt_55136848686612000",
  "type": "organization.directory_created",
  "occurred_at": "2025-01-15T08:55:22.702860294Z",
  "environment_id": "env_27758032200925221",
  "organization_id": "org_55135410258444802",
  "object": "Directory",
  "data": {
    "directory_type": "SCIM",
    "id": "dir_55135622825771522",
    "organization_id": "org_55135410258444802",
    "provider": "OKTA"
  }
}
```

| Field             | Type   | Description                             |
| ----------------- | ------ | --------------------------------------- |
| `id`              | string | Directory ID, prefixed with `dir_`      |
| `organization_id` | string | Organization that owns the directory    |
| `directory_type`  | string | How the directory syncs, such as `SCIM` |
| `provider`        | string | Directory provider, such as `OKTA`      |

### `organization.directory_enabled`

Sent when directory sync is enabled for an organization.

organization.directory_enabled

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_55136848686613000",
4
  "type": "organization.directory_enabled",
5
  "occurred_at": "2025-01-15T08:55:22.802860294Z",
6
  "environment_id": "env_27758032200925221",
7
  "organization_id": "org_55135410258444802",
8
  "object": "Directory",
9
  "data": {
10
    "directory_type": "SCIM",
11
    "enabled": true,
12
    "id": "dir_55135622825771522",
13
    "organization_id": "org_55135410258444802",
14
    "provider": "OKTA",
15
    "updated_at": "2025-01-15T08:55:22.792993454Z"
16
  }
17
}
```

| Field             | Type    | Description                             |
| ----------------- | ------- | --------------------------------------- |
| `id`              | string  | Directory ID, prefixed with `dir_`      |
| `organization_id` | string  | Organization that owns the directory    |
| `directory_type`  | string  | How the directory syncs, such as `SCIM` |
| `provider`        | string  | Directory provider, such as `OKTA`      |
| `enabled`         | boolean | Always `true` on this event             |
| `updated_at`      | string  | When the directory was last updated     |

### `organization.directory_disabled`

Sent when directory sync is disabled for an organization. `data` has four fields only.

organization.directory_disabled

```json
{
  "spec_version": "1",
  "id": "evt_53891640779079756",
  "type": "organization.directory_disabled",
  "occurred_at": "2025-01-06T18:45:21.057814Z",
  "environment_id": "env_53814739859406915",
  "organization_id": "org_53879494091473415",
  "object": "Directory",
  "data": {
    "directory_type": "SCIM",
    "id": "dir_53879621145330183",
    "organization_id": "org_53879494091473415",
    "provider": "OKTA"
  }
}
```

| Field             | Type   | Description                             |
| ----------------- | ------ | --------------------------------------- |
| `id`              | string | Directory ID, prefixed with `dir_`      |
| `organization_id` | string | Organization that owns the directory    |
| `directory_type`  | string | How the directory syncs, such as `SCIM` |
| `provider`        | string | Directory provider, such as `OKTA`      |

### `organization.directory_deleted`

Sent when a directory is deleted. Deleting a directory through the API or dashboard requires disabling it first. Deleting an organization also deletes its directories, enabled or not, and sends this event for each. `data` has four fields only.

organization.directory_deleted

```json
{
  "spec_version": "1",
  "id": "evt_53891640779079900",
  "type": "organization.directory_deleted",
  "occurred_at": "2025-01-06T18:50:02.118243Z",
  "environment_id": "env_53814739859406915",
  "organization_id": "org_53879494091473415",
  "object": "Directory",
  "data": {
    "directory_type": "SCIM",
    "id": "dir_53879621145330183",
    "organization_id": "org_53879494091473415",
    "provider": "OKTA"
  }
}
```

| Field             | Type   | Description                             |
| ----------------- | ------ | --------------------------------------- |
| `id`              | string | Directory ID, prefixed with `dir_`      |
| `organization_id` | string | Organization that owns the directory    |
| `directory_type`  | string | How the directory syncs, such as `SCIM` |
| `provider`        | string | Directory provider, such as `OKTA`      |

## Directory user events

All three user events send the same user object in `data`.

* `organization.directory.user_created` is sent when the directory provider creates an active user, or reactivates a deactivated user.
* `organization.directory.user_updated` is sent when an active user’s attributes change, or when the user is added to or removed from a group.
* `organization.directory.user_deleted` is sent when an active user is deactivated or deleted in the directory provider. `data` carries the last known user record. `active` is `false` when the user was deactivated.

Changes to a user who is already inactive send no event.

### Directory user fields

The first seven fields are always sent. `roles` is computed by Scalekit and is sent on every user event when your environment has roles. The rest are sent only when the directory provider supplies them.

| Field                | Type           | Description                                                                                                                                                               |
| -------------------- | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`                 | string         | Directory user ID, prefixed with `diruser_`                                                                                                                               |
| `organization_id`    | string         | Organization the user belongs to                                                                                                                                          |
| `active`             | boolean        | Whether the user is active in the directory provider                                                                                                                      |
| `preferred_username` | string         | The user’s `userName` from the provider. Empty string if not set                                                                                                          |
| `dp_id`              | string         | The user’s `externalId` from the provider. Empty string if not set                                                                                                        |
| `email`              | string or null | Primary email address. `null` if the provider marks no email as primary                                                                                                   |
| `groups`             | array or null  | Groups the user belongs to, each with `id` and `name`. `null` if the user is in no groups                                                                                 |
| `name`               | string         | Display name                                                                                                                                                              |
| `given_name`         | string         | Given (first) name                                                                                                                                                        |
| `family_name`        | string         | Family (last) name                                                                                                                                                        |
| `nickname`           | string         | Nickname                                                                                                                                                                  |
| `picture`            | string         | Profile picture URL                                                                                                                                                       |
| `phone_number`       | string         | Work phone number                                                                                                                                                         |
| `address`            | object         | The user’s SCIM address of type `work`: `street_address`, `locality`, `state` (from SCIM `region`), `postal_code`, `country` and `formatted`                              |
| `profile`            | string         | Profile URL                                                                                                                                                               |
| `locale`             | string         | Locale                                                                                                                                                                    |
| `zoneinfo`           | string         | Time zone                                                                                                                                                                 |
| `language`           | string         | Preferred language                                                                                                                                                        |
| `title`              | string         | Job title                                                                                                                                                                 |
| `user_type`          | string         | User type                                                                                                                                                                 |
| `employee_id`        | string         | Employee number from the SCIM enterprise extension                                                                                                                        |
| `cost_center`        | string         | Cost center from the enterprise extension                                                                                                                                 |
| `organization`       | string         | Organization from the enterprise extension                                                                                                                                |
| `division`           | string         | Division from the enterprise extension                                                                                                                                    |
| `department`         | string         | Department from the enterprise extension                                                                                                                                  |
| `manager`            | object         | The user’s manager, with `id`, `email` and `user_name`, when the manager is also in the directory                                                                         |
| `roles`              | array          | Scalekit roles assigned through [group-based role assignment](/directory/guides/group-based-role-assignment/), each with `role_name`. An empty array when no role applies |
| `dp_roles`           | array or null  | Roles as sent by the provider, each with `display`, `primary`, `type` and `value`. `null` if the provider sends an empty list                                             |
| `entitlements`       | array or null  | Entitlements as sent by the provider, each with `display`, `primary`, `type` and `value`. `null` if the provider sends an empty list                                      |
| `custom_attributes`  | object         | Custom attributes you mapped for this directory, plus `home_phone_number` and `mobile_phone_number` when the provider sends them                                          |
| `raw_attributes`     | object         | The SCIM user resource as received from the provider                                                                                                                      |

### `organization.directory.user_created`

organization.directory.user_created

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_53891546994442316",
4
  "type": "organization.directory.user_created",
5
  "occurred_at": "2025-01-06T18:44:25.153954Z",
6
  "environment_id": "env_53814739859406915",
7
  "organization_id": "org_53879494091473415",
8
  "object": "DirectoryUser",
9
  "data": {
10
    "active": true,
11
    "custom_attributes": {
12
      "mobile_phone_number": "+1-555-0101"
13
    },
14
    "department": "Engineering",
15
    "dp_id": "00u1a2b3c4d5e6f7g8h9",
16
    "email": "john.doe@acmecorp.com",
17
    "employee_id": "E1234",
18
    "family_name": "Doe",
19
    "given_name": "John",
20
    "groups": [
21
      {
22
        "id": "dirgroup_53891546960887900",
23
        "name": "Engineering"
24
      }
25
    ],
26
    "id": "diruser_53891546960887884",
27
    "locale": "en-US",
28
    "name": "John Doe",
29
    "organization_id": "org_53879494091473415",
30
    "phone_number": "+1-555-0100",
31
    "preferred_username": "john.doe@acmecorp.com",
32
    "raw_attributes": {
33
      "active": true,
34
      "externalId": "00u1a2b3c4d5e6f7g8h9",
35
      "userName": "john.doe@acmecorp.com"
36
    },
37
    "roles": [
38
      {
39
        "role_name": "billing_admin"
40
      }
41
    ],
42
    "title": "Software Engineer",
43
    "zoneinfo": "America/New_York"
44
  }
45
}
```

### `organization.directory.user_updated`

organization.directory.user_updated

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_53891546994442317",
4
  "type": "organization.directory.user_updated",
5
  "occurred_at": "2025-01-06T18:44:25.153954Z",
6
  "environment_id": "env_53814739859406915",
7
  "organization_id": "org_53879494091473415",
8
  "object": "DirectoryUser",
9
  "data": {
10
    "active": true,
11
    "custom_attributes": {
12
      "mobile_phone_number": "+1-555-0101"
13
    },
14
    "department": "Engineering",
15
    "dp_id": "00u1a2b3c4d5e6f7g8h9",
16
    "email": "john.doe@acmecorp.com",
17
    "employee_id": "E1234",
18
    "family_name": "Doe",
19
    "given_name": "John",
20
    "groups": [
21
      {
22
        "id": "dirgroup_53891546960887900",
23
        "name": "Engineering"
24
      }
25
    ],
26
    "id": "diruser_53891546960887884",
27
    "locale": "en-US",
28
    "name": "John Doe",
29
    "organization_id": "org_53879494091473415",
30
    "phone_number": "+1-555-0100",
31
    "preferred_username": "john.doe@acmecorp.com",
32
    "raw_attributes": {
33
      "active": true,
34
      "externalId": "00u1a2b3c4d5e6f7g8h9",
35
      "userName": "john.doe@acmecorp.com"
36
    },
37
    "roles": [
38
      {
39
        "role_name": "billing_admin"
40
      }
41
    ],
42
    "title": "Senior Software Engineer",
43
    "zoneinfo": "America/New_York"
44
  }
45
}
```

### `organization.directory.user_deleted`

This example is a deactivated user, so `active` is `false`.

organization.directory.user_deleted

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_53891546994442318",
4
  "type": "organization.directory.user_deleted",
5
  "occurred_at": "2025-01-06T18:44:25.153954Z",
6
  "environment_id": "env_53814739859406915",
7
  "organization_id": "org_53879494091473415",
8
  "object": "DirectoryUser",
9
  "data": {
10
    "active": false,
11
    "custom_attributes": {
12
      "mobile_phone_number": "+1-555-0101"
13
    },
14
    "department": "Engineering",
15
    "dp_id": "00u1a2b3c4d5e6f7g8h9",
16
    "email": "john.doe@acmecorp.com",
17
    "employee_id": "E1234",
18
    "family_name": "Doe",
19
    "given_name": "John",
20
    "groups": [
21
      {
22
        "id": "dirgroup_53891546960887900",
23
        "name": "Engineering"
24
      }
25
    ],
26
    "id": "diruser_53891546960887884",
27
    "locale": "en-US",
28
    "name": "John Doe",
29
    "organization_id": "org_53879494091473415",
30
    "phone_number": "+1-555-0100",
31
    "preferred_username": "john.doe@acmecorp.com",
32
    "raw_attributes": {
33
      "active": false,
34
      "externalId": "00u1a2b3c4d5e6f7g8h9",
35
      "userName": "john.doe@acmecorp.com"
36
    },
37
    "roles": [
38
      {
39
        "role_name": "billing_admin"
40
      }
41
    ],
42
    "title": "Software Engineer",
43
    "zoneinfo": "America/New_York"
44
  }
45
}
```

## Directory group events

All three group events send the same group object in `data`. Every key is sent, even when its value is `null`.

| Field             | Type           | Description                                                                         |
| ----------------- | -------------- | ----------------------------------------------------------------------------------- |
| `id`              | string         | Directory group ID, prefixed with `dirgroup_`                                       |
| `organization_id` | string         | Organization the group belongs to                                                   |
| `directory_id`    | string         | Directory the group belongs to                                                      |
| `display_name`    | string         | Group display name                                                                  |
| `dp_id`           | string or null | The group’s `externalId` from the provider. `null` if the provider doesn’t send one |
| `raw_attributes`  | object or null | The SCIM group resource as received from the provider                               |

### `organization.directory.group_created`

organization.directory.group_created

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_38862741515010639",
4
  "type": "organization.directory.group_created",
5
  "occurred_at": "2024-09-25T02:26:39.036398577Z",
6
  "environment_id": "env_32080745237316098",
7
  "organization_id": "org_38609339635728478",
8
  "object": "DirectoryGroup",
9
  "data": {
10
    "directory_id": "dir_38610496391217780",
11
    "display_name": "Engineering",
12
    "dp_id": "00g1a2b3c4d5e6f7g8h9",
13
    "id": "dirgroup_38862741498233423",
14
    "organization_id": "org_38609339635728478",
15
    "raw_attributes": {
16
      "displayName": "Engineering",
17
      "externalId": "00g1a2b3c4d5e6f7g8h9"
18
    }
19
  }
20
}
```

### `organization.directory.group_updated`

organization.directory.group_updated

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_38864948910162368",
4
  "type": "organization.directory.group_updated",
5
  "occurred_at": "2024-09-25T02:48:34.745030921Z",
6
  "environment_id": "env_32080745237316098",
7
  "organization_id": "org_38609339635728478",
8
  "object": "DirectoryGroup",
9
  "data": {
10
    "directory_id": "dir_38610496391217780",
11
    "display_name": "Platform Engineering",
12
    "dp_id": "00g1a2b3c4d5e6f7g8h9",
13
    "id": "dirgroup_38862741498233423",
14
    "organization_id": "org_38609339635728478",
15
    "raw_attributes": {
16
      "displayName": "Platform Engineering",
17
      "externalId": "00g1a2b3c4d5e6f7g8h9"
18
    }
19
  }
20
}
```

### `organization.directory.group_deleted`

organization.directory.group_deleted

```json
1
{
2
  "spec_version": "1",
3
  "id": "evt_40650399597723966",
4
  "type": "organization.directory.group_deleted",
5
  "occurred_at": "2024-10-07T10:25:26.289331747Z",
6
  "environment_id": "env_12205603854221623",
7
  "organization_id": "org_39802449573184223",
8
  "object": "DirectoryGroup",
9
  "data": {
10
    "directory_id": "dir_39802485862301855",
11
    "display_name": "Admins",
12
    "dp_id": "7c66a173-79c6-4270-ac78-8f35a8121e0a",
13
    "id": "dirgroup_40072007005503806",
14
    "organization_id": "org_39802449573184223",
15
    "raw_attributes": {}
16
  }
17
}
```

Note

Deleting a directory group removes it from every affected member and recalculates each member’s role from their remaining group memberships, even if the user belongs to multiple mapped groups. Scalekit sends an `organization.directory.user_updated` event for each affected member alongside this `organization.directory.group_deleted` event.

---
# DOCUMENT BOUNDARY
---

# Brand your login page

> Learn how to customize the look and feel of your Scalekit-hosted login page to match your brand.

A sign up or a login page is the first interaction your users have with your application. It’s important to create a consistent and branded experience for your users. In this guide, we’ll show you how to customize the Scalekit-hosted login page to match your brand.

## Access branding settings

[Section titled “Access branding settings”](#access-branding-settings)

Navigate to **Customization** > **Branding** in your Scalekit dashboard.

![Scalekit Branding page with a login page preview and controls for logo, favicon, button colors, border radius, and logo position](/.netlify/images?url=_astro%2Flogin.DBMXYWSJ.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

## Available customization options

[Section titled “Available customization options”](#available-customization-options)

| Setting                    | Description                                              | Options                                                              |
| -------------------------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
| **Logo**                   | Upload your company logo for the sign-in box             | Any image file or URL                                                |
| **Favicon**                | Set a custom favicon for the browser tab                 | Any image file or URL                                                |
| **Border Radius**          | Adjust the roundness of the login box corners            | Small, Medium, Large                                                 |
| **Logo Position**          | Choose where your logo appears                           | Inside or outside the login box                                      |
| **Logo Alignment**         | Align your logo horizontally                             | Left, Center, Right                                                  |
| **Header Text Alignment**  | Align the main header text                               | Left, Center, Right                                                  |
| **Social Login Placement** | Control positioning of social login buttons              | Various placement options                                            |
| **Background Color**       | Set the background color of the login page               | Color picker selection                                               |
| **Background Style**       | Style the page background using CSS shorthand properties | Supports image, position, size, repeat, origin, clip, and attachment |

## Background Style configuration

[Section titled “Background Style configuration”](#background-style-configuration)

The Background Style setting allows you to fully customize your login page background using CSS shorthand properties. This powerful feature gives you complete control over how your background appears.

### Understanding CSS background shorthand

[Section titled “Understanding CSS background shorthand”](#understanding-css-background-shorthand)

CSS background shorthand combines multiple background properties into a single declaration. Instead of setting each property separately, you can define them all at once.

```css
background: [background-color] [background-image] [background-position] [background-size] [background-repeat] [background-origin] [background-clip] [background-attachment];
```

[Learn more on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/background)

| Use case            | Background Style value                                                       | Description                                                   |
| ------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------- |
| Background image    | `url('https://example.com/your-image.jpg') center center/cover no-repeat`    | Sets a background image that covers the entire page           |
| Position and repeat | `url('https://example.com/pattern.png') top left repeat`                     | Creates a tiled pattern with specific positioning             |
| Gradient            | `linear-gradient(135deg, #4568DC, #B06AB3)`                                  | Creates a smooth gradient transition between colors           |
| Image with fallback | `#f5f5f5 url('https://example.com/image.jpg') center center/cover no-repeat` | Uses a background color that shows if the image fails to load |

Tips for best results

* Test your background style on different screen sizes to ensure it looks good on all devices
* Use high-quality images that won’t pixelate when scaled
* Consider your brand colors and overall design when selecting backgrounds
* For text readability, avoid backgrounds with high contrast patterns where text will appear

---
# DOCUMENT BOUNDARY
---

# Organization branding

> Show each customer's own logo, name and branding on your hosted login page, admin portal, hosted widgets and authentication emails.

Organization branding shows each of your customers its own branding. When a user from Acme Corp signs in, the login page, admin portal and hosted widgets show Acme Corp’s logo and name instead of your application’s. An organization can also have its own colors, fonts, legal links and support email, and its branding can reach the emails its users get.

To set it up:

* [Turn on organization branding](#enable-organization-branding) for your environment.
* [Customize each organization’s branding](#customize-an-organizations-branding) in its **Branding** tab, or set its logo from your code.
* [Pass `organization_id` when users log in](#show-the-organizations-branding-at-login), so Scalekit knows which organization’s branding to show.

## Where it appears

[Section titled “Where it appears”](#where-it-appears)

| Interface          | What changes                                                                                                                                                                                                                                                                                  |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Login page**     | Organization logo and `Login to {Organization name}` heading, plus the rest of its [custom branding](#customize-an-organizations-branding).                                                                                                                                                   |
| **Admin portal**   | Organization logo in the portal header, plus the rest of its custom branding.                                                                                                                                                                                                                 |
| **Hosted widgets** | Organization logo in the widgets header. The rest of the widgets use your environment’s branding.                                                                                                                                                                                             |
| **Emails**         | Organization name, logo, support email and legal links in login, sign-up and invitation emails, when you send them through [your own email provider](/guides/customize-emails/#use-your-own-email-provider). See [Use organization branding in emails](#use-organization-branding-in-emails). |

## Enable organization branding

[Section titled “Enable organization branding”](#enable-organization-branding)

Go to **Branding** in your Scalekit dashboard. At the end of the settings panel, turn on **Organization Branding** and click **Save**.

![Environment Branding page with the Organization Branding toggle turned on at the end of the settings panel](/.netlify/images?url=_astro%2Forganization_branding_configuration.irxvVVDm.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

Once it is on, each organization’s branding applies wherever it appears. An organization with no branding of its own keeps showing your application’s branding.

## Customize an organization’s branding

[Section titled “Customize an organization’s branding”](#customize-an-organizations-branding)

Each organization uses one of two kinds of branding:

* **Environment branding**: the organization follows your environment’s branding, and changes to it apply right away. An organization uses this until you customize it or set its logo.
* **Custom branding**: the organization’s own saved branding applies, and later changes to your environment’s branding don’t change what it saved. A value it leaves empty, such as its [support email](#set-the-organizations-support-email), keeps following your environment’s.

You set up custom branding in the organization’s **Branding** tab: its logo, favicon, font, button and border colors, layout, corners, background, legal links and support email. The organization’s name comes from the organization itself.

1. Go to **Organizations**, select the organization, and open the **Branding** tab. The tab shows the branding the organization uses now, with a live preview of its login page and admin portal. If organization branding is off, the tab links you to your environment’s **Branding** settings to turn it on.

   ![Organization Branding tab showing environment branding, with Edit environment branding and Customize for Acme Corp buttons and a live preview of the login page](/.netlify/images?url=_astro%2Forg-branding-tab-environment.BEOAqxvw.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

2. Click **Customize for** the organization. The editor starts from your environment’s branding, with the organization’s name.

3. Change what this organization needs. For the logo, upload a file or enter a URL, or [set it from your code](#set-the-logo-from-your-code).

4. Click **Apply custom branding**. After that, save later edits with **Save changes**. To check the result, click **Try login page**: it opens your login page with this organization’s branding.

   ![Organization Branding tab showing live custom branding, with the organization's logo in the header and the login page preview, and Switch to environment branding](/.netlify/images?url=_astro%2Forg-branding-tab-custom.D0gMAgzA.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

To go back, click **Switch to environment branding**. Scalekit keeps the saved custom branding, so **Re-apply custom branding** restores it later.

### Logo requirements

[Section titled “Logo requirements”](#logo-requirements)

A logo works best when it meets these requirements:

* **Publicly accessible URL**, when you enter a URL: Scalekit fetches the image from it.
* **Allowed formats**: PNG, JPG, SVG, GIF.
* **Recommended minimum height**: 36px. Smaller logos may look pixelated in the login page, admin portal, and hosted widgets.

### Set the logo from your code

[Section titled “Set the logo from your code”](#set-the-logo-from-your-code)

If you create organizations from your own code, you can set the logo there instead, with `logo_url`. To set it when you create the organization, see [Create organization](/authenticate/manage-users-orgs/create-organization/). To change or remove it later, update the organization, as below. The same logo requirements apply, and setting a logo switches the organization to custom branding. An empty `logo_url` removes the logo, and the organization shows your environment’s logo again.

The examples use the Scalekit client from [Set up Scalekit](/authenticate/fsa/quickstart/#set-up-scalekit).

* Node.js

  ```bash
  npm install @scalekit-sdk/node
  ```

* Python

  ```sh
  pip install scalekit-sdk-python
  ```

* Go

  ```sh
  go get -u github.com/scalekit-inc/scalekit-sdk-go
  ```

* Java

  ```groovy
  /* Gradle users - add the following to your dependencies in build file */
  implementation "com.scalekit:scalekit-sdk-java:2.1.3"
  ```

  ```xml
  
  
      com.scalekit
      scalekit-sdk-java
      2.1.3
  
  ```

- Node.js

  Set or remove an organization's logo

  ```javascript
  // Set the logo
  await scalekit.organization.updateOrganization('org_12345', {
    logoUrl: 'https://cdn.acmecorp.com/logo.png',
  });


  // Remove it
  await scalekit.organization.updateOrganization('org_12345', { logoUrl: '' });
  ```

- Python

  Set or remove an organization's logo

  ```python
  from scalekit.v1.organizations.organizations_pb2 import UpdateOrganization


  # Set the logo
  scalekit_client.organization.update_organization(
      organization_id='org_12345',
      organization=UpdateOrganization(logo_url='https://cdn.acmecorp.com/logo.png')
  )


  # Remove it
  scalekit_client.organization.update_organization(
      organization_id='org_12345',
      organization=UpdateOrganization(logo_url='')
  )
  ```

- Go

  Set or remove an organization's logo

  ```go
  1
  // Set the logo
  2
  logoURL := "https://cdn.acmecorp.com/logo.png"
  3
  if _, err := scalekitClient.Organization().UpdateOrganization(
  4
      ctx,
  5
      "org_12345",
  6
      &scalekit.UpdateOrganization{LogoUrl: &logoURL},
  7
  ); err != nil {
  8
      return err
  9
  }
  10


  11
  // Remove it
  12
  emptyURL := ""
  13
  if _, err := scalekitClient.Organization().UpdateOrganization(
  14
      ctx,
  15
      "org_12345",
  16
      &scalekit.UpdateOrganization{LogoUrl: &emptyURL},
  17
  ); err != nil {
  18
      return err
  19
  }
  ```

- Java

  Set or remove an organization's logo

  ```java
  // Requires scalekit-sdk-java v2.1.3+
  // Set the logo
  scalekitClient.organizations().updateById("org_12345",
      UpdateOrganization.newBuilder().setLogoUrl("https://cdn.acmecorp.com/logo.png").build());


  // Remove it
  scalekitClient.organizations().updateById("org_12345",
      UpdateOrganization.newBuilder().setLogoUrl("").build());
  ```

### Set the organization’s support email

[Section titled “Set the organization’s support email”](#set-the-organizations-support-email)

The **Support Email** field in the organization’s custom branding sets the support email shown on its login page and, when you send emails through [your own email provider](/guides/customize-emails/#use-your-own-email-provider), in its emails. Leave it empty to use your environment’s support email; it then follows any change you make to your environment’s support email.

![Organization custom branding editor with the Support Email field set to support@acmecorp.com](/.netlify/images?url=_astro%2Forg-branding-support-email.Dc7e6m8L.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

## Show the organization’s branding at login

[Section titled “Show the organization’s branding at login”](#show-the-organizations-branding-at-login)

Scalekit shows an organization’s branding only when it knows which organization the user is logging in to. Pass `organization_id` in the authorization request: the login page then shows the organization’s branding and the `Login to {Organization name}` heading, and its login and sign-up emails are sent for that organization. Without it, the login page shows your application’s branding.

Pass `organization_id` when building the authorization URL in your [login flow](/authenticate/fsa/implement-login/#set-up-login-flow):

* Node.js

  Authorization URL with organization_id

  ```diff
  const options = {
    scopes: ['openid', 'profile', 'email'],
    state: sessionStorage.oauthState,
    organizationId: 'org_12345',
  };


  const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options);
  res.redirect(authorizationUrl);
  ```

* Python

  Authorization URL with organization_id

  ```diff
  options = AuthorizationUrlOptions()
  options.scopes = ['openid', 'profile', 'email']
  options.state = session['oauth_state']
  options.organization_id = 'org_12345'


  authorization_url = scalekit_client.get_authorization_url(redirect_uri, options)
  return redirect(authorization_url)
  ```

* Go

  Authorization URL with organization_id

  ```diff
  options := scalekit.AuthorizationUrlOptions{
      Scopes:         []string{"openid", "profile", "email"},
      State:          state,
      +OrganizationId: "org_12345",
  }


  authorizationUrl := scalekitClient.GetAuthorizationUrl(redirectUri, options)
  http.Redirect(w, r, authorizationUrl, http.StatusFound)
  ```

* Java

  Authorization URL with organization_id

  ```diff
  AuthorizationUrlOptions options = new AuthorizationUrlOptions();
  options.setScopes(List.of("openid", "profile", "email"));
  options.setState(state);
  +options.setOrganizationId("org_12345");


  String authorizationUrl = scalekitClient.getAuthorizationUrl(redirectUri, options);
  response.sendRedirect(authorizationUrl);
  ```

## Use organization branding in emails

[Section titled “Use organization branding in emails”](#use-organization-branding-in-emails)

Login, sign-up and invitation emails can carry the organization’s branding when you send them through [your own email provider](/guides/customize-emails/#use-your-own-email-provider):

* **In the email body**, use the `organization_*` variables in a [custom email template](/guides/customize-emails/#organization), such as `{{ organization_logo_url }}` and `{{ organization_support_email }}`. These fall back to your application’s values when the organization doesn’t set them; `{{ organization_name }}` is empty when the email isn’t sent for an organization.
* **In the sender name**, use `{{ organization_name }}`, such as `{{ organization_name }} Support`. See [Use variables in the sender name](/guides/customize-emails/#use-variables-in-the-sender-name).

Login and sign-up emails are sent for an organization when the authorization URL includes `organization_id`, as [shown above](#show-the-organizations-branding-at-login). Invitation emails are sent for the organization the user is invited to.

---
# DOCUMENT BOUNDARY
---

# Branded custom domains

> Learn how to set up a branded custom domain with Scalekit

Custom domain branding lets you provide a fully branded authentication experience for your customers. By default, Scalekit assigns a unique environment URL (like `https://yourapp.scalekit.com`), but you can replace it with your own domain (like `https://auth.yourapp.com`) using DNS CNAME configuration. This branded domain becomes the base URL for your admin portal, SSO connections, directory sync setup, and REST API endpoints—giving your customers a seamless, on-brand experience throughout their authentication journey.

This guide shows you how to configure a CNAME record in your DNS registrar and verify SSL certificate provisioning for your custom domain.

| Before                         | After                      |
| ------------------------------ | -------------------------- |
| `https://yourapp.scalekit.com` | `https://auth.yourapp.com` |

Production environment only

CNAME configuration is available only in production environments. Ensure you’re working in your production environment before proceeding.

Custom domains use DNS CNAME records to route traffic from your branded domain to Scalekit’s infrastructure:

1. Your custom domain (e.g., `auth.yourapp.com`) points to Scalekit’s infrastructure via a CNAME record
2. Scalekit automatically provisions and manages SSL certificates for your domain
3. All Scalekit services (Admin Portal, SSO endpoints, directory sync, REST API) become accessible through your branded domain

This architecture ensures your domain remains on your brand while leveraging Scalekit’s secure, scalable infrastructure. CNAME records safely route traffic without exposing your configuration, and SSL certificates automatically provisioned by Scalekit ensure all traffic to your custom domain is encrypted (HTTPS).

Existing integrations remain unaffected

Integrations configured before the CNAME change will continue to work with your previous Scalekit domain. They don’t automatically update to use your custom domain.

### DNS record reference

[Section titled “DNS record reference”](#dns-record-reference)

When configuring your CNAME record, you’ll need to provide the following fields:

| DNS Record Field         | Example Value           | Description                                                                               |
| ------------------------ | ----------------------- | ----------------------------------------------------------------------------------------- |
| Record Type              | `CNAME`                 | Canonical Name record that creates an alias from your domain to Scalekit’s infrastructure |
| Name/Host/Label          | `auth.yourapp.com`      | Your custom subdomain (copied from Scalekit dashboard)                                    |
| Value/Target/Destination | `scalekit-prod-xyz.com` | Scalekit’s endpoint URL (copied from Scalekit dashboard)                                  |
| TTL                      | `3600`                  | Time to Live in seconds (optional, typically set by your registrar’s default)             |

Field names vary by registrar

Different DNS registrars use different names for these fields. The `Name` field might be called “Host” or “Label”, and the `Value` field might be called “Target” or “Destination”. The values you enter remain the same.

## Set up your custom domain

[Section titled “Set up your custom domain”](#set-up-your-custom-domain)

Let’s set up your custom domain by adding a CNAME record to your DNS registrar and verifying the configuration.

1. CNAME configuration is available only for production environments. Log into the Scalekit dashboard and ensure you’re working in your production environment.

2. In the Scalekit dashboard, go to **Dashboard > Customization > Custom Domain**. This page displays the CNAME record details you’ll need to configure in your DNS registrar.

   ![](/.netlify/images?url=_astro%2F1.BktW9U-H.png\&w=2786\&h=1746\&dpl=6ae471fc143c9a4c27550463)

3. Go to your domain registrar’s DNS management console and create a new DNS record. Select `CNAME` as the record type.

4. In **Dashboard > Customization > Custom Domain**, copy the `Name` field (your desired subdomain). Paste this value into your DNS registrar’s `Name`, `Label`, or `Host` field.

5. Still in **Dashboard > Customization > Custom Domain**, copy the `Value` field. Paste this value into your DNS registrar’s `Destination`, `Target`, or `Value` field.

6. Save the CNAME record in your DNS registrar. The changes may take some time to propagate across DNS servers.

7. Return to **Dashboard > Customization > Custom Domain** in the Scalekit dashboard and click the **Verify** button. This validates that your CNAME record is properly configured and accessible.

Existing connections?

If you have existing SSO or SCIM connections, they will continue to use your previous Scalekit environment domain. New connections will use your custom domain going forward.

### SSL certificate provisioning

[Section titled “SSL certificate provisioning”](#ssl-certificate-provisioning)

After successful CNAME verification, Scalekit automatically provisions an SSL certificate for your custom domain:

* **Initial provisioning** - SSL certificate provisioning can take up to 24 hours after CNAME verification
* **Check status** - Click the **Check** button in **Dashboard > Customization > Custom Domain** to verify SSL certificate status
* **Still pending after 24 hours** - If SSL provisioning takes longer than 24 hours, contact our support team at [](mailto:support@scalekit.com) for assistance

SSL certificate provisioning

After the CNAME record propagates, Scalekit automatically provisions an SSL certificate for your custom domain. This process can take up to 24 hours. Click the **Check** button in the dashboard to verify SSL certificate status.

## External resources

[Section titled “External resources”](#external-resources)

For detailed instructions on adding a CNAME record with popular DNS registrars:

* [GoDaddy: Add a CNAME record](https://www.godaddy.com/en-in/help/add-a-cname-record-19236)
* [Namecheap: How to create a CNAME record](https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain)

---
# DOCUMENT BOUNDARY
---

# Customize email provider and templates

> Send login, sign-up and invitation emails through Scalekit or your own SMTP provider, and customize what they say with templates and variables.

Scalekit sends your users’ authentication emails: the one-time passcode or magic link they use to log in or sign up, and invitations to join an organization. You control two things in **Emails**:

* **How the emails are sent**, in the **Custom Email Provider** tab: through Scalekit’s email service, or through your own SMTP provider from your own domain.
* **What the emails say**, in the **Custom Templates** tab: the subject and body of each email. Custom templates need your own email provider.

## Choose how emails are sent

[Section titled “Choose how emails are sent”](#choose-how-emails-are-sent)

By default, Scalekit sends every authentication email through its own email service. Switch to your own email provider when you need more control over branding, deliverability or compliance.

![Email delivery methods in Scalekit](/.netlify/images?url=_astro%2F1-email-delivery-method.DLKX49XJ.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

### Use Scalekit’s email service Default

[Section titled “Use Scalekit’s email service ”](#use-scalekits-email-service-)

This needs no setup, which suits development and testing. Scalekit handles delivery with these settings:

* **Sender Name**: `Team` followed by your application name, such as `Team Hacksmith`. You can change it.
* **From Email Address**: `hi@scalekit.com`

### Use your own email provider

[Section titled “Use your own email provider”](#use-your-own-email-provider)

Use your own SMTP provider when you need any of these:

* Emails sent from your own domain and address, to keep your brand and your users’ trust
* Your provider’s deliverability, sending reputation and analytics
* Email handling that meets your compliance requirements, such as GDPR or CCPA

Before you start, collect these details from your email provider:

| Field                  | Description                                                                                                                               |
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| **SMTP Server Host**   | Your provider’s SMTP hostname                                                                                                             |
| **SMTP Port**          | Usually 587 (TLS) or 465 (SSL)                                                                                                            |
| **SMTP Username**      | Your authentication username                                                                                                              |
| **SMTP Password**      | Your SMTP password or API key                                                                                                             |
| **From Email Address** | The email address emails are sent from                                                                                                    |
| **Sender Name**        | The display name recipients see in their inbox, such as your company name. It can include [variables](#use-variables-in-the-sender-name). |

Common providers use these settings:

* SendGrid

  ```plaintext
  Host: smtp.sendgrid.net
  Port: 587
  Username: apikey
  Password: [Your SendGrid API Key]
  ```

* Amazon SES

  ```plaintext
  Host: email-smtp..amazonaws.com  (the region of your SMTP credentials, such as us-east-1)
  Port: 587
  Username: [Your SMTP Username from AWS]
  Password: [Your SMTP Password from AWS]
  ```

* Postmark

  ```plaintext
  Host: smtp.postmarkapp.com
  Port: 587
  Username: [Your Postmark Server Token]
  Password: [Your Postmark Server Token]
  ```

Then enter them in Scalekit:

1. In your Scalekit dashboard, go to **Emails**.

2. Choose **Use your own email provider**.

3. Enter the sender details:

   ```plaintext
   From Email Address: noreply@yourdomain.com
   Sender Name: Your Company Name
   ```

4. Enter the SMTP details:

   ```plaintext
   SMTP Server Host: smtp.your-provider.com
   SMTP Port: 587
   SMTP Username: your-username
   SMTP Password: your-password
   ```

5. Click **Test & Save Configuration**. Scalekit checks the connection to your SMTP server before it saves the settings.

Scalekit stores your SMTP password encrypted and requires TLS when it sends through your server.

#### Use variables in the sender name

[Section titled “Use variables in the sender name”](#use-variables-in-the-sender-name)

The sender name can name the organization an email is sent for, such as `{{ organization_name }} Support`. Click `{ }` next to **Sender Name** to insert a variable; the field previews the result with and without an organization.

| Variable                  | Value                                                                                                                                    |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `{{ organization_name }}` | The organization the email is sent for. Empty when the email isn’t sent for one, so `{{ organization_name }} Support` becomes `Support`. |
| `{{ app_name }}`          | Your application name from **Branding**, else your workspace name                                                                        |

Only these two variables work in the sender name; saving any other returns a `400` error. If the name renders empty or as an email address, the email uses your default sender name.

![Sender Name using the organization name variable followed by Support, with previews for Acme Corp and for an email without an organization](/.netlify/images?url=_astro%2Fsender-name-preview.0ED0bTXU.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

## Customize email templates

[Section titled “Customize email templates”](#customize-email-templates)

Scalekit uses its default templates for each email. To change what an email says, go to **Emails** > **Custom Templates** and select the email.

![Emails Custom Templates tab listing authentication email templates for login, sign-up, and organization invitations, each using the Scalekit type](/.netlify/images?url=_astro%2Fcustom-templates-list.C2tT6CvK.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

For each email, choose how it is generated:

* **Use Scalekit template**: preview the subject and bodies. You can’t edit them; the email uses Scalekit’s default content.
* **Use custom template**: edit the subject, HTML body and plain text body. Your saved content is used for every later send.

![Email template editor for the login OTP email, with the custom template selected, a subject and HTML body that use organization variables, and the Organization variables listed](/.netlify/images?url=_astro%2Fsub-selection-custom-tempaltes.B2vGP1AF.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463)

Note

A custom template is used only once you [use your own email provider](#use-your-own-email-provider). Until then, every email uses Scalekit’s default template.

### Provide HTML and plain text versions

[Section titled “Provide HTML and plain text versions”](#provide-html-and-plain-text-versions)

Provide both versions of your email body in the template editor. When both are present, Scalekit sends a multipart/alternative message: clients that show HTML display the HTML part, and the others display the plain text part.

Tip

Include a clear call-to-action link in the plain text body when using tracking pixels or richly styled buttons in HTML.

### Use built-in template variables

[Section titled “Use built-in template variables”](#use-built-in-template-variables)

Use these built-in variables in your templates. Values are filled in at send time. The variables below apply to all Scalekit templates.

#### Application

[Section titled “Application”](#application)

Use application variables to include app-level data (for example, name, logo, support email) that stays the same across all emails for your app.

| Variable                         | Description                                                                       |
| -------------------------------- | --------------------------------------------------------------------------------- |
| `{{app_name}}`                   | Your application name                                                             |
| `{{app_logo_url}}`               | Public URL to your application logo                                               |
| `{{app_support_email}}`          | Support email address for your application                                        |
| `{{app_organization_meta_name}}` | What your app calls an organization, such as workspace or team, from **Branding** |
| `{{app_terms_of_service_url}}`   | Terms of service link from your branding settings                                 |
| `{{app_privacy_policy_url}}`     | Privacy policy link from your branding settings                                   |

#### Organization

[Section titled “Organization”](#organization)

Organization variables describe the organization an email is sent for. Login and sign-up emails are sent for an organization when your [authorization URL includes `organization_id`](/fsa/guides/organization-branding/#show-the-organizations-branding-at-login). Invitation emails are sent for the organization the user is invited to.

| Variable                                | Description                                                                 |
| --------------------------------------- | --------------------------------------------------------------------------- |
| `{{organization_name}}`                 | The organization name. Empty when the email isn’t sent for an organization. |
| `{{organization_logo_url}}`             | The organization’s logo, else your application logo                         |
| `{{organization_support_email}}`        | The organization’s support email, else your application’s                   |
| `{{organization_terms_of_service_url}}` | The organization’s terms of service link, else your application’s           |
| `{{organization_privacy_policy_url}}`   | The organization’s privacy policy link, else your application’s             |

The `organization_*` values other than the name come from the organization’s [custom branding](/fsa/guides/organization-branding/#customize-an-organizations-branding) when [organization branding is on](/fsa/guides/organization-branding/#enable-organization-branding). Otherwise, and for any value the organization doesn’t set, they use your application’s values, so one template works whether or not the email is sent for an organization. The `app_*` variables hold your application’s values, also in emails sent for an organization.

Logo and support line for the organization

```html
{{ if organization_name }}{{ organization_name }}{{ else }}{{ app_name }}{{ end }}

Questions? Contact {{ organization_support_email }}.

``` #### User [Section titled “User”](#user) User variables personalize the email for the recipient (for example, name and email). | Variable | Description | | ---------------- | ----------------------------- | | `{{user_name}}` | The recipient’s name | | `{{user_email}}` | The recipient’s email address | #### Contextual [Section titled “Contextual”](#contextual) Contextual variables belong to the current email and change with each send. | Variable | Description | | -------------------------- | ------------------------------------------------------------------------------------------------------------ | | `{{link}}` | The magic link the user clicks to log in or sign up | | `{{otp}}` | The one-time passcode the user enters | | `{{expiry_time_relative}}` | How long the link, passcode or invitation stays valid, rounded to one unit, such as “10 minutes” or “7 days” | ### Use Jet template syntax [Section titled “Use Jet template syntax”](#use-jet-template-syntax) Custom email templates use Jet syntax for dynamic content, including conditionals, loops and filters. Two common patterns: * Conditional welcome message ```html {{ if user_name }}

Hello {{ user_name }},

{{ else }}

Hello,

{{ end }}

Welcome to {{ app_name }}!

``` * User invite with organization ```html {{ if organization_name }}

You have been invited to join {{ organization_name }} organization in {{ app_name }}.

{{ else }}

You have been invited to {{ app_name }}.

{{ end }} ``` Jet syntax reference For the full syntax, including functions, filters and control structures, see the [Jet syntax reference](https://github.com/CloudyKit/jet/blob/master/docs/syntax.md). ### Pass your own variables at runtime Passwordless [Section titled “Pass your own variables at runtime ”](#pass-your-own-variables-at-runtime-) If you use the [Passwordless headless API](/passwordless/quickstart/), you can fill your own variables in each email from your code. They work only when you [use your own email provider](#use-your-own-email-provider). * Send up to 30 variables per email, as key-value pairs. Keys are 2 to 25 characters; values are up to 256 characters. * Every variable your template uses must have a value in the request. * These names are reserved: `otp`, `expiry_time_relative`, `link`, `expire_time`, `expiry_time`. * To change the application name or logo for one email, pass `app_name` or `app_logo_url`. They replace your environment’s branding in that email. This example is for the **Verification Code** method, so the email carries a one-time passcode. With **Magic Link** or **Magic Link + Verification Code**, also pass `magiclinkAuthUri`, the URL in your app that the magic link opens, and put `{{ link }}` in the template. To run it, install `@scalekit-sdk/node` and `dotenv` (`npm install @scalekit-sdk/node dotenv`) and put `SCALEKIT_ENVIRONMENT_URL`, `SCALEKIT_CLIENT_ID` and `SCALEKIT_CLIENT_SECRET` in a `.env` file. 1. Add your variables to the email template, next to the passcode: Example email template ```html

Hello {{ first_name }},

Your {{ company_name }} verification code is {{ otp }}.

Find your onboarding kit: {{ onboarding_resources }}

``` 2. Pass their values when you send the email. Save the code as `send-email.mts` and run it with `npx tsx send-email.mts`: send-email.mts ```js import 'dotenv/config'; import { Scalekit, ScalekitServerException } from '@scalekit-sdk/node'; const scalekit = new Scalekit( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET! ); try { await scalekit.passwordless.sendPasswordlessEmail('john.doe@example.com', { templateVariables: { first_name: 'John', company_name: 'Acme Corp', onboarding_resources: 'https://acme.com/onboarding', }, }); } catch (err) { if (!(err instanceof ScalekitServerException)) throw err; console.error(err.errorCode, err.message); } ``` 3. The email arrives with the values filled in: Example email preview ```html Hello John, Your Acme Corp verification code is 123456. Find your onboarding kit: https://acme.com/onboarding ``` Caution The API returns a `400` error if your template uses a variable that the request doesn’t include. ## Test your emails [Section titled “Test your emails”](#test-your-emails) After you change how emails are sent or what they say, log in or sign up through your app to trigger an email, then check: * It reaches the inbox, not the spam folder. * The recipient sees the sender name and address you set. * The content, branding, links and buttons look and work as expected. *** **Test your knowledge with a quiz** Which choice requires using your own email provider? * Use Scalekit template * Preview subject and bodies * Use custom template * Change the sender name Submit --- # DOCUMENT BOUNDARY --- # View logs > Monitor authentication activities and webhook deliveries using comprehensive logs that track user sign-ins, authentication methods, and webhook event processing. Scalekit provides comprehensive logging for both authentication activities and webhook deliveries. Use these logs to monitor user access patterns, troubleshoot authentication issues, debug webhook integrations, and maintain compliance with audit requirements. ## Access logs [Section titled “Access logs”](#access-logs) **Authentication logs**: Navigate to **Dashboard > Auth Logs** to view all authentication events across your environment. ![](/.netlify/images?url=_astro%2F2.DFnmlRa6.png\&w=2936\&h=1956\&dpl=6ae471fc143c9a4c27550463) Each auth log entry displays the authentication event details, status, timestamp, user information, and authentication method used. **Webhook logs**: Navigate to **Dashboard > Webhooks** to view all configured webhook endpoints. Click on the specific webhook endpoint you want to monitor, then select the **”…”** (more options) button to access detailed delivery logs for that endpoint. ![](/.netlify/images?url=_astro%2Fdashboard.Ds15e5Zk.png\&w=2936\&h=1592\&dpl=6ae471fc143c9a4c27550463) Each webhook log entry displays the webhook event details, delivery status, timestamp, and response information from your application. ## Authentication statuses [Section titled “Authentication statuses”](#authentication-statuses) Auth logs display four different statuses that help you understand where users are in the authentication flow: | Status | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Initiated** | The user has started the authentication process by accessing the `/oauth/authorize` endpoint. This indicates they’ve begun the authorization flow but haven’t completed it yet. | | **Pending** | The authentication is in a transitional state between initiation and completion. During this phase, the system performs redirects while exchanging user profile details for authorization code grants. The authentication is still in progress. | | **Success** | The system successfully exchanged the authorization code grant, verified the user’s identity, and granted them access. The authentication flow has completed successfully. | | **Failure** | The authentication process failed and access was denied. This could be due to invalid credentials, network issues, interceptor rejections, or other authentication failures. Review the error details to identify the cause of the failure. | ## Filter auth logs [Section titled “Filter auth logs”](#filter-auth-logs) When investigating incidents or troubleshooting issues, use filters to narrow down log data and quickly identify authentication problems. **Available filters:** * **Time range** - Filter logs by specific date and time periods to focus on recent activity or investigate historical events * **User email** - Search for authentication events from specific users to track individual user activity or troubleshoot sign-in issues * **Authentication status** - Filter by Initiated, Pending, Success, or Failure to isolate specific authentication outcomes * **Organization** - View authentication events for specific organizations in multi-tenant applications Combine multiple filters to narrow your search. For example, filter by a specific user email and Failure status to investigate why a user cannot sign in. ## Webhook logs [Section titled “Webhook logs”](#webhook-logs) ### Webhook delivery statuses [Section titled “Webhook delivery statuses”](#webhook-delivery-statuses) Webhook logs display four different statuses that indicate the delivery state of each webhook event: | Status | Description | | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Success** | Your application endpoint responded with a 2xx status code (typically 200 or 201), confirming successful receipt and processing of the webhook event. | | **Queued** | Due to high event volume or rate limiting, the webhook event is queued and waiting to be sent to your application endpoint. Delivery order isn’t guaranteed: use each event’s `occurred_at` when order matters. | | **Failed** | Your application endpoint did not respond, returned a non-2xx status code (typically 4xx or 5xx), or the request timed out. Failed deliveries trigger automatic retries. | | **Retrying** | Your application endpoint failed to acknowledge the webhook, and Scalekit is automatically retrying the delivery using exponential backoff. Scalekit makes up to 8 attempts over about 27 hours, with increasing delays between them. | Monitor failed webhooks Failed webhooks can indicate issues with your endpoint availability, request validation, or processing logic. Review failed webhook logs to identify patterns and fix integration issues promptly. ### Filter webhook logs [Section titled “Filter webhook logs”](#filter-webhook-logs) When troubleshooting webhook delivery issues or investigating specific events, use filters to narrow down log data and quickly identify problems. **Available filters:** * **Time range** - Filter logs from the last 5 minutes to the last 30 days to focus on recent deliveries or investigate historical events * **Event type** - Filter by specific webhook event types (e.g., `organization.directory.user_created`, `organization.directory.user_updated`) to track particular types of events * **Delivery status** - Filter by Success, Queued, Failed, or Retrying to isolate problematic deliveries or verify successful processing Combine multiple filters to narrow your search. For example, filter by Failed status and a specific event type to investigate why certain events are not being processed successfully. ### Webhook log details [Section titled “Webhook log details”](#webhook-log-details) Click on any log entry to view detailed information about the webhook delivery: **Request details:** * Event ID and type * Timestamp when the event occurred * Request payload sent to your endpoint * Request headers including webhook signature **Response details:** * HTTP status code returned by your endpoint * Response body from your application * Response time and latency * Retry attempt number (if applicable) Use these details to debug webhook processing issues, verify signature validation, and ensure your endpoint handles events correctly. ### Retry behavior [Section titled “Retry behavior”](#retry-behavior) When webhook deliveries fail, Scalekit automatically retries sending the event to your endpoint: **Retry schedule:** * **Attempt 1**: Immediate delivery * **Attempt 2**: 5 seconds after attempt 1 * **Attempt 3**: 5 minutes after attempt 2 * **Attempt 4**: 30 minutes after attempt 3 * **Attempt 5**: 2 hours after attempt 4 * **Attempt 6**: 5 hours after attempt 5 * **Attempt 7**: 10 hours after attempt 6 * **Attempt 8**: 10 hours after attempt 7 A delivery fails when your endpoint returns a status code outside `2xx` or doesn’t respond within 30 seconds. After the final retry attempt fails, about 27 hours after the first attempt, the webhook is marked as permanently failed. You can view these failed webhooks in the logs and manually replay them when your endpoint is ready to process them. If an endpoint fails every delivery for 5 days, Scalekit disables it. Best practices for webhook reliability Ensure your webhook endpoint responds quickly (within 30 seconds), returns appropriate 2xx status codes for successful processing, and implements idempotency to safely handle duplicate deliveries during retries. --- # DOCUMENT BOUNDARY --- # Organization-specific redirect URLs > Register one redirect URL pattern that Scalekit resolves to an organization-specific URL at runtime. Multi-tenant applications often need a different callback URL for each customer. Instead of registering a separate redirect URL for every organization, you can register a single template that Scalekit expands per organization at runtime. For example, `https://{{org_slug}}/oauth/callback` expands to `https://auth.megasoft.com/oauth/callback` for Megasoft and `https://auth.betacorp.com/oauth/callback` for Beta Corp, with no additional configuration needed. ## Available template variables [Section titled “Available template variables”](#available-template-variables) | Variable | Source | Example value | | ------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | `{{org_slug}}` | Organization’s slug field. Can be a single DNS label (`megasoft`) or a full hostname (`auth.megasoft.com`). | `auth.megasoft.com` | | `{{external_id}}` | Organization’s external ID from your system. | `megasoft-123` | | `{{custom_domain}}` | Any key stored in the organization’s metadata. | `metadata.custom_domain = "auth.megasoft.com"` | ## Register an organization-specific URL [Section titled “Register an organization-specific URL”](#register-an-organization-specific-url) Add organization-specific URLs in **Dashboard > Authentication > Redirects** and type the URL pattern directly into the input field. These URLs are valid in all three redirect fields: * **Allowed callback URLs**: where Scalekit sends users after authentication * **Post logout URL**: where Scalekit sends users after logout * **Initiate login URL**: where Scalekit sends IdP-initiated login requests ![Scalekit Redirect URLs settings showing allowed callback, initiate-login, and post-logout URLs using templated organization slug patterns](/.netlify/images?url=_astro%2F2026-05-22-12-22-55.CVFnVtd_.png\&w=2420\&h=1420\&dpl=6ae471fc143c9a4c27550463) Supported patterns for redirect URLs: | Pattern | Example | Notes | | ------------- | ------------------------------------------- | ------------------------------------------------------------- | | Subdomain | `https://{{org_slug}}.yourapp.com/callback` | `{{org_slug}}` is a single DNS label such as `acme` | | Full host | `https://{{org_slug}}/callback` | `{{org_slug}}` is a full hostname such as `auth.megasoft.com` | | Custom domain | `https://{{custom_domain}}/callback` | Value comes from org metadata | | Path variable | `https://yourapp.com/{{org_slug}}/callback` | Variable in the path component | DNS label constraint Template variables cannot span part of a DNS label. `https://app-{{org_slug}}.yourapp.com` is not valid; the placeholder must occupy the entire DNS label or the entire host. At runtime, Scalekit substitutes each `{{variable}}` with its resolved value, validates the expanded URL against registered templates, and redirects the user. For logout, the organization is resolved from the active session; if the session has expired, template expansion is skipped and the redirect is rejected. ## Set slug or metadata for an organization [Section titled “Set slug or metadata for an organization”](#set-slug-or-metadata-for-an-organization) Set `slug` on an organization to expand `{{org_slug}}` templates. Store any key in organization `metadata` to use it as a custom variable like `{{custom_domain}}`. Setting just one is enough. Use whichever fits your registered URL pattern. ### Via dashboard [Section titled “Via dashboard”](#via-dashboard) Navigate to **Organizations > Select an organization > Overview > Edit**. Add the organization slug in the **Slug** field. ![Create Organization dialog showing the Slug field](/.netlify/images?url=_astro%2F2026-05-22-12-46-21.vDYPy38g.png\&w=1842\&h=1386\&dpl=6ae471fc143c9a4c27550463) ### Via SDK [Section titled “Via SDK”](#via-sdk) * Node.js ```bash npm install @scalekit-sdk/node ``` * Python ```sh pip install scalekit-sdk-python ``` * Go ```sh go get -u github.com/scalekit-inc/scalekit-sdk-go ``` * Java ```groovy /* Gradle users - add the following to your dependencies in build file */ implementation "com.scalekit:scalekit-sdk-java:2.1.3" ``` ```xml com.scalekit scalekit-sdk-java 2.1.3 ``` - Node.js Create organization with slug and metadata ```javascript // Set slug, metadata, or both — each enables a different template variable. const organization = await scalekit.organization.createOrganization( 'Megasoft', { slug: 'auth.megasoft.com', metadata: { custom_domain: 'auth.megasoft.com' }, } ); ``` - Python Create organization with slug and metadata ```python # Set slug, metadata, or both — each enables a different template variable. from scalekit.v1.organizations.organizations_pb2 import CreateOrganization organization = scalekit_client.organization.create_organization( CreateOrganization( display_name='Megasoft', slug='auth.megasoft.com', metadata={'custom_domain': 'auth.megasoft.com'}, ) ) ``` - Go Create organization with slug and metadata ```go 1 // Set slug, metadata, or both — each enables a different template variable. 2 slug := "auth.megasoft.com" 3 4 _, err := scalekitClient.Organization().CreateOrganization( 5 ctx, 6 "Megasoft", 7 scalekit.CreateOrganizationOptions{ 8 Slug: &slug, 9 Metadata: map[string]interface{}{ 10 "custom_domain": "auth.megasoft.com", 11 }, 12 }, 13 ) 14 if err != nil { 15 log.Fatal(err) 16 } ``` - Java Create organization with slug and metadata ```java // Set slug, metadata, or both — each enables a different template variable. // Requires scalekit-sdk-java v2.1.3+ CreateOrganization create = CreateOrganization.newBuilder() .setDisplayName("Megasoft") .setSlug("auth.megasoft.com") .putMetadata("custom_domain", "auth.megasoft.com") .build(); scalekitClient.organizations().create(create); ``` ## Pass organization_id and redirect URL in the authorization request [Section titled “Pass organization_id and redirect URL in the authorization request”](#pass-organization_id-and-redirect-url-in-the-authorization-request) Pass the organization-specific `redirect_uri` and `organization_id` in the authorization request. Scalekit validates the redirect URL against the registered pattern using the organization’s slug or metadata. See [set up login flow](/authenticate/fsa/implement-login/#set-up-login-flow) for the full auth call reference. * Node.js Authorization URL with organization_id ```diff +const redirectUri = 'https://auth.megasoft.com/oauth/callback'; const options = { scopes: ['openid', 'profile', 'email'], state: sessionStorage.oauthState, organizationId: 'org_12345', }; const authorizationUrl = scalekit.getAuthorizationUrl(redirectUri, options); res.redirect(authorizationUrl); ``` * Python Authorization URL with organization_id ```diff redirect_uri = 'https://auth.megasoft.com/oauth/callback' options = AuthorizationUrlOptions() options.scopes = ['openid', 'profile', 'email'] options.state = session['oauth_state'] options.organization_id = 'org_12345' authorization_url = scalekit_client.get_authorization_url(redirect_uri, options) return redirect(authorization_url) ``` * Go Authorization URL with organization_id ```diff +redirectUri := "https://auth.megasoft.com/oauth/callback" options := scalekit.AuthorizationUrlOptions{ Scopes: []string{"openid", "profile", "email"}, State: state, +OrganizationId: "org_12345", } authorizationUrl := scalekitClient.GetAuthorizationUrl(redirectUri, options) http.Redirect(w, r, authorizationUrl, http.StatusFound) ``` * Java Authorization URL with organization_id ```diff +String redirectUri = "https://auth.megasoft.com/oauth/callback"; AuthorizationUrlOptions options = new AuthorizationUrlOptions(); options.setScopes(List.of("openid", "profile", "email")); options.setState(state); +options.setOrganizationId("org_12345"); String authorizationUrl = scalekitClient.getAuthorizationUrl(redirectUri, options); response.sendRedirect(authorizationUrl); ``` If no organization is in scope, the template is not expanded and the request is rejected. ## A worked example [Section titled “A worked example”](#a-worked-example) 1. **Register the URL pattern** In **Dashboard > Authentication > Redirects > Allowed callback URLs**, add the URL as `https://{{org_slug}}/oauth/callback`. 2. **Set the organization’s slug** For Megasoft, set `slug` to `auth.megasoft.com` (see [Set slug or metadata for an organization](#set-slug-or-metadata-for-an-organization) above). 3. **Pass the org-specific redirect URL and `organization_id` in the authorization request** When a Megasoft user signs in, Scalekit validates the `redirect_uri` against the registered pattern using the organization’s slug: ```plaintext https:///oauth/authorize?client_id=...&redirect_uri=https://auth.megasoft.com/oauth/callback&organization_id=org_megasoft_123&... ``` 4. **Scalekit validates and redirects** Scalekit matches `https://auth.megasoft.com/oauth/callback` against the registered pattern `https://{{org_slug}}/oauth/callback` using Megasoft’s slug and redirects the user. --- # DOCUMENT BOUNDARY --- # Configure redirect URLs > Learn how to configure and validate redirect URLs in Scalekit for secure authentication flows, including callback, login, logout, and back-channel logout endpoints Redirects are registered endpoints in Scalekit that control where users are directed during authentication flows. You must configure these endpoints in the Scalekit dashboard before they can be used. All redirect URIs must be registered under Authentication settings in your Scalekit dashboard. This is a security requirement to prevent unauthorized redirects. ## Redirect endpoint types [Section titled “Redirect endpoint types”](#redirect-endpoint-types) ### Allowed callback URLs [Section titled “Allowed callback URLs”](#allowed-callback-urls) **Purpose**: Where users are sent after successful authentication to exchange authorization codes and retrieve profile information. **Example scenario**: A user completes sign-in and Scalekit redirects them to `https://yourapp.com/callback` where your application processes the authentication response. To add or remove an redirect URL, go to Dashboard > Authentication > Redirects > Allowed Callback URLs. ### Initiate login URL [Section titled “Initiate login URL”](#initiate-login-url) **Purpose**: When authentication does not initiate from your application, Scalekit redirects users back to your application’s login initiation endpoint. This endpoint should point to a route in your application that ultimately redirects users to Scalekit’s `/authorize` endpoint. **Example scenarios**: * **Bookmarked login page**: A user bookmarks your login page and visits it directly. Your application detects they’re not authenticated and redirects them to Scalekit’s authorization endpoint. * **Organization invitation flow**: A user clicks an invitation link to join an organization. Your application receives the invitation token and redirects the user to Scalekit’s authorization endpoint to complete the sign-up process. * **IdP-initiated SSO**: An administrator initiates single sign-on from their identity provider dashboard. The IdP redirects users to your application, which then redirects them to Scalekit’s authorization endpoint to complete authentication. * **Session expiration**: When a user’s session expires or they access a protected resource, they’re redirected to `https://yourapp.com/login` which then redirects to Scalekit’s authentication endpoint. ### Post logout URL [Section titled “Post logout URL”](#post-logout-url) **Purpose**: Where users are sent after successfully signing out of your application. **Example scenario**: After logging out, users are redirected to `https://yourapp.com/goodbye` to confirm their session has ended. ### Back channel logout URL [Section titled “Back channel logout URL”](#back-channel-logout-url) **Purpose**: A secure endpoint that receives notifications whenever a user is logged out from Scalekit, regardless of how the logout was initiated — admin triggered, user initiated, or due to session policies like idle timeout. **Example scenario**: When a user logs out from any application (user-initiated, admin-initiated, or due to session policies like idle timeout), Scalekit sends a logout notification to `https://yourapp.com/logout` to suggest termination of the user’s session across all connected applications, ensuring coordinated logout for enhanced security. ### Custom URI schemes [Section titled “Custom URI schemes”](#custom-uri-schemes) Custom URI schemes allow for redirects, enabling deep linking and native app integrations. Some applications include: * **Desktop applications**: Use schemes like `{scheme}://` for native app integration * **Mobile apps**: Use schemes like `myapp://` for mobile app deep linking **Example custom schemes**: * `{scheme}://auth/callback` - For custom scheme authentication * `myapp://login/callback` - For mobile app authentication ## URI validation requirements [Section titled “URI validation requirements”](#uri-validation-requirements) Your redirect URIs must meet specific requirements that vary between development and production environments: | Requirement | Development | Production | | ----------------- | ---------------------------- | -------------------- | | Supported schemes | `http` `https` `{scheme}` | `https` `{scheme}` | | Localhost support | Allowed | Not allowed | | Wildcard domains | Allowed | Not allowed | | URI length limit | 256 characters | 256 characters | | Query parameters | Not allowed | Not allowed | | URL fragments | Not allowed | Not allowed | ### Wildcard usage patterns [Section titled “Wildcard usage patterns”](#wildcard-usage-patterns) Wildcards can simplify testing in development environments, but they must follow specific patterns: | Validation rule | Examples | | ------------------------------------------------ | -------------------------------------------------------------------- | | Wildcards cannot be used as root-level domains | `https://*.com``https://*.acmecorp.com``https://auth-*.acmecorp.com` | | Only one wildcard character is allowed per URI | `https://*.*.acmecorp.com``https://*.acmecorp.com` | | Wildcards must be in the hostname component only | `https://acmecorp.*.com``https://*.acmecorp.com` | | Wildcards must be in the outermost subdomain | `https://auth.*.acmecorp.com``https://*.auth.acmecorp.com` | Caution According to the [OAuth 2.0 specification](https://tools.ietf.org/html/rfc6749#section-3.1.2), redirect URIs must be absolute URIs. For development convenience, Scalekit relaxes this restriction slightly by allowing wildcards in development environments. ## Different redirect URL per organization [Section titled “Different redirect URL per organization”](#different-redirect-url-per-organization) If you need a different callback URL for each customer organization — for example, `https://auth.megasoft.com/oauth/callback` for Megasoft and `https://auth.betacorp.com/oauth/callback` for Beta Corp — register a single URL pattern with variables instead of registering one URL per organization. See [Organization-specific redirect URLs](/guides/dashboard/org-redirect-urls/) for the full setup. --- # DOCUMENT BOUNDARY --- # Integrations > Explore Scalekit's comprehensive integration capabilities with SSO providers, social connections, SCIM provisioning, and authentication systems. Explore integration guides for SSO, social logins, SCIM provisioning, and connecting with popular authentication systems. ## Single sign-on integrations [Section titled “Single sign-on integrations”](#single-sign-on--integrations) Configure organization IdPs and connect it to Scalekit to implement enterprise-grade authentication for your users. ### Okta - SAML Configure SSO with Okta using SAML protocol [Know more →](/guides/integrations/sso-integrations/okta-saml) ### Microsoft Entra ID - SAML Set up SSO with Microsoft Entra ID (Azure AD) using SAML [Know more →](/guides/integrations/sso-integrations/azure-ad-saml) ![JumpCloud - SAML logo](/assets/logos/jumpcloud.png) ### JumpCloud - SAML Implement SSO with JumpCloud using SAML [Know more →](/guides/integrations/sso-integrations/jumpcloud-saml) ![OneLogin - SAML logo](/assets/logos/onelogin.svg) ### OneLogin - SAML Configure SSO with OneLogin using SAML [Know more →](/guides/integrations/sso-integrations/onelogin-saml) ### Google Workspace - SAML Set up SSO with Google Workspace using SAML [Know more →](/guides/integrations/sso-integrations/google-saml) ![Ping Identity - SAML logo](/assets/logos/pingidentity.png) ### Ping Identity - SAML Configure SSO with Ping Identity using SAML [Know more →](/guides/integrations/sso-integrations/pingidentity-saml) ### Microsoft AD FS - SAML Set up SSO with Microsoft Active Directory Federation Services using SAML [Know more →](/guides/integrations/sso-integrations/microsoft-ad-fs) ![Shibboleth - SAML logo](/assets/logos/shibboleth.png) ### Shibboleth - SAML Set up SSO with Shibboleth using SAML [Know more →](/guides/integrations/sso-integrations/shibboleth-saml) ### Generic SAML Configure SSO with any SAML-compliant identity provider [Know more →](/guides/integrations/sso-integrations/generic-saml) ### Okta - OIDC Configure SSO with Okta using OpenID Connect [Know more →](/guides/integrations/sso-integrations/okta-oidc) ### Microsoft Entra ID - OIDC Set up SSO with Microsoft Entra ID using OpenID Connect [Know more →](/guides/integrations/sso-integrations/microsoft-entraid-oidc) ### Google Workspace - OIDC Set up SSO with Google Workspace using OpenID Connect [Know more →](/guides/integrations/sso-integrations/google-oidc) ![JumpCloud - OIDC logo](/assets/logos/jumpcloud.png) ### JumpCloud - OIDC Set up SSO with JumpCloud using OpenID Connect [Know more →](/guides/integrations/sso-integrations/jumpcloud-oidc) ![OneLogin - OIDC logo](/assets/logos/onelogin.svg) ### OneLogin - OIDC Set up SSO with OneLogin using OpenID Connect [Know more →](/guides/integrations/sso-integrations/onelogin-oidc) ![Ping Identity - OIDC logo](/assets/logos/pingidentity.png) ### Ping Identity - OIDC Set up SSO with Ping Identity using OpenID Connect [Know more →](/guides/integrations/sso-integrations/pingidentity-oidc) ### Generic OIDC Configure SSO with any OpenID Connect provider [Know more →](/guides/integrations/sso-integrations/generic-oidc) ## Social connections [Section titled “Social connections”](#social-connections) Enable users to sign in with their existing accounts from popular platforms. Social connections reduce signup friction and provide a familiar authentication experience. ### Google Enable Google account authentication using OAuth 2.0 [Know more →](/guides/integrations/social-connections/google) ### GitHub Allow authentication using GitHub credentials [Know more →](/guides/integrations/social-connections/github) ### Microsoft Integrate Microsoft accounts for user authentication [Know more →](/guides/integrations/social-connections/microsoft) ### GitLab Enable GitLab-based authentication [Know more →](/guides/integrations/social-connections/gitlab) ### LinkedIn Allow users to sign in with LinkedIn accounts [Know more →](/guides/integrations/social-connections/linkedin) ### Salesforce Enable Salesforce-based authentication [Know more →](/guides/integrations/social-connections/salesforce) ## SCIM Provisioning integrations [Section titled “SCIM Provisioning integrations”](#scim-provisioning-integrations) SCIM (System for Cross-domain Identity Management) automates user provisioning between identity providers and applications. These guides help you set up SCIM integration with various identity providers. ### Microsoft Entra ID (Azure AD) Automate user provisioning with Microsoft Entra ID [Know more →](/guides/integrations/scim-integrations/azure-scim) ### Okta Automate user provisioning with Okta [Know more →](/guides/integrations/scim-integrations/okta-scim) ![OneLogin logo](/assets/logos/onelogin.svg) ### OneLogin Automate user provisioning with OneLogin [Know more →](/guides/integrations/scim-integrations/onelogin) ![JumpCloud logo](/assets/logos/jumpcloud.png) ### JumpCloud Automate user provisioning with JumpCloud [Know more →](/guides/integrations/scim-integrations/jumpcloud) ### Google Workspace Automate user provisioning with Google Workspace [Know more →](/guides/integrations/scim-integrations/google-dir-sync/) ![PingIdentity logo](/assets/logos/pingidentity.png) ### PingIdentity Automate user provisioning with PingIdentity [Know more →](/guides/integrations/scim-integrations/pingidentity-scim) ### Generic SCIM Configure SCIM provisioning with any SCIM-compliant identity provider [Know more →](/guides/integrations/scim-integrations/generic-scim) ## Authentication system integrations [Section titled “Authentication system integrations”](#authentication-system-integrations) Scalekit can coexist with your existing authentication systems, allowing you to add enterprise SSO capabilities without replacing your current setup. These integrations show you how to configure Scalekit alongside popular authentication platforms. ### Auth0 Integrate Scalekit with Auth0 for enterprise SSO [Know more →](/guides/integrations/auth-systems/auth0) ### Firebase Auth Add enterprise authentication to Firebase projects [Know more →](/guides/integrations/auth-systems/firebase) ### AWS Cognito Configure Scalekit with AWS Cognito user pools [Know more →](/guides/integrations/auth-systems/aws-cognito) --- # DOCUMENT BOUNDARY --- # Auth0 > Learn how to integrate Scalekit with Auth0 for seamless Single Sign-On (SSO) authentication, allowing enterprise users to log in via Scalekit. This guide is designed to provide you a walkthrough of integrating Scalekit with Auth0, thereby facilitating seamless Single Sign-on (SSO) authentication for your application’s users. We demonstrate how to configure Scalekit so that Auth0 can allow some of your enterprise users to login via Scalekit and still continue to act as the identity management solution for your users and manage the login, session management functionality. ![Scalekit - Auth0 Integration ](/.netlify/images?url=_astro%2F0.BR2e1VI4.png\&w=3270\&h=954\&dpl=6ae471fc143c9a4c27550463) Scalekit is designed as a fully compatible OpenID Connect (OIDC) provider, thus streamlining the integration. As Auth0 continues to act as your identity management system, you’ll be able to seamlessly integrate Single Sign-on into your application without having to write code. Note Auth0 classifies OpenID Connect as Enterprise Connection and this feature is available only in the paid plans of Auth0. Please check whether your current plan has access to creating Enterprise Connections with OpenID Connect providers. Ensure you have: * Access to Auth0’s Authenticate dashboard. You need to have a role as an ‘Admin’ or ‘Editor - Connections’ to create and edit OIDC connections on Auth0 * Access to your Scalekit dashboard ## Add Scalekit as OIDC connection [Section titled “Add Scalekit as OIDC connection”](#add-scalekit-as-oidc-connection) Use [Auth0 Connections API](https://auth0.com/docs/api/management/v2/connections/post-connections) to create Scalekit as a OpenID connection for your tenant. Sample curl command below: ```bash curl --request POST \ --url 'https://.us.auth0.com/api/v2/connections' \ -H 'Content-Type: application/json' \ -H 'Accept: application/json' \ --header 'authorization: Bearer ' \ --data-raw '{ "strategy": "oidc", "name": "Scalekit", "options": { "type": "back_channel", "discovery_url": "/.well-known/openid-configuration", "client_secret" : "", "client_id" : "", "scopes": "openid profile email" } }' ``` Caution Because of an [existing issue](https://community.auth0.com/t/creating-an-oidc-connection-fails-with-options-issuer-is-required-error/128189) in adding OIDC connections via Auth0 Management Console, you need to use Auth0 API to create OIDC connection. | Parameter | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `AUTH0_TENANT_DOMAIN` | This is your Auth0 tenant url. Typically, looks like https\://yourapp.us.auth0.com | | `API_TOKEN` | [Generate an API token](https://auth0.com/docs/secure/tokens/access-tokens/management-api-access-tokens) from your Auth0 dashboard and use it to authenticate your Auth0 API calls | | `SCALEKIT_ENVIRONMENT_URL` | Find this in your [API config](https://app.scalekit.com) section of Scalekit Dashboard. For development use `https://{your-subdomain}.scalekit.dev`, for production use `https://{your-subdomain}.scalekit.com` | | `SCALEKIT_CLIENT_SECRET` | Generate a new client secret in your [API config](https://app.scalekit.com) section of Scalekit Dashboard and use that here | | `SCALEKIT_CLIENT_ID` | Find this in your [API config](https://app.scalekit.com) section of Scalekit Dashboard | After the successful execution of the above API call, you will see a new OpenID connection created in your Auth0 tenant. To confirm this, you can navigate to [Enterprise Connections](https://auth0.com/docs/authenticate/enterprise-connections#view-enterprise-connections) in your Auth0 dashboard. ## Register redirect URI in Scalekit [Section titled “Register redirect URI in Scalekit”](#register-redirect-uri-in-scalekit) After creating Scalekit as a new OIDC connection, you need to: 1. Copy the Callback URL from your Auth0 Dashboard 2. Add it as a new Allowed Callback URI in your Scalekit Authentication > Redirects section ## Copy callback URL from Auth0 [Section titled “Copy callback URL from Auth0”](#copy-callback-url-from-auth0) In your Auth0 dashboard, go to Authentication > Enterprise > OpenID Connect > Scalekit > Settings. Copy the “Callback URL” that’s available in the General section of settings. ![Copy Callback URL from your Auth0 Dashboard](/.netlify/images?url=_astro%2F1.BEM7Y6HL.png\&w=3154\&h=2154\&dpl=6ae471fc143c9a4c27550463) ## Set redirect URI in Scalekit API config [Section titled “Set redirect URI in Scalekit API config”](#set-redirect-uri-in-scalekit-api-config) Go to your Scalekit dashboard. Select environment as Development or Production. Navigate to **Authentication** > **Redirects** > **Allowed Callback URIs**. In the Allowed Callback URIs section, select **Add new URI**. Paste the Callback URL that you copied from Auth0 dashboard. Click on Add button. ![Add new Redirect URI in Scalekit Dashboard](/.netlify/images?url=_astro%2Fscreenshot.Dmtybz_t.png\&w=1422\&h=717\&dpl=6ae471fc143c9a4c27550463) ## Onboard Single Sign-on customers in Scalekit [Section titled “Onboard Single Sign-on customers in Scalekit”](#onboard-single-sign-on-customers-in-scalekit) To onboard new enterprise customers using Single Sign-on login, you need to: 1. Create an Organization in Scalekit 2. Generate Admin Portal link to allow your customers configure SSO settings 3. Configure Domain in the Scalekit dashboard for that Organization 4. Update Home Realm Discovery settings in your Auth0 tenant with this Organization’s domain ## Update home realm discovery in Auth0 [Section titled “Update home realm discovery in Auth0”](#update-home-realm-discovery-in-auth0) In step 2, you have successfully configured Scalekit as an OIDC connection in your Auth0 tenant. It’s time to enable Home Realm Discovery for your enterprise customers in Auth0. This configuration will help Auth0 determine which users to be routed to login via Single Sign-on. In your Auth0 dashboard, go to Authentication > Enterprise > OpenID Connect > Scalekit > Login Experience. Navigate to “Home Realm Discovery” in the Login Experience Customization section. In the Identity Provider domains, add the comma separated list of domains that need to be authenticated with Single Sign-on via Scalekit. Auth0 uses this configuration to compare the users email domain at the time of login: * If there is a match in the configured domains, users will be redirected to the Scalekit’s Single Sign-on * If there is no match, users will be prompted to login via other authentication methods like password or Magic Link & OTP based on your Auth0 configuration For example, if you would like users from three Organizations (FooCorp, BarCorp, AcmeCorp) to access your application using their respective identity providers, you need to add them as a comma separated list foocorp.com, barcorp.com, acmecorp.com. Screenshot below for reference ![Add domains for Home Realm Discovery in Auth0](/.netlify/images?url=_astro%2F3.BFtPgz8x.png\&w=2796\&h=1670\&dpl=6ae471fc143c9a4c27550463) **Save** the Home Realm Discovery settings. You have now successfully integrated Scalekit with Auth0, thereby facilitating seamless SSO authentication for your application’s users. --- # DOCUMENT BOUNDARY --- # AWS Cognito > Learn how to integrate Scalekit with AWS Cognito as an OIDC provider for seamless enterprise Single Sign-On (SSO) authentication. Expand your existing AWS Cognito authentication system by integrating Scalekit as an OpenID Connect (OIDC) provider. This integration enables enterprise users to log into your application seamlessly using Single Sign-On (SSO). ![Integration flow: your application connects to AWS Cognito over OpenID Connect, then to Scalekit, which links to customer identity providers via SAML and OpenID Connect](/.netlify/images?url=_astro%2F0.vqDHIV-X.png\&w=3270\&h=954\&dpl=6ae471fc143c9a4c27550463) Here’s a typical flow illustrating the integration: 1. **User initiates login**: Enterprise users enter their company email address on your application’s custom login page (not managed by AWS Cognito) to initiate SSO 2. **Authentication via Scalekit**: Based on identifiers such as the user’s company email and Scalekit’s connection identifier, users are redirected to authenticate through their organization’s Identity Provider (IdP) Prefer exploring an example app? Check out this [Next.js example on GitHub](https://github.com/scalekit-developers/nextjs-example-apps/tree/main/cognito-scalekit) ## Configure Scalekit as an OIDC provider in AWS Cognito [Section titled “Configure Scalekit as an OIDC provider in AWS Cognito”](#configure-scalekit-as-an-oidc-provider-in-aws-cognito) To enable AWS Cognito to redirect users to Scalekit for SSO initiation, configure your Scalekit account as an OIDC provider within AWS Cognito: 1. Navigate to **AWS Cognito** and select your existing **User Pool** 2. Under the **Authentication** section, choose **Social and external providers** 3. Click **Add identity provider > OpenID Connect (OIDC)** AWS Cognito will display a form requiring specific details to establish the connection with Scalekit: ![Scalekit - AWS Cognito Integration](/.netlify/images?url=_astro%2F1.sOx18KK4.png\&w=2048\&h=1072\&dpl=6ae471fc143c9a4c27550463) AWS Cognito - Add Identity Provider | **Field** | **Description** | | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Provider Name | A recognizable label for Scalekit within the AWS ecosystem. This name is used programmatically when generating authorization URLs. For example: `ScalekitIdPRouter` | | Client ID | Obtain this from your Scalekit Dashboard under **Authentication** > **Redirects** > **Allowed Callback URIs** | | Client Secret | Generate a secret from your Scalekit Dashboard (**Authentication** > **Redirects** > **Allowed Callback URIs**) and input it here | | Authorized Scopes | Scopes defining the user attributes that AWS Cognito can access from Scalekit | | Identifiers | Identifiers instruct AWS Cognito to check user-entered email addresses during sign-in and direct users accordingly to the associated identity provider based on their domain | | Attribute Request Method | Method used to exchange attributes and generate tokens for users; ensure you map Scalekit’s user attributes correctly to your user pool attributes in AWS Cognito | | Issuer URL | Enter your Scalekit environment URL found in the Scalekit Dashboard under **Authentication** > **Redirects** > **Allowed Callback URIs**. For development use `https://{your-subdomain}.scalekit.dev` and for production use `https://{your-subdomain}.scalekit.com` | Scalekit’s profile information includes various user attributes useful for your application requirements. Map these attributes between both providers using the attribute list found at **Scalekit Dashboard > Authentication > Single Sign-On**. This ensures standardized information exchange between your customers’ identity providers and your application. ![Scalekit - AWS Cognito Integration](/.netlify/images?url=_astro%2F2.BFLDa-7t.png\&w=2048\&h=1120\&dpl=6ae471fc143c9a4c27550463) The same attribute names are considered OpenID Connect attributes within AWS Cognito, streamlining user profile synchronization between your app and identity providers. ![Scalekit - AWS Cognito Integration](/.netlify/images?url=_astro%2F3.C3utCsuA.png\&w=2048\&h=1119\&dpl=6ae471fc143c9a4c27550463) Click **Add identity provider** to complete adding Scalekit as an identity provider. ## Implement Single Sign-On in your application [Section titled “Implement Single Sign-On in your application”](#implement-single-sign-on-in-your-application) Your application should use its own custom login page instead of the managed login page provided by AWS Cognito. This approach allows you to collect enterprise users’ email addresses and redirect them appropriately for authentication via SSO. ![Scalekit - AWS Cognito Integration](/.netlify/images?url=_astro%2F4.ClJKzgig.png\&w=1356\&h=764\&dpl=6ae471fc143c9a4c27550463) Generate an authorization URL with two additional parameters, `identity_provider` and `login_hint`, to redirect users seamlessly: Example Code ```typescript import { Issuer, Client } from "openid-client"; const client = await getOidcClient(); const authUrl = client.authorizationUrl({ scope: "openid email", state: state, nonce: nonce, identity_provider: "ScalekitIdPRouter", // Same as Provider name (above) login_hint: email, // User's company email address }); console.log("authUrl", authUrl); const response = NextResponse.redirect(authUrl); ``` ### Example authorization endpoint URL [Section titled “Example authorization endpoint URL”](#example-authorization-endpoint-url) Here’s an example of a complete authorization endpoint URL incorporating the required parameters: ```sh https://[domain].auth.[region].amazoncognito.com/oauth2/authorize ?client_id=k6tana1l8b0bvhk9gfixkurr6 &scope=openid%20email &response_type=code &redirect_uri=http%3A%2F%2Flocalhost%3A3000%2Fauth%2Fcallback &state=-5iLRZmPwwdqwqT-A4yiJM6KQvCLQM0JRx9QaXOlzRE &nonce=sGSXePnJ0Ue5GZyTpKG4rRsVeWyfZloImbMWunUDbG4 &identity_provider=ScalekitIdPRouter &login_hint=enterpriseuser%40example.org ``` For ease of development, Scalekit supports testing with `@example.org` and `@example.com` domains. Authorization endpoints generated using these domains as `login_hint` will redirect enterprise users to Scalekit’s built-in IdP Simulator. ![Scalekit - AWS Cognito Integration](/.netlify/images?url=_astro%2F5.CZPyx7vZ.png\&w=2048\&h=1306\&dpl=6ae471fc143c9a4c27550463) Treat the IdP Simulator as equivalent to an actual organization’s IdP authentication step. For instance, if John belongs to Megasoft (using Okta as their IdP), logging in with `john@megasoft.org` would redirect him to Okta’s authentication process (including MFA or other organizational policies). Scalekit integrates seamlessly with [major identity providers](/guides/integrations/sso-integrations/). Use Scalekit’s [Admin Portal](/authenticate/sso/admin-portal/) to onboard enterprise customers, enabling them to set up connections between their identity providers and your application. Note The domain of your enterprise customer should be added to the list of identifiers in the AWS Cognito > User Pool > Authentication > Social and external providers > \[ScalekitIdPRouter] > Identifiers ### Successful SSO response [Section titled “Successful SSO response”](#successful-sso-response) Upon successful authentication via SSO, your application receives user profile details mapped according to AWS Cognito’s configured user attributes: Successful SSO response ```json { "sub": "807c593c-d0c1-709c-598f-633ec61bcc8b", "email_verified": "false", "email": "john@example.com", "username": "scalekitIdPRouter_conn_60040666217971987;a2c49d97-d36f-460f-97c2-87eb295095af" } ``` Now that you’ve successfully integrated AWS Cognito with Scalekit for SSO, here are some recommended next steps. Onboard Enterprise Customers using the Scalekit Admin Portal to help customers configure their identity providers. --- # DOCUMENT BOUNDARY --- # Co-exist with Firebase > Learn how to integrate Scalekit with Firebase for enterprise SSO, using either Firebase's OIDC provider or direct SSO with custom tokens. This guide explains how to integrate Scalekit with Firebase applications for enterprise Single Sign-On (SSO) authentication. You’ll learn two distinct approaches based on your Firebase Authentication setup. ![Scalekit - Firebase Integration](/.netlify/images?url=_astro%2F0.yumx0AEz.png\&w=3270\&h=954\&dpl=6ae471fc143c9a4c27550463) ## Before you begin [Section titled “Before you begin”](#before-you-begin) Review your Firebase Authentication setup to determine which integration approach suits your application: * **Option 1**: Requires Firebase Authentication with Identity Platform (paid tier) * **Option 2**: Works with Legacy Firebase Authentication (free tier) You also need: * Access to a [Scalekit account](https://app.scalekit.com) * Firebase project with Authentication enabled * Basic understanding of [Firebase Admin SDK](https://firebase.google.com/docs/reference/admin) (for Option 2) Checkout our [Firebase integration example](https://github.com/scalekit-inc/scalekit-firebase-sso) for a complete implementation. ## Option 1: Configure Scalekit as an OIDC Provider [Section titled “Option 1: Configure Scalekit as an OIDC Provider”](#option-1-configure-scalekit-as-an-oidc-provider) Use this approach if you have **Firebase Authentication with Identity Platform**. Firebase acts as an OpenID Connect (OIDC) relying party that integrates directly with Scalekit. Note OpenID Connect providers are not available in Legacy Firebase Authentication. See the [Firebase product comparison](https://cloud.google.com/identity-platform/docs/product-comparison) for details. ![Firebase with Scalekit as an OIDC provider: Firebase delegates login to Scalekit, which connects to the identity provider](/d2/docs/guides/integrations/auth-systems/firebase-0.svg) Firebase handles the OAuth 2.0 flow automatically using its built-in OIDC provider support. 1. #### Configure Firebase to accept Scalekit as an identity provider [Section titled “Configure Firebase to accept Scalekit as an identity provider”](#configure-firebase-to-accept-scalekit-as-an-identity-provider) Log in to the [Firebase Console](https://console.firebase.google.com/) and navigate to your project. * Go to **Authentication** > **Sign-in method** * Click **Add new provider** and select **OpenID Connect** * Set the **Name** to “Scalekit” * Choose **Code flow** for the **Grant Type** ![Sign-in tab in your Firebase Console](/.netlify/images?url=_astro%2F1.CzGhJ8GY.png\&w=2952\&h=2474\&dpl=6ae471fc143c9a4c27550463) 2. #### Copy your Scalekit API credentials [Section titled “Copy your Scalekit API credentials”](#copy-your-scalekit-api-credentials) In your Scalekit Dashboard, navigate to **Settings** > **API Config** and copy these values: * **Client ID**: Your Scalekit application identifier * **Environment URL**: Your Scalekit environment (e.g., `https://your-subdomain.scalekit.dev`) * **Client Secret**: Generate a new secret if needed ![Scalekit API Configuration](/.netlify/images?url=_astro%2F2.DW5ajBz2.png\&w=3380\&h=2474\&dpl=6ae471fc143c9a4c27550463) 3. #### Connect Firebase to Scalekit using your API credentials [Section titled “Connect Firebase to Scalekit using your API credentials”](#connect-firebase-to-scalekit-using-your-api-credentials) In Firebase Console, paste the Scalekit values into the corresponding fields: * **Client ID**: Paste your Scalekit Client ID * **Issuer URL**: Paste your Scalekit Environment URL * **Client Secret**: Paste your Scalekit Client Secret ![Firebase OIDC Provider Configuration](/.netlify/images?url=_astro%2F3.B8I5cBOV.png\&w=3380\&h=2474\&dpl=6ae471fc143c9a4c27550463) 4. #### Allow Firebase to redirect users back to your app [Section titled “Allow Firebase to redirect users back to your app”](#allow-firebase-to-redirect-users-back-to-your-app) Copy the **Callback URL** from your Firebase OIDC Integration settings. ![Firebase Callback URL](/.netlify/images?url=_astro%2F4.BgGZ4s_j.png\&w=3380\&h=2474\&dpl=6ae471fc143c9a4c27550463) Add this URL as a **Allowed Callback URI** in your Scalekit Authentication > Redirects section. ![Scalekit Redirect URI Configuration](/.netlify/images?url=_astro%2F5.Df1HXppc.png\&w=3380\&h=2474\&dpl=6ae471fc143c9a4c27550463) 5. #### Configure allowed callback URIs in Scalekit [Section titled “Configure allowed callback URIs in Scalekit”](#configure-allowed-callback-uris-in-scalekit) In your Scalekit Dashboard, navigate to **Authentication** > **Redirects** > **Allowed Callback URIs**. Add your Firebase callback URL to the allowed callback URIs list: * **For development**: `https://your-firebase-domain.com/__/auth/handler` * **For production**: `https://your-domain.com/__/auth/handler` Note Firebase automatically generates the callback URL format. Make sure to use the exact URL provided by Firebase in your OIDC provider configuration. 6. #### Add SSO login to your frontend code [Section titled “Add SSO login to your frontend code”](#add-sso-login-to-your-frontend-code) Use Firebase’s standard OIDC authentication in your frontend: Login Implementation ```javascript 1 import { getAuth, OAuthProvider, signInWithPopup } from 'firebase/auth'; 2 3 const auth = getAuth(); 4 5 // Initialize Scalekit as an OIDC provider 6 const scalekitProvider = new OAuthProvider('oidc.scalekit'); 7 8 // Set SSO parameters 9 scalekitProvider.setCustomParameters({ 10 domain: 'customer@company.com', // or organization_id, connection_id 11 }); 12 13 // Handle SSO login 14 const loginButton = document.getElementById('sso-login'); 15 loginButton.onclick = async () => { 16 try { 17 const result = await signInWithPopup(auth, scalekitProvider); 18 const user = result.user; 19 20 console.log('Authenticated user:', user.email); 21 // User is now signed in to Firebase 22 } catch (error) { 23 console.error('Authentication failed:', error); 24 } 25 }; ``` ## Option 2: Direct SSO with Custom Tokens [Section titled “Option 2: Direct SSO with Custom Tokens”](#option-2-direct-sso-with-custom-tokens) Use this approach if you have **Legacy Firebase Authentication** or need full control over the authentication flow. Your backend integrates directly with Scalekit and creates custom Firebase tokens. View authentication flow summary ![Firebase direct SSO with custom tokens: your app completes SSO with Scalekit and signs the user in to Firebase with a custom token](/d2/docs/guides/integrations/auth-systems/firebase-1.svg) Your backend handles SSO authentication and creates custom tokens for Firebase. 1. #### Install Scalekit and Firebase Admin SDKs [Section titled “Install Scalekit and Firebase Admin SDKs”](#install-scalekit-and-firebase-admin-sdks) Install the Scalekit SDK and configure your backend server with Firebase Admin SDK: ```bash npm install @scalekit-sdk/node firebase-admin ``` backend/server.js ```javascript import { ScalekitClient } from '@scalekit-sdk/node'; import admin from 'firebase-admin'; // Initialize Scalekit const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL, process.env.SCALEKIT_CLIENT_ID, process.env.SCALEKIT_CLIENT_SECRET ); // Initialize Firebase Admin ``` 2. #### Handle SSO callback and create Firebase tokens [Section titled “Handle SSO callback and create Firebase tokens”](#handle-sso-callback-and-create-firebase-tokens) Implement the SSO callback handler that exchanges the authorization code for user details and creates custom Firebase tokens: SSO Callback Handler ```javascript 1 app.get('/auth/callback', async (req, res) => { 2 const { code, error, error_description } = req.query; 3 4 if (error) { 5 return res.status(400).json({ 6 error: 'Authentication failed', 7 details: error_description 8 }); 9 } 10 11 try { 12 // Exchange code for user profile 13 const result = await scalekit.authenticateWithCode( 14 code, 15 'https://your-app.com/auth/callback' 16 ); 17 18 const user = result.user; 19 20 // Create custom Firebase token 21 const customToken = await admin.auth().createCustomToken(user.id, { 22 email: user.email, 23 name: `${user.givenName} ${user.familyName}`, 24 organizationId: user.organizationId, 25 }); 26 27 res.json({ 28 customToken, 29 user: { 30 email: user.email, 31 name: `${user.givenName} ${user.familyName}`, 32 } 33 }); 34 } catch (error) { 35 console.error('SSO authentication failed:', error); 36 res.status(500).json({ error: 'Internal server error' }); 37 } 38 }); ``` 3. #### Generate authorization URL to initiate SSO [Section titled “Generate authorization URL to initiate SSO”](#generate-authorization-url-to-initiate-sso) Create an endpoint to generate Scalekit authorization URLs: * Node.js Authorization URL Endpoint ```javascript 1 app.post('/auth/start-sso', async (req, res) => { 2 const { organizationId, domain, connectionId } = req.body; 3 4 try { 5 const options = {}; 6 if (organizationId) options.organizationId = organizationId; 7 if (domain) options.domain = domain; 8 if (connectionId) options.connectionId = connectionId; 9 10 const authorizationUrl = scalekit.getAuthorizationUrl( 11 'https://your-app.com/auth/callback', 12 options 13 ); 14 15 res.json({ authorizationUrl }); 16 } catch (error) { 17 console.error('Failed to generate authorization URL:', error); 18 res.status(500).json({ error: 'Internal server error' }); 19 } 20 }); ``` * Python Authorization URL Endpoint ```python 1 @app.route('/auth/start-sso', methods=['POST']) 2 def start_sso(): 3 data = request.get_json() 4 organization_id = data.get('organizationId') 5 domain = data.get('domain') 6 connection_id = data.get('connectionId') 7 8 try: 9 options = {} 10 if organization_id: 11 options['organization_id'] = organization_id 12 if domain: 13 options['domain'] = domain 14 if connection_id: 15 options['connection_id'] = connection_id 16 17 authorization_url = scalekit.get_authorization_url( 18 'https://your-app.com/auth/callback', 19 options 20 ) 21 22 return jsonify({'authorizationUrl': authorization_url}) 23 except Exception as e: 24 print(f'Failed to generate authorization URL: {e}') 25 return jsonify({'error': 'Internal server error'}), 500 ``` * Go Authorization URL Endpoint ```go 1 func startSSOHandler(w http.ResponseWriter, r *http.Request) { 2 var requestData struct { 3 OrganizationID string `json:"organizationId"` 4 Domain string `json:"domain"` 5 ConnectionID string `json:"connectionId"` 6 } 7 8 if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil { 9 http.Error(w, "Invalid request body", http.StatusBadRequest) 10 return 11 } 12 13 options := scalekit.AuthorizationUrlOptions{} 14 if requestData.OrganizationID != "" { 15 options.OrganizationId = requestData.OrganizationID 16 } 17 if requestData.Domain != "" { 18 options.Domain = requestData.Domain 19 } 20 if requestData.ConnectionID != "" { 21 options.ConnectionId = requestData.ConnectionID 22 } 23 24 authorizationURL := scalekitClient.GetAuthorizationUrl( 25 "https://your-app.com/auth/callback", 26 options, 27 ) 28 29 response := map[string]string{ 30 "authorizationUrl": authorizationURL, 31 } 32 33 w.Header().Set("Content-Type", "application/json") 34 json.NewEncoder(w).Encode(response) 35 } ``` * Java Authorization URL Endpoint ```java 1 @PostMapping("/auth/start-sso") 2 public ResponseEntity startSSO(@RequestBody Map request) { 3 String organizationId = request.get("organizationId"); 4 String domain = request.get("domain"); 5 String connectionId = request.get("connectionId"); 6 7 try { 8 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 9 if (organizationId != null) options.setOrganizationId(organizationId); 10 if (domain != null) options.setDomain(domain); 11 if (connectionId != null) options.setConnectionId(connectionId); 12 13 String authorizationUrl = scalekitClient.authentication() 14 .getAuthorizationUrl("https://your-app.com/auth/callback", options) 15 .toString(); 16 17 return ResponseEntity.ok(Map.of("authorizationUrl", authorizationUrl)); 18 } catch (Exception e) { 19 System.err.println("Failed to generate authorization URL: " + e.getMessage()); 20 return ResponseEntity.status(500).body(Map.of("error", "Internal server error")); 21 } 22 } ``` 4. #### Build frontend SSO flow with custom tokens [Section titled “Build frontend SSO flow with custom tokens”](#build-frontend-sso-flow-with-custom-tokens) Create the frontend flow that initiates SSO and handles the custom token: Frontend SSO Implementation ```javascript 1 import { getAuth, signInWithCustomToken } from 'firebase/auth'; 2 3 const auth = getAuth(); 4 5 // Initiate SSO flow 6 const initiateSSO = async () => { 7 try { 8 // Get authorization URL from your backend 9 const response = await fetch('/auth/start-sso', { 10 method: 'POST', 11 headers: { 'Content-Type': 'application/json' }, 12 body: JSON.stringify({ 13 organizationId: 'org_123456789', // or domain, connectionId 14 }), 15 }); 16 17 const { authorizationUrl } = await response.json(); 18 19 // Redirect to SSO 20 window.location.href = authorizationUrl; 21 } catch (error) { 22 console.error('Failed to initiate SSO:', error); 23 } 24 }; 25 26 // Handle SSO callback (call this on your callback page) 27 const handleSSOCallback = async () => { 28 const urlParams = new URLSearchParams(window.location.search); 29 const code = urlParams.get('code'); 30 const error = urlParams.get('error'); 31 32 if (error) { 33 console.error('SSO failed:', error); 34 return; 35 } 36 37 try { 38 // Exchange code for custom token 39 const response = await fetch(`/auth/callback?code=${code}`); 40 const { customToken, user } = await response.json(); 41 42 // Sign in to Firebase with custom token 43 const userCredential = await signInWithCustomToken(auth, customToken); 44 const firebaseUser = userCredential.user; 45 46 console.log('Successfully authenticated:', firebaseUser); 47 48 // Redirect to your app 49 window.location.href = '/dashboard'; 50 } catch (error) { 51 console.error('Authentication failed:', error); 52 } 53 }; ``` ## Handle identity provider-initiated SSO [Section titled “Handle identity provider-initiated SSO”](#handle-identity-provider-initiated-sso) Both approaches support IdP-initiated SSO, where users access your application directly from their identity provider portal. Create a dedicated endpoint to handle these requests. For detailed implementation instructions, refer to the [IdP-Initiated SSO guide](/sso/guides/idp-init-sso/). Both approaches provide secure, enterprise-grade SSO authentication while maintaining compatibility with Firebase’s ecosystem and features. --- # DOCUMENT BOUNDARY --- # Pre-check SSO by domain > Validate that a user's email domain has an active SSO connection before redirecting to prevent dead-end redirects and improve user experience. When using discovery through `loginHint`, validate that the user’s email domain has an active SSO connection before redirecting. This prevents dead-end redirects and improves user experience by routing users to the correct authentication path. ## When to use domain pre-checking [Section titled “When to use domain pre-checking”](#when-to-use-domain-pre-checking) Use domain pre-checking when: * You implement identifier-driven or SSO button flows that collect email first * You infer SSO availability from the user’s email domain * You want to show helpful error messages for domains without SSO Skip this check when: * You already pass `organizationId` explicitly (you know the organization) * You implement organization-specific pages where SSO is always available ## Implementation workflow [Section titled “Implementation workflow”](#implementation-workflow) 1. ## Capture the user’s email and extract the domain [Section titled “Capture the user’s email and extract the domain”](#capture-the-users-email-and-extract-the-domain) First, collect the user’s email address through your login form. Login form handler ```javascript // Extract domain from user's email const email = req.body.email; const domain = email.split('@')[1]; // e.g., "acmecorp.com" ``` 2. ## Query for SSO connections by domain [Section titled “Query for SSO connections by domain”](#query-for-sso-connections-by-domain) Use the Scalekit API to check if the domain has an active SSO connection configured. * Node.js Express.js ```javascript 1 // Use case: Check if user's domain has SSO before redirecting 2 app.post('/auth/check-sso', async (req, res) => { 3 const { email } = req.body; 4 const domain = email.split('@')[1]; 5 6 try { 7 // Query Scalekit for connections matching this domain 8 const connections = await scalekit.connection.listConnections({ 9 domain: domain 10 }); 11 12 if (connections.length > 0) { 13 // Domain has active SSO - redirect to SSO login 14 const authorizationURL = scalekit.getAuthorizationUrl( 15 process.env.REDIRECT_URI, 16 { loginHint: email } 17 ); 18 res.json({ ssoAvailable: true, redirectUrl: authorizationURL }); 19 } else { 20 // No SSO configured - route to password or social login 21 res.json({ ssoAvailable: false, message: 'Please use password login' }); 22 } 23 } catch (error) { 24 console.error('Failed to check SSO availability:', error); 25 res.status(500).json({ error: 'sso_check_failed' }); 26 } 27 }); ``` * Python Flask ```python 1 # Use case: Check if user's domain has SSO before redirecting 2 @app.route('/auth/check-sso', methods=['POST']) 3 def check_sso(): 4 data = request.get_json() 5 email = data.get('email') 6 domain = email.split('@')[1] 7 8 try: 9 # Query Scalekit for connections matching this domain 10 connections = scalekit_client.connection.list_connections( 11 domain=domain 12 ) 13 14 if len(connections) > 0: 15 # Domain has active SSO - redirect to SSO login 16 options = AuthorizationUrlOptions() 17 options.login_hint = email 18 authorization_url = scalekit_client.get_authorization_url( 19 redirect_uri=os.getenv("REDIRECT_URI"), 20 options=options 21 ) 22 return jsonify({ 23 'ssoAvailable': True, 24 'redirectUrl': authorization_url 25 }) 26 else: 27 # No SSO configured - route to password or social login 28 return jsonify({ 29 'ssoAvailable': False, 30 'message': 'Please use password login' 31 }) 32 except Exception as error: 33 print(f"Failed to check SSO availability: {error}") 34 return jsonify({'error': 'sso_check_failed'}), 500 ``` * Go Gin ```go 1 // Use case: Check if user's domain has SSO before redirecting 2 func checkSSOHandler(c *gin.Context) { 3 var body struct { 4 Email string `json:"email"` 5 } 6 c.BindJSON(&body) 7 8 domain := strings.Split(body.Email, "@")[1] 9 10 // Query Scalekit for connections matching this domain 11 connections, err := scalekitClient.Connection.ListConnections( 12 &scalekit.ListConnectionsOptions{ 13 Domain: domain, 14 }, 15 ) 16 17 if err != nil { 18 log.Printf("Failed to check SSO availability: %v", err) 19 c.JSON(http.StatusInternalServerError, gin.H{"error": "sso_check_failed"}) 20 return 21 } 22 23 if len(connections) > 0 { 24 // Domain has active SSO - redirect to SSO login 25 authorizationURL, _ := scalekitClient.GetAuthorizationUrl( 26 os.Getenv("REDIRECT_URI"), 27 scalekit.AuthorizationUrlOptions{ 28 LoginHint: body.Email, 29 }, 30 ) 31 c.JSON(http.StatusOK, gin.H{ 32 "ssoAvailable": true, 33 "redirectUrl": authorizationURL, 34 }) 35 } else { 36 // No SSO configured - route to password or social login 37 c.JSON(http.StatusOK, gin.H{ 38 "ssoAvailable": false, 39 "message": "Please use password login", 40 }) 41 } 42 } ``` * Java Spring Boot ```java 1 // Use case: Check if user's domain has SSO before redirecting 2 @PostMapping(path = "/auth/check-sso") 3 public ResponseEntity> checkSSOHandler(@RequestBody CheckSSORequest body) { 4 String email = body.getEmail(); 5 String domain = email.split("@")[1]; 6 7 try { 8 // Query Scalekit for connections matching this domain 9 ListConnectionsResponse connections = scalekitClient 10 .connection() 11 .listConnections( 12 new ListConnectionsOptions().setDomain(domain) 13 ); 14 15 if (!connections.getConnections().isEmpty()) { 16 // Domain has active SSO - redirect to SSO login 17 String authorizationURL = scalekitClient 18 .authentication() 19 .getAuthorizationUrl( 20 System.getenv("REDIRECT_URI"), 21 new AuthorizationUrlOptions().setLoginHint(email) 22 ) 23 .toString(); 24 25 Map response = new HashMap<>(); 26 response.put("ssoAvailable", true); 27 response.put("redirectUrl", authorizationURL); 28 return ResponseEntity.ok(response); 29 } else { 30 // No SSO configured - route to password or social login 31 Map response = new HashMap<>(); 32 response.put("ssoAvailable", false); 33 response.put("message", "Please use password login"); 34 return ResponseEntity.ok(response); 35 } 36 } catch (Exception error) { 37 System.err.println("Failed to check SSO availability: " + error.getMessage()); 38 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) 39 .body(Collections.singletonMap("error", "sso_check_failed")); 40 } 41 } ``` 3. ## Route users based on SSO availability [Section titled “Route users based on SSO availability”](#route-users-based-on-sso-availability) Based on the API response, either redirect to SSO or show alternative authentication options. Client-side routing ```javascript 1 // Handle the response from your backend 2 const response = await fetch('/auth/check-sso', { 3 method: 'POST', 4 headers: { 'Content-Type': 'application/json' }, 5 body: JSON.stringify({ email: userEmail }) 6 }); 7 8 const data = await response.json(); 9 10 if (data.ssoAvailable) { 11 // Redirect to SSO login 12 window.location.href = data.redirectUrl; 13 } else { 14 // Show password login or social authentication options 15 showPasswordLoginForm(); 16 } ``` Note This API returns results only when organizations have configured their domains in Scalekit through **Dashboard > Organizations > \[Organization] > Domains**. See the [connections API reference](https://docs.scalekit.com/apis/#tag/connections/GET/api/v1/connections) for complete details. --- # DOCUMENT BOUNDARY --- # Set up SCIM connection > Set up a SCIM connection to your directory provider Scalekit supports user provisioning based on the [SCIM protocol](/directory/guides/user-provisioning-basics/). This allows your customers to manage their users automatically through directory providers, simplifying user access and revocation to your app when their employees join or leave an organization. By configuring their directory provider with your app via the Scalekit admin portal, customers can ensure seamless user management. 1. ## Enable SCIM provisioning for the organization [Section titled “Enable SCIM provisioning for the organization”](#enable-scim-provisioning-for-the-organization) The SCIM provisioning feature should be enabled for that particular organization. You can manually do this via the Scalekit dashboard > organization > overview. The other way, is to provide an option in your app so that organization admins (customers) can enable it within your app. Here’s how you can do that with Scalekit. Use the following SDK method to enable SCIM provisioning for the organization: * Node.js Enable SCIM ```javascript const settings = { features: [ { name: 'scim', enabled: true, } ], }; await scalekit.organization.updateOrganizationSettings( '', // Get this from the idToken or accessToken settings ); ``` * Python Enable SCIM ```python settings = [ { "name": "scim", "enabled": True } ] scalekit.organization.update_organization_settings( organization_id='', # Get this from the idToken or accessToken settings=settings ) ``` * Java Enable SCIM ```java OrganizationSettingsFeature featureSCIM = OrganizationSettingsFeature.newBuilder() .setName("scim") .setEnabled(true) .build(); updatedOrganization = scalekitClient.organizations() .updateOrganizationSettings(organizationId, List.of(featureSCIM)); ``` * Go Enable SCIM ```go settings := OrganizationSettings{ Features: []Feature{ { Name: "scim", Enabled: true, }, }, } organization, err := sc.Organization().UpdateOrganizationSettings(ctx, organizationId, settings) if err != nil { // Handle error } ``` Alternatively, enable SCIM provisioning from the Scalekit dashboard: navigate to Organizations, open the menu (⋯) for an organization, and check SCIM provisioning. 2. ## Enable admin portal for enterprise customer onboarding [Section titled “Enable admin portal for enterprise customer onboarding”](#enable-admin-portal-for-enterprise-customer-onboarding) After SCIM provisioning is enabled for that organization, provide a method for configuring a SCIM connection with the organization’s identity provider. Scalekit offers two primary approaches: * Generate a link to the admin portal from the Scalekit dashboard and share it with organization admins via your usual channels. * Or embed the admin portal in your application in an inline frame so administrators can configure their IdP without leaving your app. [See how to onboard enterprise customers](/directory/guides/onboard-enterprise-customers/) 3. ## Test your SCIM integration [Section titled “Test your SCIM integration”](#test-your-scim-integration) To verify that SCIM provisioning is working correctly, create a new user in the directory provider and confirm that it is automatically created in the Scalekit organization’s user list. To programmatically list the connected directories in your app, use the following SDK methods: * Node.js List connected directories ```javascript const { directories } = await scalekit.directory.listDirectories(''); ``` * Python List connected directories ```python directories = scalekit_client.directory.list_directories(organization_id='') ``` * Java List connected directories ```java ListDirectoriesResponse response = scalekitClient.directories().listDirectories(organizationId); ``` * Go List connected directories ```go directories, err := sc.Directory().ListDirectories(ctx, organizationId) ``` The response will be a list of connected directories, similar to the following: List connected directories response ```json { "directories": [ { "attribute_mappings": { "attributes": [] }, "directory_endpoint": "https://yourapp.scalekit.com/api/v1/directoies/dir_123212312/scim/v2", "directory_provider": "OKTA", "directory_type": "SCIM", "email": "john.doe@scalekit.cloud", "enabled": true, "groups_tracked": "ALL", "id": "dir_121312434123312", "last_synced_at": "2024-10-01T00:00:00Z", "name": "Azure AD", "organization_id": "org_121312434123312", "role_assignments": { "assignments": [ { "group_id": "dirgroup_121312434123", "role_name": "string" } ] }, "secrets": [ { "create_time": "2024-10-01T00:00:00Z", "directory_id": "dir_12362474900684814", "expire_time": "2025-10-01T00:00:00Z", "id": "string", "last_used_time": "2024-10-01T00:00:00Z", "secret_suffix": "Nzg5", "status": "INACTIVE" } ], "stats": { "group_updated_at": "2024-10-01T00:00:00Z", "total_groups": 10, "total_users": 10, "user_updated_at": "2024-10-01T00:00:00Z" }, "status": "IN_PROGRESS", "total_groups": 10, "total_users": 10 } ] } ``` 4. ## Enterprise users are now automatically provisioned your app [Section titled “Enterprise users are now automatically provisioned your app”](#enterprise-users-are-now-automatically-provisioned-your-app) Scalekit automatically provisions and synchronizes users from the directory provider to your application. The organization administrator configures the synchronization frequency within their directory provider console. To retrieve a list of all provisioned users, use the [Directory API](https://docs.scalekit.com/apis/#tag/directory/GET/api/v1/organizations/%7Borganization_id%7D/directories/%7Bdirectory_id%7D/users). --- # DOCUMENT BOUNDARY --- # Following webhook best practices > Learn best practices for implementing webhooks in your SCIM integration. Covers security measures, event handling, signature verification, and performance optimization techniques for real-time directory updates. Webhooks are HTTP endpoints that you register with a system, allowing that system to inform your application about events by sending HTTP POST requests with event information in the body. Developers register their applications’ webhook endpoints with Scalekit to listen to events from the directory providers of their enterprise customers. Here are some common best practices developers follow to ensure their apps are secure and performant: ## Subscribe only to relevant events [Section titled “Subscribe only to relevant events”](#subscribe-only-to-relevant-events) While you can listen to all events from Scalekit, it’s best to subscribe only to the events your app needs. This approach has several benefits: * Your app doesn’t have to process every event * You can avoid overloading a single execution context by handling every event type ## Verify webhook signatures [Section titled “Verify webhook signatures”](#verify-webhook-signatures) Scalekit sends POST requests to your registered webhook endpoint. Verify each request with the signing secret from **Dashboard > Webhooks > Any Endpoint** (`whsec_…`). Pass the raw body and these headers to the SDK: `webhook-id`, `webhook-timestamp`, and `webhook-signature`. * Node.js ```javascript 1 app.post( 2 '/webhook', 3 express.raw({ type: 'application/json' }), 4 async (req, res) => { 5 const secret = process.env.SCALEKIT_WEBHOOK_SECRET 6 const payload = req.body.toString() 7 const headers = { 8 'webhook-id': req.headers['webhook-id'], 9 'webhook-timestamp': req.headers['webhook-timestamp'], 10 'webhook-signature': req.headers['webhook-signature'], 11 } 12 13 try { 14 scalekit.verifyWebhookPayload(secret, headers, payload) 15 } catch (error) { 16 return res.status(401).json({ error: 'Invalid signature' }) 17 } 18 19 const event = JSON.parse(payload) 20 await processWebhookEvent(event) 21 return res.status(201).json({ received: true }) 22 } 23 ) ``` * Python ```python 1 import os 2 3 from fastapi import FastAPI, Request 4 from fastapi.responses import JSONResponse 5 6 app = FastAPI() 7 8 @app.post("/webhook") 9 async def api_webhook(request: Request): 10 # Get request data 11 body = await request.body() 12 13 # Extract webhook headers 14 headers = { 15 'webhook-id': request.headers.get('webhook-id'), 16 'webhook-signature': request.headers.get('webhook-signature'), 17 'webhook-timestamp': request.headers.get('webhook-timestamp') 18 } 19 20 try: 21 scalekit_client.verify_webhook_payload( 22 secret=os.environ["SCALEKIT_WEBHOOK_SECRET"], 23 headers=headers, 24 payload=body, 25 ) 26 except Exception: 27 return JSONResponse(status_code=401, content={"error": "Invalid signature"}) 28 29 return JSONResponse(status_code=201, content={"status": "received"}) ``` * Go ```go 1 mux.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) { 2 webhookSecret := os.Getenv("SCALEKIT_WEBHOOK_SECRET") 3 4 // Read request body 5 bodyBytes, err := io.ReadAll(r.Body) 6 if err != nil { 7 http.Error(w, err.Error(), http.StatusBadRequest) 8 return 9 } 10 11 // Prepare headers for verification 12 headers := map[string]string{ 13 "webhook-id": r.Header.Get("webhook-id"), 14 "webhook-signature": r.Header.Get("webhook-signature"), 15 "webhook-timestamp": r.Header.Get("webhook-timestamp"), 16 } 17 18 valid, err := scalekitClient.VerifyWebhookPayload( 19 webhookSecret, 20 headers, 21 bodyBytes, 22 ) 23 if err != nil || !valid { 24 http.Error(w, "Invalid signature", http.StatusUnauthorized) 25 return 26 } 27 }) ``` * Java ```java 1 @PostMapping("/webhook") 2 public ResponseEntity webhook( 3 @RequestBody String body, 4 @RequestHeader Map requestHeaders 5 ) { 6 // Spring may keep title-case keys. The Java SDK reads lowercase names only. 7 Map headers = new HashMap<>(); 8 headers.put("webhook-id", requestHeaders.get("webhook-id")); 9 headers.put("webhook-timestamp", requestHeaders.get("webhook-timestamp")); 10 headers.put("webhook-signature", requestHeaders.get("webhook-signature")); 11 12 String secret = System.getenv("SCALEKIT_WEBHOOK_SECRET"); 13 14 try { 15 scalekitClient.webhook().verifyWebhookPayload( 16 secret, 17 headers, 18 body.getBytes(java.nio.charset.StandardCharsets.UTF_8) 19 ); 20 } catch (Exception error) { 21 return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature"); 22 } 23 24 ObjectMapper mapper = new ObjectMapper(); 25 26 try { 27 // Parse event data 28 JsonNode node = mapper.readTree(body); 29 String eventType = node.get("type").asText(); 30 JsonNode data = node.get("data"); 31 32 // Handle different event types 33 switch (eventType) { 34 case "organization.directory.user_created": 35 handleUserCreate(data); 36 break; 37 case "organization.directory.user_updated": 38 handleUserUpdate(data); 39 break; 40 default: 41 System.out.println("Unhandled event type: " + eventType); 42 } 43 } catch (IOException e) { 44 return ResponseEntity.badRequest().body("error"); 45 } 46 47 return ResponseEntity.ok("ok"); 48 } ``` ## Check the event type before processing [Section titled “Check the event type before processing”](#check-the-event-type-before-processing) Make sure to check the event.type before consuming the data received by the webhook endpoint. This ensures that your application relies on accurate information, even if more events are added in the future. * Node.js ```javascript 1 app.post('/webhook', async (req, res) => { 2 const event = req.body; 3 4 // Handle different event types 5 switch (event.type) { 6 case 'organization.directory.user_created': 7 const { email, name } = event.data; 8 await createUserAccount(email, name); 9 break; 10 11 case 'organization.directory.user_updated': 12 await updateUserAccount(event.data); 13 break; 14 15 default: 16 console.log('Unhandled event type:', event.type); 17 } 18 19 return res.status(201).json({ 20 status: 'success', 21 }); 22 }); 23 24 async function createUserAccount(email, name) { 25 // Implement your user creation logic 26 } ``` * Python ```python 1 from fastapi import FastAPI, Request 2 3 app = FastAPI() 4 5 @app.post("/webhook") 6 async def api_webhook(request: Request): 7 # Parse request body 8 body = await request.body() 9 payload = json.loads(body.decode()) 10 event_type = payload['type'] 11 12 # Handle different event types 13 match event_type: 14 case 'organization.directory.user_created': 15 await handle_user_create(payload['data']) 16 case 'organization.directory.user_updated': 17 await handle_user_update(payload['data']) 18 case _: 19 print('Unhandled event type:', event_type) 20 21 return JSONResponse( 22 status_code=201, 23 content={'status': 'success'} 24 ) ``` * Go ```go 1 mux.HandleFunc("POST /webhook", func(w http.ResponseWriter, r *http.Request) { 2 // Read and verify webhook payload 3 bodyBytes, err := io.ReadAll(r.Body) 4 if err != nil { 5 http.Error(w, err.Error(), http.StatusBadRequest) 6 return 7 } 8 9 // Parse event data 10 var event map[string]interface{} 11 err = json.Unmarshal(bodyBytes, &event) 12 if err != nil { 13 http.Error(w, err.Error(), http.StatusBadRequest) 14 return 15 } 16 17 // Handle different event types 18 eventType := event["type"] 19 switch eventType { 20 case "organization.directory.user_created": 21 handleUserCreate(event["data"]) 22 case "organization.directory.user_updated": 23 handleUserUpdate(event["data"]) 24 default: 25 fmt.Println("Unhandled event type:", eventType) 26 } 27 28 w.WriteHeader(http.StatusOK) 29 }) ``` * Java ```java 1 @PostMapping("/webhook") 2 public ResponseEntity webhook( 3 @RequestBody String body, 4 @RequestHeader Map requestHeaders 5 ) { 6 Map headers = new HashMap<>(); 7 headers.put("webhook-id", requestHeaders.get("webhook-id")); 8 headers.put("webhook-timestamp", requestHeaders.get("webhook-timestamp")); 9 headers.put("webhook-signature", requestHeaders.get("webhook-signature")); 10 11 String secret = System.getenv("SCALEKIT_WEBHOOK_SECRET"); 12 try { 13 scalekitClient.webhook().verifyWebhookPayload( 14 secret, 15 headers, 16 body.getBytes(java.nio.charset.StandardCharsets.UTF_8) 17 ); 18 } catch (Exception error) { 19 return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid signature"); 20 } 21 22 try { 23 // Parse event data 24 ObjectMapper mapper = new ObjectMapper(); 25 JsonNode node = mapper.readTree(body); 26 String eventType = node.get("type").asText(); 27 JsonNode data = node.get("data"); 28 29 // Handle different event types 30 switch (eventType) { 31 case "organization.directory.user_created": 32 handleUserCreate(data); 33 break; 34 case "organization.directory.user_updated": 35 handleUserUpdate(data); 36 break; 37 default: 38 System.out.println("Unhandled event type: " + eventType); 39 } 40 } catch (IOException e) { 41 return ResponseEntity.badRequest().body("error"); 42 } 43 44 return ResponseEntity.ok("ok"); 45 } ``` ## Avoid webhook timeouts [Section titled “Avoid webhook timeouts”](#avoid-webhook-timeouts) To avoid unnecessary timeouts, respond to the webhook trigger with a response code of 201 and process the event asynchronously. If you process events from a queue, verify the signature when the request arrives, before you queue the event. The SDK rejects any request whose `webhook-timestamp` is more than 5 minutes old. An event that waits in your queue longer than that fails verification in the worker. Queue the verified event with its `webhook-id`, and have the worker skip IDs it has already processed. The payload’s top-level `id` works for this too. Don’t use `data.id`, which identifies the user or organization the event is about. By following these best practices, you can ensure that your application effectively handles events from Scalekit, maintaining optimal performance and security. ## Handle duplicate and out-of-order events [Section titled “Handle duplicate and out-of-order events”](#handle-duplicate-and-out-of-order-events) Process each event once, and in the order it happened: * **Skip duplicates.** A retried delivery keeps the same `webhook-id` header, and the payload’s top-level `id` (`evt_…`) is unique per event. Store the IDs you’ve processed and skip any you’ve seen. * **Don’t rely on delivery order.** Events can arrive out of order. Use each event’s `occurred_at`, and don’t let an older event overwrite a newer change you’ve already applied. ## Recover from missed events [Section titled “Recover from missed events”](#recover-from-missed-events) Your endpoint can miss events when it’s down for longer than Scalekit retries a delivery. Scalekit retries a failed delivery 8 times over about 27 hours (see the [retry schedule](/authenticate/implement-workflows/implement-webhooks/)). If an endpoint fails every delivery for 5 days, Scalekit disables it. After an outage: 1. **Turn the endpoint back on.** In **Developers** > **Webhooks**, open the endpoint. If its status is **Disabled**, select **Enable** from its actions menu. 2. **Resend the events you need.** Open the endpoint’s message log, select a failed message and select **Resend**. 3. **Reconcile each directory.** For a long outage, compare your records with the directory itself instead of replaying every event: * List every user in the directory, passing `next_page_token` to get each next page (up to 30 users per page). Create or update the users you’re missing or whose details changed. * Deactivate the users in your app who aren’t in the list. The list returns active users only, so a user who was deactivated or deleted while your endpoint was down doesn’t appear. `updated_after` returns users who changed, but not users who were removed. * List the directory’s groups the same way and update group memberships. See [List users in a directory](/directory/scim/quickstart/#list-users-in-a-directory) for the SDK calls. ## Do not ignore errors [Section titled “Do not ignore errors”](#do-not-ignore-errors) Do not overlook repeated 4xx and 5xx error codes. Instead, verify that your API interactions are correct. For instance, if an endpoint expects a string but receives a numeric value, a validation error should occur. Likewise, trying to access an unauthorized or nonexistent endpoint will trigger a 4xx error. ## Manual verification without an SDK [Section titled “Manual verification without an SDK”](#manual-verification-without-an-sdk) Use the SDK when your language has one. If it does not, HMAC-SHA256 the string `{webhook-id}.{webhook-timestamp}.{rawBody}`. The key is the base64-decoded text after the first `_` in the `whsec_…` secret. The `webhook-signature` header is a space-separated list of `v1,` values. Reject timestamps more than 5 minutes in the past or future. Compare digests with a timing-safe function. Do not use Stripe-style `t=,v1=` headers, a `scalekit-signature` header, or a `sha256=` hex digest. Those are not Scalekit webhook signatures. See [Handle webhook events](/authenticate/implement-workflows/implement-webhooks/#verify-with-the-three-webhook-headers) for the SDK call. ## Advanced error handling and reliability [Section titled “Advanced error handling and reliability”](#advanced-error-handling-and-reliability) Implement comprehensive error handling to ensure reliable webhook processing across various failure scenarios. ### Retry logic with exponential backoff [Section titled “Retry logic with exponential backoff”](#retry-logic-with-exponential-backoff) Retry with exponential backoff ```javascript 1 async function processWebhookWithRetry(event, maxRetries = 3) { 2 for (let attempt = 1; attempt <= maxRetries; attempt++) { 3 try { 4 await processWebhookEvent(event); 5 return; // Success, exit retry loop 6 7 } catch (error) { 8 console.error(`Webhook processing attempt ${attempt} failed:`, error); 9 10 if (attempt === maxRetries) { 11 // Final attempt failed - log to dead letter queue 12 await deadLetterQueue.add('failed_webhook', { 13 event, 14 error: error.message, 15 attempts: attempt, 16 timestamp: new Date() 17 }); 18 throw error; 19 } 20 21 // Wait before retry with exponential backoff 22 // Attempt 1: 1s, Attempt 2: 2s, Attempt 3: 4s 23 const waitTime = Math.pow(2, attempt) * 1000; 24 await new Promise(resolve => setTimeout(resolve, waitTime)); 25 } 26 } 27 } ``` ### Circuit breaker pattern [Section titled “Circuit breaker pattern”](#circuit-breaker-pattern) Prevent cascading failures by implementing a circuit breaker: Circuit breaker for webhook processing ```javascript 1 class WebhookCircuitBreaker { 2 constructor(options = {}) { 3 this.failureThreshold = options.failureThreshold || 5; 4 this.recoveryTimeout = options.recoveryTimeout || 60000; // 60 seconds 5 this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN 6 this.failures = 0; 7 this.nextAttempt = Date.now(); 8 } 9 10 async execute(fn) { 11 if (this.state === 'OPEN') { 12 if (Date.now() < this.nextAttempt) { 13 throw new Error('Circuit breaker is OPEN'); 14 } 15 // Try to recover 16 this.state = 'HALF_OPEN'; 17 } 18 19 try { 20 const result = await fn(); 21 this.onSuccess(); 22 return result; 23 } catch (error) { 24 this.onFailure(); 25 throw error; 26 } 27 } 28 29 onSuccess() { 30 this.failures = 0; 31 this.state = 'CLOSED'; 32 } 33 34 onFailure() { 35 this.failures++; 36 if (this.failures >= this.failureThreshold) { 37 this.state = 'OPEN'; 38 this.nextAttempt = Date.now() + this.recoveryTimeout; 39 } 40 } 41 } 42 43 // Usage 44 const circuitBreaker = new WebhookCircuitBreaker({ 45 failureThreshold: 5, 46 recoveryTimeout: 60000 47 }); 48 49 async function handleWebhook(event) { 50 try { 51 await circuitBreaker.execute(async () => { 52 return await processWebhookEvent(event); 53 }); 54 } catch (error) { 55 if (error.message === 'Circuit breaker is OPEN') { 56 // Service is unhealthy, queue for later 57 await queueForLater(event); 58 } 59 throw error; 60 } 61 } ``` ## Advanced testing strategies [Section titled “Advanced testing strategies”](#advanced-testing-strategies) ### Webhook testing utilities [Section titled “Webhook testing utilities”](#webhook-testing-utilities) Create comprehensive testing utilities for your webhook handlers: Webhook testing utilities ```javascript 1 // Test webhook handler with sample events 2 async function testWebhookHandler() { 3 const sampleUserCreatedEvent = { 4 spec_version: '1', 5 id: 'evt_test_123', 6 type: 'organization.directory.user_created', 7 occurred_at: new Date().toISOString(), 8 environment_id: 'env_test_123', 9 organization_id: 'org_test_123', 10 object: 'DirectoryUser', 11 data: { 12 id: 'diruser_test_123', 13 organization_id: 'org_test_123', 14 email: 'test@example.com', 15 given_name: 'Test', 16 family_name: 'User', 17 active: true, 18 groups: [], 19 roles: [] 20 } 21 }; 22 23 // Test your webhook processing 24 await processWebhookEvent(sampleUserCreatedEvent); 25 console.log('Test webhook processed successfully'); 26 } ``` ## Monitoring and debugging [Section titled “Monitoring and debugging”](#monitoring-and-debugging) ### Webhook delivery monitoring [Section titled “Webhook delivery monitoring”](#webhook-delivery-monitoring) Track webhook processing metrics to identify issues and optimize performance: Webhook monitoring ```javascript 1 // Track webhook processing metrics 2 async function trackWebhookMetrics(event, processingTime, success) { 3 await metricsService.record('webhook_processed', { 4 event_type: event.type, 5 processing_time_ms: processingTime, 6 success: success, 7 organization_id: event.organization_id, 8 environment_id: event.environment_id, 9 timestamp: new Date() 10 }); 11 12 // Alert on processing time anomalies 13 if (processingTime > 5000) { // 5 seconds 14 await alertService.warn({ 15 message: 'Slow webhook processing detected', 16 eventType: event.type, 17 processingTime: processingTime, 18 eventId: event.id 19 }); 20 } 21 22 // Alert on failures 23 if (!success) { 24 await alertService.error({ 25 message: 'Webhook processing failed', 26 eventType: event.type, 27 eventId: event.id 28 }); 29 } 30 } 31 32 // Dashboard endpoint to view webhook statistics 33 app.get('/admin/webhook-stats', async (req, res) => { 34 const stats = await db.query(` 35 SELECT 36 event_type, 37 COUNT(*) as total_events, 38 SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END) as successful, 39 SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed, 40 AVG(processing_time_ms) as avg_processing_time, 41 MAX(processing_time_ms) as max_processing_time, 42 MIN(processing_time_ms) as min_processing_time 43 FROM processed_webhooks 44 WHERE processed_at > NOW() - INTERVAL 24 HOUR 45 GROUP BY event_type 46 ORDER BY total_events DESC 47 `); 48 49 res.json(stats); 50 }); 51 52 // Real-time webhook monitoring 53 async function monitorWebhookHealth() { 54 const recentFailures = await db.processed_webhooks.count({ 55 where: { 56 status: 'failed', 57 processed_at: { 58 $gte: new Date(Date.now() - 5 * 60 * 1000) // Last 5 minutes 59 } 60 } 61 }); 62 63 if (recentFailures > 10) { 64 await alertService.critical({ 65 message: 'High webhook failure rate detected', 66 failureCount: recentFailures, 67 timeWindow: '5 minutes' 68 }); 69 } 70 } 71 72 // Run health check every minute 73 setInterval(monitorWebhookHealth, 60000); ``` ### Debugging webhook issues [Section titled “Debugging webhook issues”](#debugging-webhook-issues) Webhook debugging utilities ```javascript 1 // Detailed webhook logging 2 async function logWebhookDetails(event, context) { 3 await db.webhook_logs.create({ 4 event_id: event.id, 5 event_type: event.type, 6 organization_id: event.organization_id, 7 environment_id: event.environment_id, 8 received_at: new Date(), 9 headers: context.headers, 10 payload: event, 11 ip_address: context.ip, 12 user_agent: context.userAgent 13 }); 14 } 15 16 // Webhook replay for debugging 17 async function replayWebhook(eventId) { 18 // Retrieve original webhook from logs 19 const webhookLog = await db.webhook_logs.findOne({ 20 event_id: eventId 21 }); 22 23 if (!webhookLog) { 24 throw new Error(`Webhook ${eventId} not found`); 25 } 26 27 // Replay the webhook 28 console.log(`Replaying webhook ${eventId}`); 29 await processWebhookEvent(webhookLog.payload); 30 console.log(`Webhook ${eventId} replayed successfully`); 31 } 32 33 // Dead letter queue processor for failed webhooks 34 async function processDeadLetterQueue() { 35 const failedWebhooks = await deadLetterQueue.getAll('failed_webhook'); 36 37 for (const item of failedWebhooks) { 38 try { 39 console.log(`Reprocessing failed webhook: ${item.event.id}`); 40 await processWebhookEvent(item.event); 41 42 // Remove from dead letter queue on success 43 await deadLetterQueue.remove('failed_webhook', item.id); 44 45 } catch (error) { 46 console.error(`Failed to reprocess webhook ${item.event.id}:`, error); 47 48 // Increment retry count 49 item.retries = (item.retries || 0) + 1; 50 51 if (item.retries >= 5) { 52 // Move to permanent failure queue 53 await permanentFailureQueue.add(item); 54 await deadLetterQueue.remove('failed_webhook', item.id); 55 } 56 } 57 } 58 } 59 60 // Run dead letter queue processor periodically 61 setInterval(processDeadLetterQueue, 5 * 60 * 1000); // Every 5 minutes ``` ### Performance optimization [Section titled “Performance optimization”](#performance-optimization) Webhook performance optimization ```javascript 1 // Batch processing for high-volume webhooks 2 class WebhookBatchProcessor { 3 constructor(options = {}) { 4 this.batchSize = options.batchSize || 100; 5 this.flushInterval = options.flushInterval || 5000; // 5 seconds 6 this.queue = []; 7 this.timer = null; 8 } 9 10 add(event) { 11 this.queue.push(event); 12 13 if (this.queue.length >= this.batchSize) { 14 this.flush(); 15 } else if (!this.timer) { 16 this.timer = setTimeout(() => this.flush(), this.flushInterval); 17 } 18 } 19 20 async flush() { 21 if (this.queue.length === 0) return; 22 23 const batch = this.queue.splice(0, this.batchSize); 24 clearTimeout(this.timer); 25 this.timer = null; 26 27 try { 28 await this.processBatch(batch); 29 } catch (error) { 30 console.error('Batch processing error:', error); 31 // Re-queue failed items 32 this.queue.unshift(...batch); 33 } 34 } 35 36 async processBatch(events) { 37 // Process multiple events efficiently 38 await db.transaction(async (trx) => { 39 // Bulk insert processed events 40 await trx('processed_webhooks').insert( 41 events.map(e => ({ 42 event_id: e.id, 43 event_type: e.type, 44 organization_id: e.organization_id, 45 status: 'processing', 46 received_at: new Date() 47 })) 48 ); 49 50 // Process events in parallel 51 await Promise.all(events.map(e => this.processEvent(e, trx))); 52 }); 53 } 54 55 async processEvent(event, trx) { 56 // Event-specific processing logic 57 // Use transaction for atomicity 58 } 59 } 60 61 // Usage 62 const batchProcessor = new WebhookBatchProcessor({ 63 batchSize: 100, 64 flushInterval: 5000 65 }); 66 67 app.post('/webhooks/manage-users', async (req, res) => { 68 // Verify signature... 69 const event = req.body; 70 71 // Add to batch processor 72 batchProcessor.add(event); 73 74 // Respond immediately 75 return res.status(201).json({ received: true }); 76 }); ``` By following these advanced best practices, you can build a robust, reliable, and performant webhook integration that handles high volumes of events while maintaining data consistency and security. --- # DOCUMENT BOUNDARY --- # Bring Your Own Auth > Using Scalekit as a drop-in OAuth 2.1 authorization layer for your MCP Servers with federated authentication to your existing auth layer. Scalekit also offers the option to integrate your existing authentication infrastructure with Scalekit’s OAuth layer for MCP servers. **Use this when you have an existing auth system and want to add MCP OAuth without migrating users.** When your B2B application already has an established authentication system, you can connect it to your MCP server through Scalekit. This ensures that: * Users see the same familiar login screen whether accessing your application or your MCP server * No user migration required - your existing user accounts work immediately with MCP * You maintain control over your authentication logic while gaining MCP OAuth 2.1 compliance This “bring your own auth” approach standardizes the authorization layer without requiring you to rebuild your existing authentication infrastructure from scratch. Update your login endpoint for MCP token exchange The following changes will need to be made in your B2B apps’s Login API Endpoint. The connection ID, User POST URL and Redirect URI allows your app to know that scalekit is attempting to perform the Token Exchange for MCP Auth, so the user should get redirected to the correct consent screen post MCP Login instead of your B2B app. ## Step-by-Step Workflow [Section titled “Step-by-Step Workflow”](#step-by-step-workflow) When an MCP client initiates an authentication flow, Scalekit redirects to your login endpoint. You then provide user details to Scalekit via a secure backend call, and finally redirect back to Scalekit to complete the process. ![Scalekit MCP bring-your-own-auth flow](/d2/docs/mcp/auth-methods/custom-auth-0.svg) ### 1. Initiate Authentication [Section titled “1. Initiate Authentication”](#1-initiate-authentication) * The MCP client starts the authentication flow by calling `/oauth/authorize` on Scalekit. * Scalekit redirects the user to your login endpoint, passing two parameters: * `login_request_id`: Unique identifier for the login request. * `state`: Value to maintain state between requests. Example Redirect URL ```txt https://app.example.com/login?login_request_id=lri_86659065219908156&state=HntJ_ENB6y161i9_P1yzuZVv2SSTfD3aZH-Tej0_Y33_Fk8Z3g ``` ### 2. Handle Authentication in Your Application [Section titled “2. Handle Authentication in Your Application”](#2-handle-authentication-in-your-application) Once the user lands on your login page: #### a. Authenticate the User [Section titled “a. Authenticate the User”](#a-authenticate-the-user) Take the user through your regular authentication logic (e.g., username/password, SSO, etc.). #### b. Send User Details to Scalekit [Section titled “b. Send User Details to Scalekit”](#b-send-user-details-to-scalekit) Send the authenticated user’s profile details from your backend to Scalekit to complete the login handshake. * Python ```bash pip install scalekit-sdk-python ``` send_user_details.py ```python 1 from scalekit import ScalekitClient 2 import os 3 4 scalekit_client = ScalekitClient( 5 os.environ.get('SCALEKIT_ENVIRONMENT_URL'), 6 os.environ.get('SCALEKIT_CLIENT_ID'), 7 os.environ.get('SCALEKIT_CLIENT_SECRET') 8 ) 9 10 # Update login user details 11 try: 12 response = scalekit_client.auth.update_login_user_details( 13 connection_id="{{connection_id}}", 14 login_request_id="{{login_request_id}}", 15 user={ 16 "sub": "1234567890", 17 "email": "alice@example.com" 18 }, 19 ) 20 except Exception: 21 # Handle the request/SDK failure (network error, invalid credentials, etc.) 22 # and stop — do NOT redirect back to Scalekit on a failed request. 23 raise 24 25 # response is a (message, grpc.Call) tuple; the auth request ID (req_xxx) is on the first element. 26 auth_request_id = response[0].auth_request_id 27 # Optional: paste auth_request_id into Scalekit Dashboard > Auth Logs 28 # to view the entire auth journey for this login. 29 30 # To report a failed login instead, send login_failed=True (sub/email not required): 31 # scalekit_client.auth.update_login_user_details( 32 # connection_id="{{connection_id}}", 33 # login_request_id="{{login_request_id}}", 34 # user={"login_failed": True}, 35 # ) ``` * Node.js ```bash npm install @scalekit-sdk/node ``` sendUserDetails.js ```javascript 1 import { Scalekit } from '@scalekit-sdk/node'; 2 3 // Initialize client 4 const scalekit = new Scalekit( 5 process.env.SCALEKIT_ENVIRONMENT_URL, 6 process.env.SCALEKIT_CLIENT_ID, 7 process.env.SCALEKIT_CLIENT_SECRET 8 ); 9 10 // Update login user details 11 let res; 12 try { 13 res = await scalekit.auth.updateLoginUserDetails( 14 '{{connection_id}}', // connectionId 15 '{{login_request_id}}', // loginRequestId 16 { 17 sub: '1234567890', 18 email: 'alice@example.com' 19 } 20 ); 21 } catch (err) { 22 // Handle the request/SDK failure (network error, invalid credentials, etc.) 23 // and stop — do NOT redirect back to Scalekit on a failed request. 24 throw err; 25 } 26 27 // The response carries the auth request ID (req_xxx). 28 const authRequestId = res.authRequestId; 29 // Optional: paste authRequestId into Scalekit Dashboard > Auth Logs 30 // to view the entire auth journey for this login. 31 32 // To report a failed login instead, send loginFailed: true (sub/email not required): 33 // await scalekit.auth.updateLoginUserDetails( 34 // '{{connection_id}}', 35 // '{{login_request_id}}', 36 // { loginFailed: true } 37 // ); ``` * Go ```bash go get -u github.com/scalekit-inc/scalekit-sdk-go ``` send_user_details.go ```go 1 import ( 2 "context" 3 "fmt" 4 "github.com/scalekit-inc/scalekit-sdk-go/v2" 5 "os" 6 ) 7 8 // Get the connectionId from ScaleKit dashboard -> MCP Server -> Your Server -> User Info Post Url 9 // eg. https://example.scalekit.dev/api/v1/connections/conn_70982106544698372/auth-requests/{{login_request_id}}/user 10 // Your connectionId is conn_70982106544698372 in this example 11 func updateLoggedInUserDetails() error { 12 scalekitClient := scalekit.NewScalekitClient( 13 os.Getenv("SCALEKIT_ENVIRONMENT_URL"), 14 os.Getenv("SCALEKIT_CLIENT_ID"), 15 os.Getenv("SCALEKIT_CLIENT_SECRET"), 16 ) 17 resp, err := scalekitClient.Auth().UpdateLoginUserDetails(context.Background(), &scalekit.UpdateLoginUserDetailsRequest{ 18 ConnectionId: "{{connection_id}}", 19 LoginRequestId: "{{login_request_id}}", // this value is dynamic per login 20 User: &scalekit.LoggedInUserDetails{ 21 Sub: "1234567890", 22 Email: "alice@example.com", 23 }, 24 }) 25 if err != nil { 26 return err 27 } 28 29 // The response carries the auth request ID (req_xxx). 30 authRequestId := resp.GetAuthRequestId() 31 fmt.Println("auth request id:", authRequestId) 32 // Optional: paste authRequestId into Scalekit Dashboard -> Auth Logs 33 // to view the entire auth journey for this login. 34 35 // To report a failed login instead, send LoginFailed: true (Sub/Email not required): 36 // _, err = scalekitClient.Auth().UpdateLoginUserDetails(context.Background(), &scalekit.UpdateLoginUserDetailsRequest{ 37 // ConnectionId: "{{connection_id}}", 38 // LoginRequestId: "{{login_request_id}}", 39 // User: &scalekit.LoggedInUserDetails{LoginFailed: true}, 40 // }) 41 42 // Only if there is no error, perform the redirect to scalekit using the redirect url on your Scalekit Dashboard -> MCP Servers 43 return nil 44 } ``` * Java pom.xml ```xml com.scalekit scalekit-sdk-java 2.3.0 ``` SendUserDetails.java ```java 1 import com.scalekit.ScalekitClient; 2 import com.scalekit.grpc.scalekit.v1.auth.User; 3 import com.scalekit.internal.http.UpdateLoginUserDetailsResult; 4 import com.scalekit.shaded.google.protobuf.Struct; 5 import com.scalekit.shaded.google.protobuf.Value; 6 7 public class SendUserDetails { 8 9 // Returns the auth request ID (req_xxx) for the login request. 10 public String sendUserDetails(String connectionId, String loginRequestId, boolean loginSucceeded) { 11 ScalekitClient scalekitClient = new ScalekitClient( 12 System.getenv("SCALEKIT_ENVIRONMENT_URL"), 13 System.getenv("SCALEKIT_CLIENT_ID"), 14 System.getenv("SCALEKIT_CLIENT_SECRET") 15 ); 16 17 User user; 18 if (loginSucceeded) { 19 Struct customAttributes = Struct.newBuilder() 20 .putFields("access_level", Value.newBuilder().setNumberValue(101).build()) 21 .putFields("subscription_type", Value.newBuilder().setStringValue("PREMIUM").build()) 22 .build(); 23 24 user = User.newBuilder() 25 .setSub("user_123") 26 .setEmail("test@example.com") 27 .setName("Test User") 28 .addRoles("support") 29 .addRoles("developer") 30 .setCustomAttributes(customAttributes) 31 .build(); 32 } else { 33 // Report a failed login — sub/email are not required when loginFailed is true. 34 user = User.newBuilder().setLoginFailed(true).build(); 35 } 36 37 try { 38 UpdateLoginUserDetailsResult result = 39 scalekitClient.login().updateLoginUserDetails(connectionId, loginRequestId, user); 40 // The auth request ID (req_xxx). Optionally store it to fetch events for this auth 41 // request later, or paste it into Scalekit Dashboard > Auth Logs to view the entire 42 // auth journey for this login. 43 return result.getAuthRequestId(); 44 } catch (Exception e) { 45 // Handle the request/SDK failure (network error, invalid credentials, etc.) and 46 // stop — do NOT redirect back to Scalekit on a failed request. 47 throw new RuntimeException("Failed to update login user details", e); 48 } 49 } 50 } ``` * cURL Acquire an `access_token` before you could send user details by hitting the `/oauth/token` endpoint. You can get `env_url`, `sk_client_id` and `sk_client_secret` from *Scalekit Dashboard > Settings* get_access_token.sh ```bash # --fail-with-body makes curl exit non-zero on an HTTP error so a failed token # request is distinguishable from a successful one. curl --fail-with-body --location '{{env_url}}/oauth/token' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'grant_type=client_credentials' \ --data-urlencode 'client_id={{sk_client_id}}' \ --data-urlencode 'client_secret={{sk_client_secret}}' ``` Scalekit responds with a JSON payload similar to: ```json { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIn0...", "token_type": "Bearer", "expires_in": 3600 } ``` Use the `access_token` in the `Authorization` header when making a machine-to-machine POST request to Scalekit with the user’s details. send_user_details.sh ```bash # --fail-with-body makes curl exit non-zero (and still print the body) on an HTTP # error, so a failed request is distinguishable from a successful one. curl --fail-with-body --location '{{env_url}}/api/v1/connections/{{connection_id}}/auth-requests/{{login_request_id}}/user' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer {{access_token}}' \ --data-raw '{ "sub": "1234567890", "email": "alice@example.com", "roles": ["support", "developer"], "custom_attributes": { "access_level": 101, "subscription_type": "PREMIUM" } }' ``` Note * Replace placeholders like `{{env_url}}`, `{{connection_id}}`, `{{login_request_id}}`, and `{{access_token}}` with actual values. * Only `sub` and `email` are required fields; all other properties are optional. **Finding your `connection_id`:** Open **Dashboard > MCP Servers > \[your server] > Configuration > Advanced Configuration > Connection ID**. It starts with `conn_` and is distinct from the MCP server’s resource ID (which starts with `res_`). Do not use the resource ID here. ![MCP Server Advanced Configuration showing BYOA settings — Login Endpoint URL, Connection ID, User Info Post URL and Redirect URL](/.netlify/images?url=_astro%2Fbyoa-configuration.l3NExeTA.png\&w=1249\&h=1699\&dpl=6ae471fc143c9a4c27550463) **Using raw HTTP instead of the SDK:** Making direct HTTP calls to this endpoint is a fully supported alternative to using an SDK. If the SDK introduces transitive dependency conflicts in your project, use the cURL tab above for the equivalent request. *** ### 3. Redirect Back to Scalekit [Section titled “3. Redirect Back to Scalekit”](#3-redirect-back-to-scalekit) * Once you receive a successful response from Scalekit, redirect the user back to Scalekit using the provided `state` value to the below endpoint. **Example Redirect URL:** ```txt {{envurl}}/sso/v1/connections/{{connection_id}}/partner:callback?state={{state_value}} ``` `state_value` should match the `state` parameter you received in Step 1. *** ### 4. Completion [Section titled “4. Completion”](#4-completion) * After processing the callback from your auth system, Scalekit will handle the remaining steps (showing the consent screen to the user, token exchange, etc.) automatically. Tip * Ensure your backend securely stores and transmits all sensitive data. * The `login_request_id` and `state` parameters are essential for correlating requests and maintaining security. **Try out the BYOA MCP server**: Clone the [byoa-mcp-node sample](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/byoa-mcp-node) — a working Node.js MCP server with a custom login page, Scalekit token validation, and custom claims flowing through to tool handlers. Follow the README to run it locally end-to-end. --- # DOCUMENT BOUNDARY --- # Secure MCP with Enterprise SSO > Use Scalekit's out-of-the-box enterprise SSO connections to authenticate your MCP server from first request. Scalekit automatically handles identity verification via any authentication method, including but not limited to social providers like Google and Microsoft. It also supports authentication with your enterprise identity provider, such as Okta, Microsoft Entra AD, or ADFS, via SAML or OIDC. In this article, we will explain how to configure an Enterprise SSO connection with Okta as an identity provider. You can follow the same steps to configure any other identity provider. The steps with **blue arrows indicate that the step occurs during the browser redirects** and the steps with the **red arrows are Headless or Machine-to-Machine operations happening in the background.** ![MCP SSO authorization: the user signs in through their organization's identity provider before the MCP client gets a token](/d2/docs/mcp/auth-methods/enterprise-0.svg) ## Understanding the MCP SSO Flow at a high level [Section titled “Understanding the MCP SSO Flow at a high level”](#understanding-the-mcp-sso-flow-at-a-high-level) ## Before you start [Section titled “Before you start”](#before-you-start) Please make sure you have implemented MCP Auth with any of these [examples](/authenticate/mcp/fastmcp-quickstart). ## Configure Okta for authentication [Section titled “Configure Okta for authentication”](#configure-okta-for-authentication) 1. To configure Enterprise SSO, you need to create an organization.\ Open the **[Scalekit Dashboard](https://app.scalekit.com)** -> **Organizations** -> **Create Organization**. ![Create Organization](/.netlify/images?url=_astro%2Fcreate-org.CcRUR9lM.png\&w=1328\&h=818\&dpl=6ae471fc143c9a4c27550463) 2. Navigate to the **Single Sign-On** tab and follow the on-screen instructions. Make sure to click **Test Connection**, and then **Enable Connection**. ![Setup Organization SSO](/.netlify/images?url=_astro%2Fsetup-org-sso.DKNJlLtE.png\&w=832\&h=1424\&dpl=6ae471fc143c9a4c27550463) 3. To enforce that users from this organization are authenticated with the identity provider, add the domain under the **Domains** section in the **Overview** tab (e.g., `acmecorp.com`). ![Organization Domain Setup](/.netlify/images?url=_astro%2Forg-domain.BY_Mm5M_.png\&w=2582\&h=1146\&dpl=6ae471fc143c9a4c27550463) You have successfully implemented Enterprise SSO for your MCP server. Try running any of the [example apps](/authenticate/mcp/fastmcp-quickstart) next. If you don’t have access to the Identity Provider console You can generate an Admin Portal link from Scalekit and share it with your IT admin. ![Organization Generate Admin Portal Link](/.netlify/images?url=_astro%2Forg-generate-admin-portal.DQcNFzB_.png\&w=2598\&h=1162\&dpl=6ae471fc143c9a4c27550463) [Explore More Enterprise SSO Providers](/guides/integrations/sso-integrations) --- # DOCUMENT BOUNDARY --- # Secure MCP with Social Logins > Use Scalekit's out-of-the-box social connections to authenticate your MCP server from the first request. Scalekit supports a variety of social connections out of the box, such as Google, Microsoft, GitHub, GitLab, LinkedIn, Salesforce, and Facebook. This section focuses on how to use Google authentication, and the same process can be used for other social connections. ## Before you start [Section titled “Before you start”](#before-you-start) Please make sure you have implemented MCP auth with any of these [examples](/authenticate/mcp/fastmcp-quickstart). ## Configure Google connection [Section titled “Configure Google connection”](#configure-google-connection) 1. To configure the Google connection, open **[Dashboard](https://app.scalekit.com)** -> navigate to the **Authentication** section -> select **Auth Methods** -> select **Social Login**, and click on the **Edit** button against **Google**. 2. You can select **Use Scalekit credentials**, or you can follow the on-screen instructions to bring your own Google credentials. ![Google Auth Method](/.netlify/images?url=_astro%2Fgoogle-setup-enable.Qu9_1oNn.png\&w=3018\&h=902\&dpl=6ae471fc143c9a4c27550463) You have successfully implemented the social connection for your MCP server. Try running any of the [example apps](/authenticate/mcp/fastmcp-quickstart) next. [Explore More Social Providers](/guides/integrations/social-connections/) --- # DOCUMENT BOUNDARY --- # Secure MCP with Cross-App Access (XAA) > Use your customer's enterprise IdP to broker short-lived, scoped access to your MCP server, with no long-lived keys and no code changes required. Cross-App Access (XAA) lets your customer’s enterprise identity provider (IdP), such as Okta, act as the trust broker for your MCP server. Instead of storing long-lived API keys or requiring individual OAuth consent flows, an AI agent presents a signed identity assertion from the enterprise IdP. Scalekit exchanges that assertion for a short-lived, scoped access token, which the agent uses to call your MCP server. The enterprise admin authorizes the connector once. All users in the relevant IdP groups inherit access automatically, and every token request is logged through the IdP’s existing audit trail. Enterprise plan XAA is available on the Enterprise plan. To enable it for your workspace, email . ## How Cross-App Access works [Section titled “How Cross-App Access works”](#how-cross-app-access-works) ![Cross-App Access: the MCP client gets a signed ID-JAG from the enterprise identity provider and exchanges it with Scalekit for an access token](/d2/docs/mcp/auth-methods/xaa-0.svg) 1. The AI agent requests an **identity assertion (ID-JAG)** from the enterprise IdP. The IdP can be any provider that supports XAA - Okta, Microsoft Entra, or others. 2. The IdP returns a **signed ID-JAG JWT** containing standard claims: issuer (`iss`), subject (`sub`), audience (`aud`), and expiry (`exp`). 3. The agent exchanges the ID-JAG at Scalekit’s `/oauth/token` endpoint using the JWT Bearer grant (`grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer`). 4. Scalekit validates the JWT’s signature, issuer trust, and audience, then enforces the configured scopes and returns a **short-lived access token**. 5. The agent calls your **MCP server** with the Bearer token. Your server validates it exactly as it does today, with no code changes required. ## Enable Cross-App Access [Section titled “Enable Cross-App Access”](#enable-cross-app-access) 1. Open the **[Scalekit Dashboard](https://app.scalekit.com)**. 2. Navigate to **Auth for SaaS > MCP Servers**. 3. Select your MCP server. 4. Toggle on **Enable Cross-App Access (XAA)**. No code changes needed The XAA flow is handled entirely by Scalekit’s authorization server. Your MCP server validates Bearer tokens exactly as it does today, so enabling XAA requires only a single dashboard toggle. ## Supported MCP clients [Section titled “Supported MCP clients”](#supported-mcp-clients) XAA works with all three registration types that Scalekit supports: * **DCR clients** (Dynamic Client Registration) - Public clients such as Claude Desktop, Cursor, and VS Code that auto-register are fully supported. No additional configuration is needed. * **CIMD clients** (Client ID Metadata Document) - Clients that use a metadata URL as their identifier are also supported with XAA automatically. * **Pre-registered confidential clients** - If your MCP client requires a fixed `client_id` and `client_secret`, [pre-register the client in the Scalekit dashboard](/authenticate/mcp/managing-mcp-clients/) and provide your customer with the client id and secret for them to configure in their MCP clients. ## When to use XAA [Section titled “When to use XAA”](#when-to-use-xaa) * **No secret sprawl** - agents never store persistent API keys; tokens are short-lived and scoped to specific operations. * **Central IdP governance** - the enterprise IdP is the single control plane for all agent access, with a unified audit trail. * **One-time admin setup** - an admin authorizes the connector once; all users in the mapped groups inherit access without individual consent flows. --- # DOCUMENT BOUNDARY --- # UI events from the embedded admin portal > Learn how to listen for and handle UI events from the embedded admin portal, such as SSO connection status and session expiration. The embedded admin portal emits browser events that allow your application to respond to configuration changes made by organization admins. Use these events to provide real-time feedback, update your UI, sync configuration state, or trigger workflows in your application. Common use cases include displaying success notifications when SSO is configured, refreshing authentication settings after directory sync is enabled, or prompting users to re-authenticate when their admin portal session expires. ## Listening to admin portal events [Section titled “Listening to admin portal events”](#listening-to-admin-portal-events) Add an event listener to your parent window to receive events from the embedded admin portal iframe: ```js 1 window.addEventListener('message', (event) => { 2 // Security: Always validate the event origin matches your Scalekit environment 3 if (event.origin !== 'https://your-env.scalekit.com') { 4 return; // Ignore events from untrusted sources 5 } 6 7 // Check if this is a valid admin portal event 8 if (event.data && event.data.event_type) { 9 const { event_type, organization_id, data } = event.data; 10 11 // Handle specific event types 12 switch (event_type) { 13 case 'ORGANIZATION_SSO_ENABLED': 14 // Show success notification, refresh SSO settings, etc. 15 showNotification('SSO enabled successfully'); 16 break; 17 18 case 'PORTAL_SESSION_EXPIRY': 19 // Prompt user to refresh the admin portal 20 promptSessionRefresh(); 21 break; 22 23 default: 24 console.log('Received event:', event.data); 25 } 26 } 27 }); ``` Security requirement The domain of your parent window must be listed in **Dashboard > API Config > Redirect URIs** for the admin portal to emit events. Always validate `event.origin` to ensure events come from your trusted Scalekit environment URL. *** ## SSO events [Section titled “SSO events”](#sso-events) ### `ORGANIZATION_SSO_ENABLED` [Section titled “ORGANIZATION_SSO_ENABLED”](#organization_sso_enabled) Fires when an organization admin successfully enables a Single Sign-On connection in the admin portal. ORGANIZATION_SSO_ENABLED ```json { "event_type": "ORGANIZATION_SSO_ENABLED", "object": "connection", "organization_id": "org_4010340X34236531", // Organization that enabled SSO "message": "Single sign-on connection enabled successfully", "data": { "connection_type": "SSO", "id": "conn_4256075523X312", // Connection ID for API calls "type": "OIDC", // Protocol: OIDC or SAML "provider": "OKTA", // Identity provider configured "enabled": true } } ``` | Field | Type | Description | | ---------------------- | ------- | ------------------------------------------- | | `event_type` | string | The type of event being triggered | | `object` | string | The object type associated with the event | | `organization_id` | string | Unique identifier for the organization | | `message` | string | Human-readable message describing the event | | `data.connection_type` | string | Type of connection (SSO) | | `data.id` | string | Unique identifier for the connection | | `data.type` | string | Protocol type (e.g., OIDC, SAML) | | `data.provider` | string | Identity provider name | | `data.enabled` | boolean | Indicates if the connection is enabled | ### `ORGANIZATION_SSO_DISABLED` [Section titled “ORGANIZATION_SSO_DISABLED”](#organization_sso_disabled) Fires when an organization admin disables their Single Sign-On connection in the admin portal. ORGANIZATION_SSO_DISABLED ```json { "event_type": "ORGANIZATION_SSO_DISABLED", "object": "connection", "organization_id": "org_4010340X34236531", // Organization that disabled SSO "message": "Single sign-on connection disabled successfully", "data": { "connection_type": "SSO", "id": "conn_4256075523X312", // Connection ID that was disabled "type": "OIDC", // Protocol: OIDC or SAML "provider": "OKTA", // Identity provider that was configured "enabled": false } } ``` | Field | Type | Description | | ---------------------- | ------- | ------------------------------------------- | | `event_type` | string | The type of event being triggered | | `object` | string | The object type associated with the event | | `organization_id` | string | Unique identifier for the organization | | `message` | string | Human-readable message describing the event | | `data.connection_type` | string | Type of connection (SSO) | | `data.id` | string | Unique identifier for the connection | | `data.type` | string | Protocol type (e.g., OIDC, SAML) | | `data.provider` | string | Identity provider name | | `data.enabled` | boolean | Indicates if the connection is enabled | ## Session events [Section titled “Session events”](#session-events) ### `PORTAL_LOAD_SUCCESS` [Section titled “PORTAL_LOAD_SUCCESS”](#portal_load_success) Fires when the admin portal session is created and loaded successfully. Use this event to display the portal iframe and confirm readiness to users. PORTAL_LOAD_SUCCESS ```json { "event_type": "PORTAL_LOAD_SUCCESS", "object": "session", "message": "The admin portal loaded successfully", "organization_id": "org_43982563588440584", "data": { "expiry": "2025-02-28T12:40:35.911Z" // ISO 8601 timestamp when session expires } } ``` | Field | Type | Description | | ----------------- | ------ | ---------------------------------------------------------- | | `event_type` | string | The type of event being triggered | | `object` | string | The object type associated with the event | | `organization_id` | string | Unique identifier for the organization | | `message` | string | Human-readable message describing the event | | `data.expiry` | string | ISO 8601 timestamp indicating when the session will expire | ### `PORTAL_LOAD_FAILURE` [Section titled “PORTAL_LOAD_FAILURE”](#portal_load_failure) Fires when the admin portal session failed to load. Use this to prompt users that the session has failed to load. PORTAL_LOAD_FAILURE ```json { "event_type": "PORTAL_LOAD_FAILURE", "object": "session", "message": "The admin portal failed to load", "data": { "error_code": "SESSION_EXPIRED" // error code indicating why the session load failed } } ``` | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------- | | `event_type` | string | The type of event being triggered | | `object` | string | The object type associated with the event | | `message` | string | Human-readable message describing the event | | `data.error_code` | string | Error code indicating why the session load failed | ### `PORTAL_SESSION_WARNING` [Section titled “PORTAL_SESSION_WARNING”](#portal_session_warning) Fires when the admin portal session is about to expire (typically 5 minutes before expiration). Use this to prompt users to save their work or refresh their session. PORTAL_SESSION_WARNING ```json { "event_type": "PORTAL_SESSION_WARNING", "object": "session", "message": "The admin portal session will expire in 5 minutes", "organization_id": "org_43982563588440584", "data": { "expiry": "2025-02-28T12:40:35.911Z" // ISO 8601 timestamp when session expires } } ``` | Field | Type | Description | | ----------------- | ------ | ---------------------------------------------------------- | | `event_type` | string | The type of event being triggered | | `object` | string | The object type associated with the event | | `organization_id` | string | Unique identifier for the organization | | `message` | string | Human-readable message describing the event | | `data.expiry` | string | ISO 8601 timestamp indicating when the session will expire | ### `PORTAL_SESSION_EXPIRY` [Section titled “PORTAL_SESSION_EXPIRY”](#portal_session_expiry) Fires when the admin portal session has expired. Use this to hide the admin portal iframe and prompt users to re-authenticate. PORTAL_SESSION_EXPIRY ```json { "event_type": "PORTAL_SESSION_EXPIRY", "object": "session", "message": "The admin portal session has expired", "organization_id": "org_43982563588440584", "data": { "expiry": "2025-02-28T12:40:35.911Z" // ISO 8601 timestamp when session expired } } ``` | Field | Type | Description | | ----------------- | ------ | ------------------------------------------------------ | | `event_type` | string | The type of event being triggered | | `object` | string | The object type associated with the event | | `organization_id` | string | Unique identifier for the organization | | `message` | string | Human-readable message describing the event | | `data.expiry` | string | ISO 8601 timestamp indicating when the session expired | ## Directory events [Section titled “Directory events”](#directory-events) ### `ORGANIZATION_DIRECTORY_ENABLED` [Section titled “ORGANIZATION_DIRECTORY_ENABLED”](#organization_directory_enabled) Fires when an organization admin successfully configures and enables SCIM directory provisioning in the admin portal. ORGANIZATION_DIRECTORY_ENABLED ```json { "event_type": "ORGANIZATION_DIRECTORY_ENABLED", "object": "directory", "organization_id": "org_45716217859670289", // Organization that enabled directory sync "message": "SCIM Provisioning enabled successfully", "data": { "directory_type": "SCIM", // Directory protocol type "id": "dir_45716228982964495", // Directory connection ID for API calls "provider": "MICROSOFT_AD", // Identity provider: OKTA, AZURE_AD, GOOGLE, etc. "enabled": true } } ``` | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `event_type` | string | The type of event being triggered | | `object` | string | The object type associated with the event | | `organization_id` | string | Unique identifier for the organization | | `message` | string | Human-readable message describing the event | | `data.directory_type` | string | Type of directory synchronization (SCIM) | | `data.id` | string | Unique identifier for the directory connection | | `data.provider` | string | Identity provider name | | `data.enabled` | boolean | Indicates if the directory sync is enabled | ### `ORGANIZATION_DIRECTORY_DISABLED` [Section titled “ORGANIZATION_DIRECTORY_DISABLED”](#organization_directory_disabled) Fires when an organization admin disables SCIM directory provisioning in the admin portal. ORGANIZATION_DIRECTORY_DISABLED ```json { "event_type": "ORGANIZATION_DIRECTORY_DISABLED", "object": "directory", "organization_id": "org_45716217859670289", // Organization that disabled directory sync "message": "SCIM Provisioning disabled successfully", "data": { "directory_type": "SCIM", // Directory protocol type "id": "dir_45716228982964495", // Directory connection ID that was disabled "provider": "MICROSOFT_AD", // Identity provider that was configured "enabled": false } } ``` | Field | Type | Description | | --------------------- | ------- | ---------------------------------------------- | | `event_type` | string | The type of event being triggered | | `object` | string | The object type associated with the event | | `organization_id` | string | Unique identifier for the organization | | `message` | string | Human-readable message describing the event | | `data.directory_type` | string | Type of directory synchronization (SCIM) | | `data.id` | string | Unique identifier for the directory connection | | `data.provider` | string | Identity provider name | | `data.enabled` | boolean | Indicates if the directory sync is enabled | ## Complete event handler Example [Section titled “Complete event handler ”](#complete-event-handler-) Here’s a complete example showing how to handle all admin portal events in a production application: Complete admin portal event handler ```js 1 // Initialize event handling for the admin portal 2 function initAdminPortalEventHandling(scalekitEnvironmentUrl) { 3 window.addEventListener('message', (event) => { 4 // Security: Validate event origin 5 if (event.origin !== scalekitEnvironmentUrl) { 6 return; 7 } 8 9 if (!event.data || !event.data.event_type) { 10 return; 11 } 12 13 const { event_type, organization_id, data, message } = event.data; 14 15 // Log all events for debugging 16 console.log('[Admin Portal Event]', { event_type, organization_id, data }); 17 18 switch (event_type) { 19 case 'ORGANIZATION_SSO_ENABLED': 20 handleSSOEnabled(organization_id, data); 21 break; 22 23 case 'ORGANIZATION_SSO_DISABLED': 24 handleSSODisabled(organization_id, data); 25 break; 26 27 case 'ORGANIZATION_DIRECTORY_ENABLED': 28 handleDirectoryEnabled(organization_id, data); 29 break; 30 31 case 'ORGANIZATION_DIRECTORY_DISABLED': 32 handleDirectoryDisabled(organization_id, data); 33 break; 34 35 case 'PORTAL_LOAD_SUCCESS': 36 handlePortalLoadSuccess(data.expiry); 37 break; 38 39 case 'PORTAL_LOAD_FAILURE': 40 handlePortalLoadFailure(data.error_code); 41 break; 42 43 case 'PORTAL_SESSION_WARNING': 44 handleSessionWarning(data.expiry); 45 break; 46 47 case 'PORTAL_SESSION_EXPIRY': 48 handleSessionExpiry(); 49 break; 50 51 default: 52 console.warn('Unknown event type:', event_type); 53 } 54 }); 55 } 56 57 function handleSSOEnabled(orgId, data) { 58 // Show success notification 59 showToast('success', `SSO enabled successfully with ${data.provider}`); 60 61 // Sync configuration to your backend 62 fetch('/api/organizations/${orgId}/sync-sso', { 63 method: 'POST', 64 headers: { 'Content-Type': 'application/json' }, 65 body: JSON.stringify({ connectionId: data.id, provider: data.provider }) 66 }); 67 68 // Update UI to reflect SSO is active 69 updateOrganizationUI(orgId, { ssoEnabled: true }); 70 } 71 72 function handlePortalLoadSuccess(expiryTime) { 73 const expiryDate = new Date(expiryTime); 74 console.log('[Admin Portal] Loaded successfully, session expires at', expiryDate); 75 76 // Update UI to show the portal is ready 77 document.getElementById('admin-portal-iframe').style.display = 'block'; 78 } 79 80 function handlePortalLoadFailure(errorCode) { 81 console.error('[Admin Portal] Failed to load, error code:', errorCode); 82 83 // Hide the iframe and show an error message to the user 84 document.getElementById('admin-portal-iframe').style.display = 'none'; 85 86 showModal({ 87 title: 'Portal failed to load', 88 message: errorCode === 'SESSION_EXPIRED' 89 ? 'Your session has expired. Please refresh to continue.' 90 : `The admin portal could not be loaded (${errorCode}). Please try again.`, 91 action: { 92 label: 'Refresh Page', 93 onClick: () => window.location.reload() 94 } 95 }); 96 } 97 98 function handleSessionWarning(expiryTime) { 99 const expiryDate = new Date(expiryTime); 100 const minutesLeft = Math.round((expiryDate - new Date()) / 60000); 101 102 showNotification({ 103 type: 'warning', 104 message: `Your admin session will expire in ${minutesLeft} minutes`, 105 action: { 106 label: 'Refresh Session', 107 onClick: () => window.location.reload() 108 } 109 }); 110 } 111 112 function handleSessionExpiry() { 113 // Hide the admin portal iframe 114 document.getElementById('admin-portal-iframe').style.display = 'none'; 115 116 // Show message to user 117 showModal({ 118 title: 'Session Expired', 119 message: 'Your admin portal session has expired. Please refresh to continue.', 120 action: { 121 label: 'Refresh Page', 122 onClick: () => window.location.reload() 123 } 124 }); 125 } 126 127 // Initialize when your app loads 128 initAdminPortalEventHandling('https://your-env.scalekit.com'); ``` --- # DOCUMENT BOUNDARY --- # Outbound IP addresses > Allowlist Scalekit's outbound IP addresses so providers and services accept the requests and tool calls that originate from Scalekit. When Scalekit makes outbound requests on your behalf, such as tool calls to upstream providers, the traffic originates from a fixed set of NAT IP addresses. If a provider or one of your own services restricts inbound traffic by IP, add these addresses to its allowlist so the provider accepts the requests. ## IP addresses by region [Section titled “IP addresses by region”](#ip-addresses-by-region) | Region | Outbound IP address | | ------------------- | ------------------- | | United States (US) | `34.94.129.140` | | European Union (EU) | `35.198.115.68` | Use the address for the region your Scalekit environment runs in. If you run environments in both regions, allowlist both addresses. Confirm before you rely on these long-term Outbound IP addresses can change. Check this page for the current values before you finalize a firewall rule, and contact Scalekit support if you need advance notice of changes for a compliance review. ## When to allowlist these addresses [Section titled “When to allowlist these addresses”](#when-to-allowlist-these-addresses) Allowlist Scalekit’s outbound IP addresses when: * An upstream provider restricts API access to known IP addresses, and you call that provider’s tools through Scalekit. * Your own backend validates the source IP of the requests it receives from Scalekit. --- # DOCUMENT BOUNDARY --- # API rate limits > Understand Scalekit API rate limits, tell them apart from upstream provider limits, handle 429 responses, and request a higher limit. Scalekit applies a rate limit to the API requests from each environment. When a workload exceeds that limit, the API responds with HTTP `429 Too Many Requests`. This page explains how to recognize a rate-limit response, tell a Scalekit limit apart from an upstream provider’s limit, handle the response, and request a higher limit. ## How rate limits work [Section titled “How rate limits work”](#how-rate-limits-work) Scalekit enforces a per-environment request rate, measured in requests per minute. Scalekit tunes the limit per account, so high-throughput workloads can need a higher limit than the default. Routing MCP tool calls through Scalekit on top of authentication traffic is one example of a workload that can need more headroom. When you exceed the limit, Scalekit returns HTTP `429 Too Many Requests`. Back off and retry with exponential backoff rather than retrying immediately. ## Tell Scalekit limits apart from provider limits [Section titled “Tell Scalekit limits apart from provider limits”](#tell-scalekit-limits-apart-from-provider-limits) When you call tools through Scalekit, a `429` can come from either Scalekit or the upstream provider that Scalekit calls on your behalf, such as a CRM or data API. The `error_code` field on the error identifies the source: | `error_code` | Source | What to do | | -------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------- | | `RATE_LIMITED` | Scalekit’s own rate limit | Reduce the overall request frequency and back off before retrying. | | `TOOL_ERROR` | The upstream provider rate-limited the tool call | Apply the provider’s recommended backoff. Check the tool call logs for the provider’s message. | Review the detailed error in your dashboard’s tool call logs to confirm which provider and which tool triggered the limit. ## Handle a 429 response [Section titled “Handle a 429 response”](#handle-a-429-response) Every Scalekit SDK raises a dedicated exception when the API returns `429`. Catch it, read the error code to determine the source, and back off before retrying. * Node.js rate-limit-handling.ts ```ts 1 import { ScalekitTooManyRequestsException } from '@scalekit-sdk/node'; 2 3 try { 4 // Your Scalekit SDK call, for example executing a tool 5 await scalekit.tools.executeTool(/* ... */); 6 } catch (error) { 7 if (error instanceof ScalekitTooManyRequestsException) { 8 // errorCode identifies the source of the 429 so you can back off correctly 9 if (error.errorCode === 'TOOL_ERROR') { 10 // Upstream provider rate-limited the call: apply provider-specific backoff 11 console.error('Provider rate limit:', error.message); 12 } else { 13 // Scalekit's own rate limit (RATE_LIMITED): reduce overall request frequency 14 console.error('Scalekit rate limit:', error.message); 15 } 16 } 17 } ``` * Python rate_limit_handling.py ```py from scalekit.common.exceptions import ScalekitTooManyRequestsException try: result = scalekit_client.tools.execute_tool(...) except ScalekitTooManyRequestsException as e: # error_code identifies the source of the 429 so you can back off correctly if e.error_code == "TOOL_ERROR": # Upstream provider rate-limited the call: apply provider-specific backoff print("Provider rate limit:", e.message) else: # Scalekit's own rate limit (RATE_LIMITED): reduce overall request frequency print("Scalekit rate limit:", e.message) ``` * Go rate_limit_handling.go ```go // Your Scalekit SDK call, for example executing a tool _, err := scalekitClient.Tools.ExecuteTool(ctx /* ... */) if err != nil { // Inspect the error code to find the source of the 429. // "TOOL_ERROR" is the upstream provider's limit; "RATE_LIMITED" is Scalekit's // own limit. Back off with exponential backoff before retrying. log.Printf("rate limited: %v", err) } ``` * Java RateLimitHandling.java ```java try { // Your Scalekit SDK call, for example executing a tool scalekitClient.tools().executeTool(/* ... */); } catch (ScalekitException error) { // Inspect the error code to find the source of the 429. // "TOOL_ERROR" is the upstream provider's limit; "RATE_LIMITED" is Scalekit's // own limit. Back off with exponential backoff before retrying. System.err.println("Rate limited: " + error.getMessage()); } ``` Use exponential backoff Retrying a rate-limited request immediately keeps you over the limit. Use exponential backoff with jitter, and cap the number of retries so a sustained limit doesn’t block your application indefinitely. ## Request a higher limit [Section titled “Request a higher limit”](#request-a-higher-limit) If your workload needs a higher limit, contact Scalekit support with your account details and your expected peak requests per minute. Plan ahead before you route additional traffic, such as MCP tool calls, through Scalekit. Scalekit reviews the request and adjusts the limit for your account. When you estimate the limit you need, include headroom above your current peak so that normal spikes do not trigger `429` responses. --- # DOCUMENT BOUNDARY --- # Code Samples > Explore comprehensive code samples and examples for integrating with Scalekit across different programming languages and frameworks ### [MCP Auth](/resources/code-samples/mcp-auth/) [MCP server authentication examples in Python and Node.js](/resources/code-samples/mcp-auth/) ### [AgentKit](/agentkit/examples/) [Working examples with LangChain, OpenAI, Anthropic, Vercel AI SDK, Mastra, CrewAI, Google ADK and Claude Managed Agents](/agentkit/examples/) ### [Modular SSO](/resources/code-samples/modular-sso/) [Single Sign-On implementations for enterprise authentication with Express.js, PHP Symfony, .NET Core, Firebase, and AWS Cognito integrations](/resources/code-samples/modular-sso/) ### [Modular SCIM](/resources/code-samples/modular-scim/) [SCIM provisioning examples and integration patterns for user and group management](/resources/code-samples/modular-scim/) ### Full stack auth Complete authentication implementations across different frameworks including Next.js, Express.js, Spring Boot, FastAPI, and Go [See all code samples →](/resources/code-samples/full-stack-auth/) --- # DOCUMENT BOUNDARY --- # Full stack auth > Code samples demonstrating complete authentication implementations with hosted login and session management ### [Full Stack Auth with Next.js](https://github.com/scalekit-inc/scalekit-nextjs-auth-example) [Complete authentication solution for Next.js apps. Includes hosted login pages, session management, and protected routes](https://github.com/scalekit-inc/scalekit-nextjs-auth-example) ### [Full Stack Auth with FastAPI](https://github.com/scalekit-inc/scalekit-fastapi-auth-example) [Authentication template for FastAPI projects. Featuring integrated user sessions, hosted login flow, and ready-to-use route protection specifically tailored for Python web backends.](https://github.com/scalekit-inc/scalekit-fastapi-auth-example) ### [Full Stack Auth with Flask](https://github.com/scalekit-inc/scalekit-flask-auth-example) [Authentication template for Flask applications. Features session management, hosted login flow, and decorator-based route protection](https://github.com/scalekit-inc/scalekit-flask-auth-example) ### [Full Stack Auth with Django](https://github.com/scalekit-inc/scalekit-django-auth-example) [Authentication template for Django projects. Features session management, hosted login flow, and middleware-based route protection](https://github.com/scalekit-inc/scalekit-django-auth-example) ### [Full Stack Auth with Express](https://github.com/scalekit-inc/scalekit-express-auth-example) [Complete authentication solution for Express.js applications. Includes hosted login pages, session management, and middleware-protected routes](https://github.com/scalekit-inc/scalekit-express-auth-example) ### [Full Stack Auth with Spring Boot](https://github.com/scalekit-inc/scalekit-springboot-auth-example) [End-to-end authentication for Java applications. Features Spring Security integration, hosted login, and session handling](https://github.com/scalekit-inc/scalekit-springboot-auth-example) ### [Full Stack Auth with Laravel](https://github.com/scalekit-inc/scalekit-laravel-auth-example) [Complete authentication solution for Laravel applications. Includes hosted login pages, session management, and middleware-protected routes](https://github.com/scalekit-inc/scalekit-laravel-auth-example) ### End to end full stack auth demo Coffee Desk App Complete coffee shop management application with full stack. Features workspaces, organization switcher, and mulitple auth methods [View demo](https://dashboard.coffeedesk.app/) | [View code](https://github.com/scalekit-inc/coffee-desk-demo) --- # DOCUMENT BOUNDARY --- # MCP Auth > Model Context Protocol authentication examples and patterns ### [Add Auth to Node.js MCP Servers](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node) [Add Scalekit auth to a Node.js MCP server with minimal setup. Includes a working example with user greeting.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-node) ### [Add Auth to Python MCP Servers](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python) [Add Scalekit auth to a Python MCP server in minutes. Includes a working example with user greeting.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/greeting-mcp-python) ### [Secure FastMCP Apps with Auth](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp) [Build a secure FastMCP app with Scalekit. Features a complete todo list with protected endpoints and session management.](https://github.com/scalekit-inc/mcp-auth-demos/tree/main/todo-fastmcp) --- # DOCUMENT BOUNDARY --- # Modular SCIM > Code samples demonstrating SCIM provisioning examples and integration patterns for user and group management ### [Handle SCIM webhooks](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/webhook-events) [Process SCIM directory updates in Next.js. Example shows how to verify webhook signatures and sync user data](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/webhook-events) ### [Embed admin portal](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample) [Securely embed the Scalekit Admin Portal via iframe. Node.js example for managing directory sync and organizational settings](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample) --- # DOCUMENT BOUNDARY --- # Modular SSO > Code samples demonstrating Single Sign-On implementations with Express.js, PHP Symfony, .NET Core, Firebase, AWS Cognito, and Next.js ### [Add SSO to Express.js apps](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/sso-express-example) [Implement Scalekit SSO in a Node.js Express application. Includes middleware setup for secure session handling](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/sso-express-example) ### [Add SSO to PHP Symfony apps](https://github.com/scalekit-developers/scalekit-php-symfony-sso) [Call Scalekit OIDC endpoints from Symfony with no PHP SDK. Try handshake-only login or Modular SSO with a local user store](https://github.com/scalekit-developers/scalekit-php-symfony-sso) ### [Add SSO to .NET Core apps](https://github.com/scalekit-inc/dotnet-example-apps) [Secure .NET Core applications with Scalekit SSO. Demonstrates authentication pipelines and user claims management](https://github.com/scalekit-inc/dotnet-example-apps) ### [Add SSO to Spring Boot apps](https://github.com/scalekit-developers/scalekit-springboot-example) [Integrate Scalekit SSO with Spring Security. Shows how to configure security filters and protect Java endpoints](https://github.com/scalekit-developers/scalekit-springboot-example) ### [Add SSO to Python FastAPI](https://github.com/scalekit-developers/scalekit-fastapi-example) [Add enterprise SSO to FastAPI services using Scalekit. Includes async route protection and user session validation](https://github.com/scalekit-developers/scalekit-fastapi-example) ### [Add SSO to Go applications](https://github.com/scalekit-developers/scalekit-go-example) [Implement Scalekit SSO in Go. Features idiomatically written middleware for securing HTTP handlers](https://github.com/scalekit-developers/scalekit-go-example) ### [Add SSO to Next.js apps](https://github.com/scalekit-developers/scalekit-nextjs-demo) [Secure Next.js applications with Scalekit. Covers both App Router and Pages Router authentication patterns](https://github.com/scalekit-developers/scalekit-nextjs-demo) ### Scalekit SSO + Your own auth system [Section titled “Scalekit SSO + Your own auth system”](#scalekit-sso--your-own-auth-system) ### [Connect Firebase Auth with SSO](https://github.com/scalekit-inc/scalekit-firebase-sso) [Enable Enterprise SSO for Firebase apps using Scalekit. Learn to link Scalekit identities with Firebase Authentication](https://github.com/scalekit-inc/scalekit-firebase-sso) ### [Connect AWS Cognito with SSO](https://github.com/scalekit-inc/scalekit-cognito-sso) [Add Enterprise SSO to Cognito user pools via Scalekit. Step-by-step guide to federating identity providers](https://github.com/scalekit-inc/scalekit-cognito-sso) ### [Cognito + Scalekit for Next.js](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/cognito-scalekit) [Integrate Cognito and Scalekit SSO in Next.js. Uses OIDC protocols to secure your full-stack React application](https://github.com/scalekit-inc/nextjs-example-apps/tree/main/cognito-scalekit) ## Admin portal [Section titled “Admin portal”](#admin-portal) ### [Embed admin portal](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample) [Embed the Scalekit Admin Portal into your app via **iframe**. Node.js example for generating secure admin sessions](https://github.com/scalekit-inc/nodejs-example-apps/tree/main/embed-admin-portal-sample) --- # DOCUMENT BOUNDARY --- # Django session middleware > Add hosted login and an encrypted session cookie to Django with ScalekitAuthMiddleware Use `scalekit.frameworks.django` to add hosted login, an encrypted `sk_session` cookie, token refresh, and logout. Typical flow: install the `django` extra, add settings and `ScalekitAuthMiddleware`, include the auth URLs, and decorate one view with `@login_required`. There is no `ScalekitAuth` instance to construct. Requires `scalekit-sdk-python` 2.17.0 or later. Register these URLs in the Scalekit Dashboard under **Authentication > Redirects** before you test. Paths have **no trailing slash**. | Dashboard field | Must match | | ---------------------------- | ----------------------------------------------------------------------------- | | **Redirect URI** | `SCALEKIT_REDIRECT_URI` exactly, for example `http://localhost:8000/callback` | | **Post Logout Redirect URI** | Absolute URL after full logout, for example `http://localhost:8000/` | | **Initiate Login URL** | Login path, for example `http://localhost:8000/login` | Store credentials in environment variables. Never hard-code secrets. .env ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=... COOKIE_ENCRYPTION_SECRET= # openssl rand -base64 32 REDIRECT_URI=http://localhost:8000/callback ``` Keep `COOKIE_ENCRYPTION_SECRET` identical on every server instance. ## Install the package [Section titled “Install the package”](#install-the-package) Terminal ```bash pip install "scalekit-sdk-python[django]" ``` ## Protect a route [Section titled “Protect a route”](#protect-a-route) settings.py ```python import os MIDDLEWARE = [ # ... "scalekit.frameworks.django.ScalekitAuthMiddleware", ] SCALEKIT_ENV_URL = os.environ["SCALEKIT_ENVIRONMENT_URL"] SCALEKIT_CLIENT_ID = os.environ["SCALEKIT_CLIENT_ID"] SCALEKIT_CLIENT_SECRET = os.environ["SCALEKIT_CLIENT_SECRET"] SCALEKIT_REDIRECT_URI = os.environ["REDIRECT_URI"] SCALEKIT_COOKIE_ENCRYPTION_SECRET = os.environ["COOKIE_ENCRYPTION_SECRET"] SCALEKIT_COOKIE_SECURE = False # set True behind HTTPS ``` urls.py ```python from django.urls import include, path from . import views urlpatterns = [ path("", include("scalekit.frameworks.django")), path("account", views.account), ] ``` views.py ```python from django.http import JsonResponse from scalekit.frameworks.django import login_required @login_required def account(request): return JsonResponse({"sub": request.scalekit_user["sub"]}) ``` Open `http://localhost:8000/account`. A missing session returns **302** to `/login`, not a JSON 401. `request.scalekit_user` is access-token claims, or `None` when the visitor is anonymous. `sub` is always present on an authenticated user. `email` appears only when you add it as a custom access-token claim. Middleware does not block anonymous requests `ScalekitAuthMiddleware` sets `request.scalekit_user` and refreshes the cookie. It does not redirect. Use `@login_required` on views that must be signed in. This matches Django’s `AuthenticationMiddleware` plus `login_required`. Set SCALEKIT_COOKIE_SECURE=False on local HTTP `SCALEKIT_COOKIE_SECURE` defaults to `True`. Set it `False` for local HTTP. Set it `True` in production. ### ScalekitAuthMiddleware [Section titled “ScalekitAuthMiddleware”](#scalekitauthmiddleware) classScalekitAuthMiddlewarehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/django.py [#](#__call__)\_\_call\_\_ Django middleware that reads `sk_session` on every request. Sets `request.scalekit_user` (`None` if unauthenticated) and writes a refreshed cookie about 10 seconds before expiry. Add the class path to `MIDDLEWARE`. Required settings: `SCALEKIT_REDIRECT_URI`, `SCALEKIT_COOKIE_ENCRYPTION_SECRET`, and either `SCALEKIT_CLIENT` or `SCALEKIT_ENV_URL` + `SCALEKIT_CLIENT_ID` + `SCALEKIT_CLIENT_SECRET`. paramrequestHttpRequest Incoming request. returnsHttpResponse Downstream response, with a new or cleared session cookie when needed. ```python MIDDLEWARE = [ "scalekit.frameworks.django.ScalekitAuthMiddleware", ] ``` ### login_required [Section titled “login_required”](#login_required) modulescalekit.frameworks.djangohttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/django.py [#](#login_required)login_required View decorator that requires `request.scalekit_user`. Redirects to `SCALEKIT_LOGIN_PATH?returnTo=...` when the user is missing. Requires `ScalekitAuthMiddleware`. paramview_funcCallable Django view to protect. returnsCallable Wrapped view. Missing session → 302, never JSON 401. ```python from scalekit.frameworks.django import login_required @login_required def billing(request): return JsonResponse({"sub": request.scalekit_user["sub"]}) ``` ### get_session [Section titled “get_session”](#get_session) modulescalekit.frameworks.djangohttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/django.py [#](#get_session)get_session Read-only session lookup when you also need `expires_at`. Most views can read `request.scalekit_user` instead. Does not refresh or write a cookie. paramrequestHttpRequest Incoming request. returnsdict | None `{"user": ..., "expires_at": ...}`, or `None`. Never includes `access_token`, `refresh_token`, or `id_token`. ```python from scalekit.frameworks.django import get_session session = get_session(request) if session: print(session["user"]["sub"], session["expires_at"]) ``` --- # DOCUMENT BOUNDARY --- # Express session middleware > Add hosted login and an encrypted session cookie to Express with ScalekitAuth Use `ScalekitAuth` from `@scalekit-sdk/node/express` to add hosted login, an encrypted `sk_session` cookie, token refresh, and logout. Typical flow: install `@scalekit-sdk/node` 2.12.0 or later, mount `auth.router`, and guard one route with `auth.requiresAuth`. Use the methods below to change paths, logout, or pass an existing [ScalekitClient](/saaskit/sdks/node/scalekit-client/). Register these URLs in the Scalekit Dashboard under **Authentication > Redirects** before you test: | Dashboard field | Must match | | ---------------------------- | -------------------------------------------------------------------- | | **Redirect URI** | `redirectUri` exactly, for example `http://localhost:5001/callback` | | **Post Logout Redirect URI** | Absolute URL after full logout, for example `http://localhost:5001/` | | **Initiate Login URL** | Login path, for example `http://localhost:5001/login` | Store credentials in environment variables. Never hard-code secrets. .env ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=... COOKIE_ENCRYPTION_SECRET= # openssl rand -base64 32 REDIRECT_URI=http://localhost:5001/callback ``` Keep `COOKIE_ENCRYPTION_SECRET` identical on every server instance. The SDK does not ship a default. ## Install the package [Section titled “Install the package”](#install-the-package) Terminal ```bash npm install @scalekit-sdk/node express ``` `cookie-parser` is optional. `ScalekitAuth` reads the `Cookie` header when `req.cookies` is missing. ## Protect a route [Section titled “Protect a route”](#protect-a-route) server.ts ```typescript import express from 'express'; import { ScalekitAuth } from '@scalekit-sdk/node/express'; const auth = new ScalekitAuth({ envUrl: process.env.SCALEKIT_ENVIRONMENT_URL, clientId: process.env.SCALEKIT_CLIENT_ID, clientSecret: process.env.SCALEKIT_CLIENT_SECRET, redirectUri: process.env.REDIRECT_URI, cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET, }); const app = express(); app.use(auth.router); app.get('/account', auth.requiresAuth, (req, res) => { res.json({ sub: req.scalekitUser?.sub }); }); app.listen(5001); ``` Open `http://localhost:5001/account`. A missing session returns **302** to `/login?returnTo=/account`, not a JSON 401. After login, the callback restores `/account`. `req.scalekitUser` is access-token claims. `sub` is always present. `email` appears only when you add it as a custom access-token claim. Do not treat 302 as an API error A background `fetch` that follows redirects can complete a login instead of returning 401. Use `requiresAuth` on browser navigations, not as a JSON API gate. ### constructor [Section titled “constructor”](#constructor) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/express.ts [#](#constructor)constructor Creates the Express session helper and builds `auth.router` for `/login`, `/callback`, and `/logout`. paramclientScalekitClient Existing client. When omitted, the constructor builds one from `envUrl`, `clientId`, and `clientSecret`. optional paramenvUrlstring Scalekit environment URL. required if client is omitted paramclientIdstring Application client ID. required if client is omitted paramclientSecretstring Application client secret. required if client is omitted paramredirectUristring Exact **Redirect URI** registered in the dashboard. paramcookieEncryptionSecretstring Secret used to encrypt `sk_session`. Generate with `openssl rand -base64 32`. paramcookieNamestring Session cookie name. optional, default sk_session paramloginPathstring Path served by `auth.router` for login. optional, default /login paramcallbackPathstring Path served by `auth.router` for the OAuth callback. optional, default /callback paramlogoutPathstring Path served by `auth.router` for logout. optional, default /logout parampostLoginRedirectstring Fallback path after login when `returnTo` is absent. optional, default / parampostLogoutRedirectUristring Where logout lands. Defaults to `postLoginRedirect`. Register the absolute URL as **Post Logout Redirect URI**. optional paramfullLogoutboolean When `true`, logout ends the Scalekit session with `id_token_hint`. Set `false` to clear only the local cookie. optional, default true returnsScalekitAuth Helper with `router` and `requiresAuth`. ```typescript const auth = new ScalekitAuth({ envUrl: process.env.SCALEKIT_ENVIRONMENT_URL, clientId: process.env.SCALEKIT_CLIENT_ID, clientSecret: process.env.SCALEKIT_CLIENT_SECRET, redirectUri: process.env.REDIRECT_URI, cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET, }); app.use(auth.router); ``` ### requiresAuth [Section titled “requiresAuth”](#requiresauth) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/express.ts [#](#requiresAuth)asyncrequiresAuth Express middleware that requires a valid session. Refreshes the cookie about 10 seconds before expiry. Redirects to `loginPath` when the session is missing or invalid. paramreqRequest Incoming request. On success, sets `req.scalekitUser` to access-token claims. paramresResponse Outgoing response. May receive a refreshed `sk_session` cookie. paramnextNextFunction Called only when the session is valid. returnsPromise\ Completes the request, or sends 302 to `/login?returnTo=...`. ```typescript app.get('/billing', auth.requiresAuth, (req, res) => { res.send(`Hello ${req.scalekitUser.sub}`); }); ``` ### router [Section titled “router”](#router) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/express.ts [#](#router)router Express router that serves `loginPath`, `callbackPath`, and `logoutPath`. Mount it before protected routes. returnsRouter Router registered by the constructor. ```typescript app.use(auth.router); ``` Session cookies are Secure The helper sets the `Secure` attribute on `sk_session`. Use HTTPS, or a browser that accepts `Secure` cookies on `http://localhost`. --- # DOCUMENT BOUNDARY --- # FastAPI session middleware > Add hosted login and an encrypted session cookie to FastAPI with ScalekitAuth Use `ScalekitAuth` from `scalekit.frameworks.fastapi` to add hosted login, an encrypted `sk_session` cookie, token refresh, and logout. Typical flow: install the `fastapi` extra, call `auth.install(app)`, and protect an endpoint with `Depends(auth.requires_auth)`. Requires `scalekit-sdk-python` 2.17.0 or later. Register these URLs in the Scalekit Dashboard under **Authentication > Redirects** before you test: | Dashboard field | Must match | | ---------------------------- | -------------------------------------------------------------------- | | **Redirect URI** | `redirect_uri` exactly, for example `http://localhost:5001/callback` | | **Post Logout Redirect URI** | Absolute URL after full logout, for example `http://localhost:5001/` | | **Initiate Login URL** | Login path, for example `http://localhost:5001/login` | Store credentials in environment variables. Never hard-code secrets. .env ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=... COOKIE_ENCRYPTION_SECRET= # openssl rand -base64 32 REDIRECT_URI=http://localhost:5001/callback ``` Keep `COOKIE_ENCRYPTION_SECRET` identical on every server instance. ## Install the package [Section titled “Install the package”](#install-the-package) Terminal ```bash pip install "scalekit-sdk-python[fastapi]" ``` ## Protect a route [Section titled “Protect a route”](#protect-a-route) app.py ```python import os from fastapi import Depends, FastAPI from scalekit.frameworks.fastapi import ScalekitAuth app = FastAPI() auth = ScalekitAuth( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], redirect_uri=os.environ["REDIRECT_URI"], cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], cookie_secure=False, # set True behind HTTPS ) auth.install(app) @app.get("/account") async def account(user: dict = Depends(auth.requires_auth)): return {"sub": user["sub"]} ``` Open `http://localhost:5001/account`. A missing session returns **302** to `/login`, not a JSON 401. `user` is access-token claims. `sub` is always present. `email` appears only when you add it as a custom access-token claim. Set cookie_secure=False on local HTTP `cookie_secure` defaults to `True`. Set `cookie_secure=False` for local HTTP. Set it `True` in production. ### constructor [Section titled “constructor”](#constructor) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/fastapi.py [#](#__init__)\_\_init\_\_ Creates the FastAPI session helper. Call `install(app)` after construction to register routes and the 302 handler. paramclientScalekitClient | None Existing client. When omitted, the constructor builds one from `env_url`, `client_id`, and `client_secret`. optional paramenv_urlstr Scalekit environment URL. required if client is omitted paramclient_idstr Application client ID. required if client is omitted paramclient_secretstr Application client secret. required if client is omitted paramredirect_uristr Exact **Redirect URI** registered in the dashboard. paramcookie_encryption_secretstr Secret used to encrypt `sk_session`. Generate with `openssl rand -base64 32`. paramcookie_namestr Session cookie name. optional, default sk_session paramcookie_securebool Set `False` for local HTTP. optional, default True paramlogin_pathstr Login route path. optional, default /login paramcallback_pathstr Callback route path. optional, default /callback paramlogout_pathstr Logout route path. optional, default /logout parampost_login_redirectstr Fallback path after login when `returnTo` is absent. optional, default / parampost_logout_redirect_uristr | None Where logout lands. Defaults to `post_login_redirect`. optional paramfull_logoutbool When `True`, logout ends the Scalekit session with `id_token_hint`. optional, default True returnsScalekitAuth Helper with `install`, `requires_auth`, and `get_session`. ```python auth = ScalekitAuth( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], redirect_uri=os.environ["REDIRECT_URI"], cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], ) auth.install(app) ``` ### install [Section titled “install”](#install) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/fastapi.py [#](#install)install Mounts the login, callback, and logout routes and registers the exception handler that turns a missing session into a 302. Call this once. `app.include_router(auth.router)` alone skips the 302 handler. paramappFastAPI FastAPI application. returnsNone Routes and the redirect handler are registered in place. ```python auth.install(app) ``` ### requires_auth [Section titled “requires_auth”](#requires_auth) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/fastapi.py [#](#requires_auth)asyncrequires_auth FastAPI dependency. Use `user: dict = Depends(auth.requires_auth)`. Session check and refresh run in a thread pool so they do not block the event loop. paramrequestRequest Incoming request. Injected by FastAPI. paramresponseResponse Injected response. Receives a refreshed `sk_session` cookie when needed. returnsdict | None Access-token claims. Missing session raises an internal error that `install` converts to 302, not `HTTPException(401)`. ```python @app.get("/billing") async def billing(user: dict = Depends(auth.requires_auth)): return {"sub": user["sub"]} ``` Do not return a Response from a protected endpoint `requires_auth` writes the refreshed cookie on the injected `response`. If the endpoint returns a `Response` instance, FastAPI discards that injected response. Return a plain value, or copy `response.raw_headers` onto the response you return. ### get_session [Section titled “get_session”](#get_session) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/fastapi.py [#](#get_session)get_session Read-only session lookup. Does not refresh or write a cookie. Only `requires_auth` refreshes the session. paramrequestRequest Incoming request. returnsdict | None `{"user": ..., "expires_at": ...}`, or `None`. Never includes `access_token`, `refresh_token`, or `id_token`. ```python session = auth.get_session(request) if session: print(session["user"]["sub"], session["expires_at"]) ``` --- # DOCUMENT BOUNDARY --- # Flask session middleware > Add hosted login and an encrypted session cookie to Flask with ScalekitAuth Use `ScalekitAuth` from `scalekit.frameworks.flask` to add hosted login, an encrypted `sk_session` cookie, token refresh, and logout. Typical flow: install the `flask` extra, construct `ScalekitAuth` with the Flask app, and decorate one view with `@auth.requires_auth`. Requires `scalekit-sdk-python` 2.17.0 or later. Register these URLs in the Scalekit Dashboard under **Authentication > Redirects** before you test: | Dashboard field | Must match | | ---------------------------- | -------------------------------------------------------------------- | | **Redirect URI** | `redirect_uri` exactly, for example `http://localhost:5001/callback` | | **Post Logout Redirect URI** | Absolute URL after full logout, for example `http://localhost:5001/` | | **Initiate Login URL** | Login path, for example `http://localhost:5001/login` | Store credentials in environment variables. Never hard-code secrets. .env ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=... COOKIE_ENCRYPTION_SECRET= # openssl rand -base64 32 REDIRECT_URI=http://localhost:5001/callback ``` Keep `COOKIE_ENCRYPTION_SECRET` identical on every server instance. The SDK does not ship a default. ## Install the package [Section titled “Install the package”](#install-the-package) Terminal ```bash pip install "scalekit-sdk-python[flask]" ``` ## Protect a route [Section titled “Protect a route”](#protect-a-route) app.py ```python import os from flask import Flask from scalekit.frameworks.flask import ScalekitAuth app = Flask(__name__) auth = ScalekitAuth( app, env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], redirect_uri=os.environ["REDIRECT_URI"], cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], cookie_secure=False, # set True behind HTTPS ) @app.route("/account") @auth.requires_auth def account(): return {"sub": auth.current_user["sub"]} ``` Open `http://localhost:5001/account`. A missing session returns **302** to `/login`, not a JSON 401. `auth.current_user` is access-token claims. `sub` is always present. `email` appears only when you add it as a custom access-token claim. Set cookie_secure=False on local HTTP `cookie_secure` defaults to `True`. Some browsers drop a `Secure` cookie on plain `http://localhost`, which looks like a session that never sticks. Set `cookie_secure=False` for local HTTP. Set it `True` in production. ### constructor [Section titled “constructor”](#constructor) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/flask.py [#](#__init__)\_\_init\_\_ Creates the Flask session helper. Pass `app` to register routes immediately, or call `init_app` later. paramappFlask | None Flask app. When provided, registers `/login`, `/callback`, and `/logout`. optional paramclientScalekitClient | None Existing client. When omitted, the constructor builds one from `env_url`, `client_id`, and `client_secret`. optional paramenv_urlstr Scalekit environment URL. required if client is omitted paramclient_idstr Application client ID. required if client is omitted paramclient_secretstr Application client secret. required if client is omitted paramredirect_uristr Exact **Redirect URI** registered in the dashboard. paramcookie_encryption_secretstr Secret used to encrypt `sk_session`. Generate with `openssl rand -base64 32`. paramcookie_namestr Session cookie name. optional, default sk_session paramcookie_securebool Set `False` for local HTTP. optional, default True paramlogin_pathstr Login view path. optional, default /login paramcallback_pathstr Callback view path. optional, default /callback paramlogout_pathstr Logout view path. optional, default /logout parampost_login_redirectstr Fallback path after login when `returnTo` is absent. optional, default / parampost_logout_redirect_uristr | None Where logout lands. Defaults to `post_login_redirect`. optional paramfull_logoutbool When `True`, logout ends the Scalekit session with `id_token_hint`. optional, default True returnsScalekitAuth Helper with `requires_auth`, `current_user`, and `get_session`. ```python auth = ScalekitAuth( app, env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], redirect_uri=os.environ["REDIRECT_URI"], cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], ) ``` ### init_app [Section titled “init_app”](#init_app) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/flask.py [#](#init_app)init_app Registers the login, callback, and logout views on a Flask app. Use this when you construct `ScalekitAuth` without `app`. paramappFlask Flask application. returnsNone Routes are added in place. ```python auth = ScalekitAuth( redirect_uri=os.environ["REDIRECT_URI"], cookie_encryption_secret=os.environ["COOKIE_ENCRYPTION_SECRET"], env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) auth.init_app(app) ``` ### requires_auth [Section titled “requires_auth”](#requires_auth) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/flask.py [#](#requires_auth)requires_auth View decorator that requires a valid session. Sets `g.scalekit_user` and refreshes the cookie about 10 seconds before expiry. Redirects to `login_path` when the session is missing. paramview_funcCallable Flask view to protect. returnsCallable Wrapped view. Missing session → 302, never JSON 401. ```python @app.route("/billing") @auth.requires_auth def billing(): return {"sub": auth.current_user["sub"]} ``` ### current_user [Section titled “current_user”](#current_user) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/flask.py [#](#current_user)current_user Access-token claims for the current request. Same object as `g.scalekit_user`. returnsdict | None Claims when `requires_auth` has run, otherwise `None`. ```python sub = auth.current_user["sub"] ``` ### get_session [Section titled “get_session”](#get_session) classScalekitAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/frameworks/flask.py [#](#get_session)get_session Read-only session lookup for a public page that shows logged-in vs logged-out state. Does not refresh or write a cookie. Only `requires_auth` refreshes the session. returnsdict | None `{"user": ..., "expires_at": ...}`, or `None`. Never includes `access_token`, `refresh_token`, or `id_token`. ```python session = auth.get_session() if session: print(session["user"]["sub"], session["expires_at"]) ``` --- # DOCUMENT BOUNDARY --- # API tokens > API tokens for service access Create and validate API tokens for programmatic access to Scalekit APIs. Use tokens for service access that is not a full end-user session. Create a token, store it securely, and validate it on incoming requests. ### CreateToken [Section titled “CreateToken”](#createtoken) clientAPITokenshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/token.go [#](#CreateToken)asyncCreateToken Runs `CreateToken` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramoptionsCreateTokenOptions Optional request settings. returnsCreateTokenResponse The created resource. ```go expiry := time.Now().Add(24 * time.Hour) resp, err := scalekitClient.Token().CreateToken(ctx, "org_123", scalekit.CreateTokenOptions{ UserId: "usr_123", Description: "Service account token", CustomClaims: map[string]string{ "source": "service", }, Expiry: &expiry, }) if err != nil { // handle } _ = resp.Token ``` ### ValidateToken [Section titled “ValidateToken”](#validatetoken) clientAPITokenshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/token.go [#](#ValidateToken)asyncValidateToken Runs `ValidateToken` and returns the result. paramctxcontext.Context Request context paramtokenstring Token string. returnsValidateTokenResponse Verified claims or result. ```go resp, err := scalekitClient.Token().ValidateToken(ctx, "api_token_here") if err != nil { // token is invalid or expired } _ = resp.Token ``` ### InvalidateToken [Section titled “InvalidateToken”](#invalidatetoken) clientAPITokenshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/token.go [#](#InvalidateToken)asyncInvalidateToken Runs `InvalidateToken` and returns the result. paramctxcontext.Context Request context paramtokenstring Token string. returnserror error ```go if err := scalekitClient.Token().InvalidateToken(ctx, "api_token_here"); err != nil { // handle } ``` ### ListTokens [Section titled “ListTokens”](#listtokens) clientAPITokenshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/token.go [#](#ListTokens)asyncListTokens Runs `ListTokens` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramoptionsListTokensOptions Optional request settings. returnsListTokensResponse Paginated results. ```go tokens, err := scalekitClient.Token().ListTokens(ctx, "org_123", scalekit.ListTokensOptions{}) if err != nil { // handle } for _, token := range tokens.Tokens { _ = token } // Filter by user ID userTokens, err := scalekitClient.Token().ListTokens(ctx, "org_123", scalekit.ListTokensOptions{ UserId: "usr_123", }) ``` ### UpdateToken [Section titled “UpdateToken”](#updatetoken) clientAPITokenshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/token.go [#](#UpdateToken)asyncUpdateToken Runs `UpdateToken` and returns the result. paramctxcontext.Context Request context paramtokenstring Token string. paramoptionsUpdateTokenOptions Optional request settings. returnsUpdateTokenResponse The updated resource. --- # DOCUMENT BOUNDARY --- # Authorization > Authorization helpers for protected resources Use auth helpers for authorization code exchange, token validation, and related login token flows on the Scalekit client. These methods sit next to session management—use them when implementing hosted login callbacks and token checks. ### UpdateLoginUserDetails [Section titled “UpdateLoginUserDetails”](#updateloginuserdetails) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/auth_service.go [#](#UpdateLoginUserDetails)UpdateLoginUserDetails Runs `UpdateLoginUserDetails` and returns the result. paramctxcontext.Context Request context paramreqUpdateLoginUserDetailsRequest Req. returns(\*UpdateLoginUserDetailsResponse, error) Response carrying the auth request ID (`resp.GetAuthRequestId()`), plus an error. ```go req := &scalekit.UpdateLoginUserDetailsRequest{ ConnectionId: "conn_123", LoginRequestId: "login_req_123", User: &scalekit.LoggedInUserDetails{ Sub: "sub", Email: "user@example.com", }, } resp, err := scalekitClient.Auth().UpdateLoginUserDetails(ctx, req) if err != nil { // handle the error, then stop return } // resp is non-nil only on success _ = resp.GetAuthRequestId() ``` --- # DOCUMENT BOUNDARY --- # OAuth clients > OAuth clients for applications Manage OAuth clients for applications that use Scalekit as an authorization server. Register clients for your apps, rotate secrets carefully, and scope clients to the right application type. ### CreateClient [Section titled “CreateClient”](#createclient) clientClientshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/clients.go [#](#CreateClient)asyncCreateClient Runs `CreateClient` and returns the result. paramctxcontext.Context Request context paramclientclientsv1.CreateClient Client. returnsCreateClientResponse The created resource. ```go import ( clients "github.com/scalekit-inc/scalekit-sdk-go/v2/pkg/grpc/scalekit/v1/clients" ) created, err := scalekitClient.Client().CreateClient(ctx, &clients.CreateClient{ Name: "My Application", ClientType: "WEB_APP", PostLoginUris: []string{"https://myapp.com/callback"}, PostLogoutRedirectUris: []string{"https://myapp.com/logout"}, }) if err != nil { // handle } _ = created.Client ``` ### GetClient [Section titled “GetClient”](#getclient) clientClientshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/clients.go [#](#GetClient)asyncGetClient Runs `GetClient` and returns the result. paramctxcontext.Context Request context paramclientIdstring Client ID. returnsGetClientResponse The response payload for this operation. ```go got, err := scalekitClient.Client().GetClient(ctx, "client_123") if err != nil { // handle } _ = got.Client ``` ### ListClients [Section titled “ListClients”](#listclients) clientClientshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/clients.go [#](#ListClients)asyncListClients Runs `ListClients` and returns the result. paramctxcontext.Context Request context paramoptionsListClientsOptions Optional request settings. returnsListClientsResponse Paginated results. ```go clients, err := scalekitClient.Client().ListClients(ctx, &scalekit.ListClientsOptions{ PageSize: 10, PageToken: "", }) if err != nil { // handle } for _, c := range clients.Clients { _ = c } ``` ### UpdateClient [Section titled “UpdateClient”](#updateclient) clientClientshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/clients.go [#](#UpdateClient)asyncUpdateClient Runs `UpdateClient` and returns the result. paramctxcontext.Context Request context paramclientIdstring Client ID. paramclientclientsv1.UpdateClient Client. parammaskfieldmaskpb.FieldMask Mask. returnsUpdateClientResponse The updated resource. ```go import ( clients "github.com/scalekit-inc/scalekit-sdk-go/v2/pkg/grpc/scalekit/v1/clients" "google.golang.org/protobuf/types/known/fieldmaskpb" ) updated, err := scalekitClient.Client().UpdateClient(ctx, "client_123", &clients.UpdateClient{ Name: "Updated Name", }, &fieldmaskpb.FieldMask{ Paths: []string{"name"}, }) if err != nil { // handle } _ = updated.Client ``` ### DeleteClient [Section titled “DeleteClient”](#deleteclient) clientClientshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/clients.go [#](#DeleteClient)asyncDeleteClient Runs `DeleteClient` and returns the result. paramctxcontext.Context Request context paramclientIdstring Client ID. returnserror error ```go if err := scalekitClient.Client().DeleteClient(ctx, "client_123"); err != nil { // handle } ``` ### AddClientSecret [Section titled “AddClientSecret”](#addclientsecret) clientClientshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/clients.go [#](#AddClientSecret)asyncAddClientSecret Runs `AddClientSecret` and returns the result. paramctxcontext.Context Request context paramclientIdstring Client ID. returnsCreateClientSecretResponse The created resource. ### RemoveClientSecret [Section titled “RemoveClientSecret”](#removeclientsecret) clientClientshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/clients.go [#](#RemoveClientSecret)asyncRemoveClientSecret Runs `RemoveClientSecret` and returns the result. paramctxcontext.Context Request context paramclientIdstring Client ID. paramsecretIdstring Secret id. returnserror error --- # DOCUMENT BOUNDARY --- # Connections > Configure enterprise SSO connections Use the connection client to configure enterprise SSO (SAML/OIDC) for an organization. Enable SSO after the organization exists: create a connection, complete IdP setup, and test sign-in for that org. ### CreateConnection [Section titled “CreateConnection”](#createconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/connection.go [#](#CreateConnection)asyncCreateConnection Runs `CreateConnection` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramconnectionconnectionsv1.CreateConnection Connection. returnsCreateConnectionResponse The created resource. ```go import ( connectionsv1 "github.com/scalekit-inc/scalekit-sdk-go/v2/pkg/grpc/scalekit/v1/connections" ) created, err := scalekitClient.Connection().CreateConnection(ctx, "org_123", &connectionsv1.CreateConnection{ Provider: connectionsv1.ConnectionProvider_OKTA, Type: connectionsv1.ConnectionType_SAML, ProviderKey: "my-okta-connection", }) if err != nil { // handle } _ = created.Connection ``` ### DeleteConnection [Section titled “DeleteConnection”](#deleteconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/connection.go [#](#DeleteConnection)asyncDeleteConnection Runs `DeleteConnection` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramidstring Resource ID. returnserror error ```go if err := scalekitClient.Connection().DeleteConnection(ctx, "org_123", "conn_456"); err != nil { // handle } ``` ### GetConnection [Section titled “GetConnection”](#getconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/connection.go [#](#GetConnection)asyncGetConnection Runs `GetConnection` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramidstring Resource ID. returnsGetConnectionResponse The response payload for this operation. ```go conn, err := scalekitClient.Connection().GetConnection(ctx, "org_123", "conn_123") if err != nil { // handle } _ = conn.Connection ``` ### ListConnectionsByDomain [Section titled “ListConnectionsByDomain”](#listconnectionsbydomain) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/connection.go [#](#ListConnectionsByDomain)asyncListConnectionsByDomain Runs `ListConnectionsByDomain` and returns the result. paramctxcontext.Context Request context paramdomainstring Domain name. returnsListConnectionsResponse Paginated connections. ```go conns, err := scalekitClient.Connection().ListConnectionsByDomain(ctx, "acme.com") if err != nil { // handle } _ = conns.Connections ``` ### ListConnections [Section titled “ListConnections”](#listconnections) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/connection.go [#](#ListConnections)asyncListConnections Runs `ListConnections` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. returnsListConnectionsResponse Paginated connections. ```go conns, err := scalekitClient.Connection().ListConnections(ctx, "org_123") if err != nil { // handle } _ = conns.Connections ``` ### EnableConnection [Section titled “EnableConnection”](#enableconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/connection.go [#](#EnableConnection)asyncEnableConnection Runs `EnableConnection` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramidstring Resource ID. returnsToggleConnectionResponse The updated resource. ```go resp, err := scalekitClient.Connection().EnableConnection(ctx, "org_123", "conn_123") if err != nil { // handle } _ = resp.Enabled ``` ### DisableConnection [Section titled “DisableConnection”](#disableconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/connection.go [#](#DisableConnection)asyncDisableConnection Runs `DisableConnection` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramidstring Resource ID. returnsToggleConnectionResponse The updated resource. ```go resp, err := scalekitClient.Connection().DisableConnection(ctx, "org_123", "conn_123") if err != nil { // handle } _ = resp.Enabled ``` --- # DOCUMENT BOUNDARY --- # Directory > Sync users and groups via SCIM directories Use the directory client for SCIM directory sync—listing providers, users, and groups provisioned from an IdP. Use this after SSO is in place when the customer provisions users via SCIM. ### CreateDirectory [Section titled “CreateDirectory”](#createdirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#CreateDirectory)asyncCreateDirectory Runs `CreateDirectory` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramdirectorydirectoriesv1.CreateDirectory Directory. returnsCreateDirectoryResponse The created resource. ```go import ( directoriesv1 "github.com/scalekit-inc/scalekit-sdk-go/v2/pkg/grpc/scalekit/v1/directories" ) created, err := scalekitClient.Directory().CreateDirectory(ctx, "org_123", &directoriesv1.CreateDirectory{ DirectoryType: directoriesv1.DirectoryType_SCIM, DirectoryProvider: directoriesv1.DirectoryProvider_OKTA, }) if err != nil { // handle } _ = created.Directory ``` ### DeleteDirectory [Section titled “DeleteDirectory”](#deletedirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#DeleteDirectory)asyncDeleteDirectory Runs `DeleteDirectory` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramdirectoryIdstring Directory ID. returnserror error ```go if err := scalekitClient.Directory().DeleteDirectory(ctx, "org_123", "dir_456"); err != nil { // handle } ``` ### ListDirectories [Section titled “ListDirectories”](#listdirectories) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#ListDirectories)asyncListDirectories Runs `ListDirectories` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. returnsListDirectoriesResponse Paginated results. ```go dirs, err := scalekitClient.Directory().ListDirectories(ctx, "org_123") if err != nil { // handle } _ = dirs.Directories ``` ### GetDirectory [Section titled “GetDirectory”](#getdirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#GetDirectory)asyncGetDirectory Runs `GetDirectory` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramdirectoryIdstring Directory ID. returnsGetDirectoryResponse The response payload for this operation. ```go dir, err := scalekitClient.Directory().GetDirectory(ctx, "org_123", "dir_123") if err != nil { // handle } _ = dir.Directory ``` ### GetPrimaryDirectoryByOrganizationId [Section titled “GetPrimaryDirectoryByOrganizationId”](#getprimarydirectorybyorganizationid) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#GetPrimaryDirectoryByOrganizationId)asyncGetPrimaryDirectoryByOrganizationId Runs `GetPrimaryDirectoryByOrganizationId` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. returnsGetDirectoryResponse The response payload for this operation. ```go dir, err := scalekitClient.Directory().GetPrimaryDirectoryByOrganizationId(ctx, "org_123") if err != nil { // handle } _ = dir.Directory ``` ### ListDirectoryUsers [Section titled “ListDirectoryUsers”](#listdirectoryusers) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#ListDirectoryUsers)asyncListDirectoryUsers Runs `ListDirectoryUsers` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramdirectoryIdstring Directory ID. paramoptionsListDirectoryUsersOptions Optional request settings. returnsListDirectoryUsersResponse Paginated results. ```go includeDetail := true updatedAfter := time.Unix(1729851960, 0) resp, err := scalekitClient.Directory().ListDirectoryUsers(ctx, "org_123", "dir_123", &scalekit.ListDirectoryUsersOptions{ PageSize: 10, PageToken: "", IncludeDetail: &includeDetail, UpdatedAfter: &updatedAfter, }) if err != nil { // handle } _ = resp.Users ``` ### ListDirectoryGroups [Section titled “ListDirectoryGroups”](#listdirectorygroups) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#ListDirectoryGroups)asyncListDirectoryGroups Runs `ListDirectoryGroups` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramdirectoryIdstring Directory ID. paramoptionsListDirectoryGroupsOptions Optional request settings. returnsListDirectoryGroupsResponse Paginated results. ```go includeDetail := true resp, err := scalekitClient.Directory().ListDirectoryGroups(ctx, "org_123", "dir_123", &scalekit.ListDirectoryGroupsOptions{ PageSize: 10, PageToken: "", IncludeDetail: &includeDetail, }) if err != nil { // handle } _ = resp.Groups ``` ### EnableDirectory [Section titled “EnableDirectory”](#enabledirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#EnableDirectory)asyncEnableDirectory Runs `EnableDirectory` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramdirectoryIdstring Directory ID. returnsToggleDirectoryResponse The updated resource. ```go resp, err := scalekitClient.Directory().EnableDirectory(ctx, "org_123", "dir_123") if err != nil { // handle } _ = resp.Enabled ``` ### DisableDirectory [Section titled “DisableDirectory”](#disabledirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/directory.go [#](#DisableDirectory)asyncDisableDirectory Runs `DisableDirectory` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramdirectoryIdstring Directory ID. returnsToggleDirectoryResponse The updated resource. ```go resp, err := scalekitClient.Directory().DisableDirectory(ctx, "org_123", "dir_123") if err != nil { // handle } _ = resp.Enabled ``` --- # DOCUMENT BOUNDARY --- # Domains > Manage organization email domains Use the domain client to manage verified email domains for an organization (SSO routing and domain-based policies). Add a domain, complete verification, then use it with SSO and organization policies. ### CreateDomain [Section titled “CreateDomain”](#createdomain) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/domain.go [#](#CreateDomain)asyncCreateDomain Runs `CreateDomain` and returns the result. paramctxcontext.Context Request context paramnamestring Display name. paramoptions...\*CreateDomainOptions Optional request settings. returnsCreateDomainResponse The created resource. ```go created, err := scalekitClient.Domain().CreateDomain(ctx, "org_123", "acme.com") if err != nil { // handle } _ = created.Domain // With options created2, err := scalekitClient.Domain().CreateDomain(ctx, "org_123", "acme.com", &scalekit.CreateDomainOptions{ DomainType: scalekit.DomainTypeOrganization, }) if err != nil { // handle } _ = created2.Domain ``` ### GetDomain [Section titled “GetDomain”](#getdomain) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/domain.go [#](#GetDomain)asyncGetDomain Runs `GetDomain` and returns the result. paramctxcontext.Context Request context paramidstring Resource ID. paramorganizationIdstring Organization ID. returnsGetDomainResponse The response payload for this operation. ```go domain, err := scalekitClient.Domain().GetDomain(ctx, "dom_123", "org_123") if err != nil { // handle } _ = domain.Domain ``` ### ListDomains [Section titled “ListDomains”](#listdomains) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/domain.go [#](#ListDomains)asyncListDomains Runs `ListDomains` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramoptions...\*ListDomainOptions Optional request settings. returnsListDomainResponse Paginated results. ```go all, err := scalekitClient.Domain().ListDomains(ctx, "org_123") if err != nil { // handle } _ = all.Domains // Filter by domain type orgDomains, err := scalekitClient.Domain().ListDomains(ctx, "org_123", &scalekit.ListDomainOptions{ DomainType: scalekit.DomainTypeOrganization, }) if err != nil { // handle } _ = orgDomains.Domains ``` ### DeleteDomain [Section titled “DeleteDomain”](#deletedomain) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/domain.go [#](#DeleteDomain)asyncDeleteDomain Runs `DeleteDomain` and returns the result. paramctxcontext.Context Request context paramidstring Resource ID. paramorganizationIdstring Organization ID. returnserror error ```go if err := scalekitClient.Domain().DeleteDomain(ctx, "dom_123", "org_123"); err != nil { // handle } ``` --- # DOCUMENT BOUNDARY --- # M2M clients > Machine-to-machine clients and tokens Use the M2M client to create and manage machine-to-machine OAuth clients and secrets for service-to-service auth. Create an M2M client, store the secret securely, then obtain tokens with client credentials. ### CreateOrganizationClient [Section titled “CreateOrganizationClient”](#createorganizationclient) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/m2mclient.go [#](#CreateOrganizationClient)asyncCreateOrganizationClient Runs `CreateOrganizationClient` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramoptionsCreateOrganizationClientOptions Optional request settings. returnsCreateOrganizationClientResponse The created resource. ### GetOrganizationClient [Section titled “GetOrganizationClient”](#getorganizationclient) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/m2mclient.go [#](#GetOrganizationClient)asyncGetOrganizationClient Runs `GetOrganizationClient` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramclientIdstring Client ID. returnsGetOrganizationClientResponse The response payload for this operation. ### UpdateOrganizationClient [Section titled “UpdateOrganizationClient”](#updateorganizationclient) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/m2mclient.go [#](#UpdateOrganizationClient)asyncUpdateOrganizationClient Runs `UpdateOrganizationClient` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramclientIdstring Client ID. paramoptionsUpdateOrganizationClientOptions Optional request settings. returnsUpdateOrganizationClientResponse The updated resource. ### DeleteOrganizationClient [Section titled “DeleteOrganizationClient”](#deleteorganizationclient) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/m2mclient.go [#](#DeleteOrganizationClient)asyncDeleteOrganizationClient Runs `DeleteOrganizationClient` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramclientIdstring Client ID. returnserror error ### CreateOrganizationClientSecret [Section titled “CreateOrganizationClientSecret”](#createorganizationclientsecret) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/m2mclient.go [#](#CreateOrganizationClientSecret)asyncCreateOrganizationClientSecret Runs `CreateOrganizationClientSecret` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramclientIdstring Client ID. returnsCreateOrganizationClientSecretResponse The created resource. ### DeleteOrganizationClientSecret [Section titled “DeleteOrganizationClientSecret”](#deleteorganizationclientsecret) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/m2mclient.go [#](#DeleteOrganizationClientSecret)asyncDeleteOrganizationClientSecret Runs `DeleteOrganizationClientSecret` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramclientIdstring Client ID. paramsecretIdstring Secret id. returnserror error ### ListOrganizationClients [Section titled “ListOrganizationClients”](#listorganizationclients) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/m2mclient.go [#](#ListOrganizationClients)asyncListOrganizationClients Runs `ListOrganizationClients` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramoptionsListOrganizationClientsOptions Optional request settings. returnsListOrganizationClientsResponse Paginated results. --- # DOCUMENT BOUNDARY --- # Organizations > Manage your customer organizations Use the Organization client to create and manage customer tenants (organizations) in your Scalekit application. Start here when onboarding a new customer: create an organization, set external IDs and branding, then add users and SSO for that org. ### CreateOrganization [Section titled “CreateOrganization”](#createorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#CreateOrganization)asyncCreateOrganization Runs `CreateOrganization` and returns the result. paramctxcontext.Context Request context paramnamestring Display name. paramoptionsCreateOrganizationOptions Optional request settings. returnsCreateOrganizationResponse The created organization. ```go import ( "context" "fmt" "github.com/scalekit-inc/scalekit-sdk-go/v2" ) ctx := context.Background() org, err := scalekitClient.Organization().CreateOrganization(ctx, "Acme Corporation", scalekit.CreateOrganizationOptions{ ExternalId: "customer_12345", Metadata: map[string]string{ "source": "signup", }, }) if err != nil { // handle } fmt.Println("Organization ID:", org.Organization.Id) ``` ### ListOrganization [Section titled “ListOrganization”](#listorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#ListOrganization)asyncListOrganization Runs `ListOrganization` and returns the result. paramctxcontext.Context Request context paramrequestListOrganizationOptions Request payload. returnsListOrganizationsResponse Paginated organizations. ```go orgs, err := scalekitClient.Organization().ListOrganization(ctx, &scalekit.ListOrganizationOptions{ PageSize: 10, PageToken: "", }) if err != nil { // handle } for _, org := range orgs.Organizations { _ = org.Id } // Use server defaults orgs, err = scalekitClient.Organization().ListOrganization(ctx, nil) ``` ### GetOrganization [Section titled “GetOrganization”](#getorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#GetOrganization)asyncGetOrganization Runs `GetOrganization` and returns the result. paramctxcontext.Context Request context paramidstring Resource ID. returnsGetOrganizationResponse The organization. ```go org, err := scalekitClient.Organization().GetOrganization(ctx, "org_123") if err != nil { // handle } _ = org.Organization ``` ### GetOrganizationByExternalId [Section titled “GetOrganizationByExternalId”](#getorganizationbyexternalid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#GetOrganizationByExternalId)asyncGetOrganizationByExternalId Runs `GetOrganizationByExternalId` and returns the result. paramctxcontext.Context Request context paramexternalIdstring External ID. returnsGetOrganizationResponse The organization. ```go org, err := scalekitClient.Organization().GetOrganizationByExternalId(ctx, "customer_12345") if err != nil { // handle } _ = org.Organization ``` ### UpdateOrganization [Section titled “UpdateOrganization”](#updateorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#UpdateOrganization)asyncUpdateOrganization Runs `UpdateOrganization` and returns the result. paramctxcontext.Context Request context paramidstring Resource ID. paramorganizationUpdateOrganization Fields to update. returnsUpdateOrganizationResponse The updated organization. ```go // DisplayName: Display name updated, err := scalekitClient.Organization().UpdateOrganization( ctx, "org_123", &organizations.UpdateOrganization{ DisplayName: func() *string { s := "Updated name"; return &s }(), }, ) if err != nil { // handle } _ = updated.Organization ``` ### UpdateOrganizationByExternalId [Section titled “UpdateOrganizationByExternalId”](#updateorganizationbyexternalid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#UpdateOrganizationByExternalId)asyncUpdateOrganizationByExternalId Runs `UpdateOrganizationByExternalId` and returns the result. paramctxcontext.Context Request context paramexternalIdstring External ID. paramorganizationUpdateOrganization Fields to update. returnsUpdateOrganizationResponse The updated organization. ```go // DisplayName: Display name updated, err := scalekitClient.Organization().UpdateOrganizationByExternalId( ctx, "customer_12345", &organizations.UpdateOrganization{ DisplayName: func() *string { s := "Updated name"; return &s }(), }, ) if err != nil { // handle } _ = updated.Organization ``` ### DeleteOrganization [Section titled “DeleteOrganization”](#deleteorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#DeleteOrganization)asyncDeleteOrganization Runs `DeleteOrganization` and returns the result. paramctxcontext.Context Request context paramidstring Resource ID. returnserror error ```go if err := scalekitClient.Organization().DeleteOrganization(ctx, "org_123"); err != nil { // handle } ``` ### GeneratePortalLink [Section titled “GeneratePortalLink”](#generateportallink) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#GeneratePortalLink)asyncGeneratePortalLink Runs `GeneratePortalLink` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. returnsLink Authorization link. ```go link, err := scalekitClient.Organization().GeneratePortalLink(ctx, "org_123") if err != nil { // handle } _ = link.Url ``` ### UpdateOrganizationSettings [Section titled “UpdateOrganizationSettings”](#updateorganizationsettings) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#UpdateOrganizationSettings)asyncUpdateOrganizationSettings Runs `UpdateOrganizationSettings` and returns the result. paramctxcontext.Context Request context paramidstring Resource ID. paramsettingsOrganizationSettings Settings. returnsGetOrganizationResponse The organization. ```go resp, err := scalekitClient.Organization().UpdateOrganizationSettings(ctx, "org_123", scalekit.OrganizationSettings{ Features: []scalekit.Feature{ {Name: "sso", Enabled: true}, {Name: "dir_sync", Enabled: true}, }, }) if err != nil { // handle } _ = resp.Organization.Settings ``` ### UpsertUserManagementSettings [Section titled “UpsertUserManagementSettings”](#upsertusermanagementsettings) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#UpsertUserManagementSettings)asyncUpsertUserManagementSettings Runs `UpsertUserManagementSettings` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramsettingsOrganizationUserManagementSettings Settings. returnsorganizationsv1.OrganizationUserManagementSettings organizationsv1.OrganizationUserManagementSettings ```go maxUsers := int32(150) settings, err := scalekitClient.Organization().UpsertUserManagementSettings( ctx, "org_123", scalekit.OrganizationUserManagementSettings{ MaxAllowedUsers: &maxUsers, }, ) if err != nil { // handle } _ = settings.MaxAllowedUsers ``` ### GetOrganizationSessionPolicy [Section titled “GetOrganizationSessionPolicy”](#getorganizationsessionpolicy) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#GetOrganizationSessionPolicy)asyncGetOrganizationSessionPolicy Runs `GetOrganizationSessionPolicy` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. returnsOrganizationSessionPolicySettings The response payload for this operation. ```go policy, err := scalekitClient.Organization().GetOrganizationSessionPolicy(ctx, "org_123") if err != nil { // handle } if policy.PolicySource == scalekit.SessionPolicySourceCustom { _ = policy.AbsoluteSessionTimeout.GetValue() } ``` ### UpdateOrganizationSessionPolicy [Section titled “UpdateOrganizationSessionPolicy”](#updateorganizationsessionpolicy) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/organization.go [#](#UpdateOrganizationSessionPolicy)asyncUpdateOrganizationSessionPolicy Runs `UpdateOrganizationSessionPolicy` and returns the result. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. parampolicyOrganizationSessionPolicy Policy. returnsOrganizationSessionPolicySettings The updated resource. ```go timeout := int32(360) idleTimeout := int32(60) idleEnabled := true policy, err := scalekitClient.Organization().UpdateOrganizationSessionPolicy(ctx, "org_123", scalekit.OrganizationSessionPolicy{ PolicySource: scalekit.SessionPolicySourceCustom, AbsoluteSessionTimeout: &timeout, AbsoluteSessionTimeoutUnit: scalekit.TimeUnitMinutes, IdleSessionTimeoutEnabled: &idleEnabled, IdleSessionTimeout: &idleTimeout, IdleSessionTimeoutUnit: scalekit.TimeUnitMinutes, }) if err != nil { // handle } _ = policy // Revert to application defaults reverted, err := scalekitClient.Organization().UpdateOrganizationSessionPolicy(ctx, "org_123", scalekit.OrganizationSessionPolicy{ PolicySource: scalekit.SessionPolicySourceApplication, }) if err != nil { // handle } _ = reverted ``` --- # DOCUMENT BOUNDARY --- # Magic link & OTP > Send and verify passwordless login codes Use the passwordless client to send and verify email magic links or one-time codes for sign-in. Call send, then verify with the code or link token from the user’s email to complete login. ### SendPasswordlessEmail [Section titled “SendPasswordlessEmail”](#sendpasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/passwordless.go [#](#SendPasswordlessEmail)asyncSendPasswordlessEmail SendPasswordlessEmail sends a passwordless authentication email paramctxcontext.Context Request context paramemailstring Email address. paramoptionsSendPasswordlessOptions Optional request settings. returnsSendPasswordlessResponse Send passwordless. ```go template := scalekit.TemplateTypeSignin resp, err := scalekitClient.Passwordless().SendPasswordlessEmail(ctx, "user@example.com", &scalekit.SendPasswordlessOptions{ Template: &template, MagiclinkAuthUri: "https://myapp.com/auth/callback", State: "state", ExpiresIn: 1800, TemplateVariables: map[string]string{ "app_name": "My App", }, }) if err != nil { // handle } _ = resp.AuthRequestId ``` ### VerifyPasswordlessEmail [Section titled “VerifyPasswordlessEmail”](#verifypasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/passwordless.go [#](#VerifyPasswordlessEmail)asyncVerifyPasswordlessEmail VerifyPasswordlessEmail verifies a passwordless authentication paramctxcontext.Context Request context paramoptionsVerifyPasswordlessOptions Optional request settings. returnsVerifyPasswordLessResponse Verified claims or result. ```go verified, err := scalekitClient.Passwordless().VerifyPasswordlessEmail(ctx, &scalekit.VerifyPasswordlessOptions{ Code: "123456", AuthRequestId: "auth_req_123", }) if err != nil { // handle } _ = verified ``` ### ResendPasswordlessEmail [Section titled “ResendPasswordlessEmail”](#resendpasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/passwordless.go [#](#ResendPasswordlessEmail)asyncResendPasswordlessEmail ResendPasswordlessEmail resends a passwordless authentication email paramctxcontext.Context Request context paramauthRequestIdstring Auth request id. returnsSendPasswordlessResponse Send passwordless. ```go resp, err := scalekitClient.Passwordless().ResendPasswordlessEmail(ctx, "auth_req_123") if err != nil { // handle } _ = resp.AuthRequestId ``` --- # DOCUMENT BOUNDARY --- # Permissions > Manage fine-grained permissions Use the permission client to define fine-grained permissions that roles can grant. Model permissions for resources in your app, then attach them to roles. ### CreatePermission [Section titled “CreatePermission”](#createpermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#CreatePermission)asyncCreatePermission CreatePermission creates a new permission paramctxcontext.Context Request context parampermissionrolesv1.CreatePermission Permission creation payload. returnsCreatePermissionResponse The created resource. ### GetPermission [Section titled “GetPermission”](#getpermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#GetPermission)asyncGetPermission GetPermission retrieves a permission by name paramctxcontext.Context Request context parampermissionNamestring Permission name. returnsGetPermissionResponse The response payload for this operation. ### ListPermissions [Section titled “ListPermissions”](#listpermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#ListPermissions)asyncListPermissions ListPermissions lists all permissions with optional pagination. paramctxcontext.Context Request context parampageTokenstring Page token. parampageSizeuint32 Page size. returnsListPermissionsResponse Paginated results. ### UpdatePermission [Section titled “UpdatePermission”](#updatepermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#UpdatePermission)asyncUpdatePermission UpdatePermission updates an existing permission by name paramctxcontext.Context Request context parampermissionNamestring Permission name. parampermissionrolesv1.CreatePermission Permission update payload. returnsUpdatePermissionResponse The updated resource. ### DeletePermission [Section titled “DeletePermission”](#deletepermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#DeletePermission)asyncDeletePermission DeletePermission deletes a permission by name paramctxcontext.Context Request context parampermissionNamestring Permission name. returnserror error ### ListRolePermissions [Section titled “ListRolePermissions”](#listrolepermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#ListRolePermissions)asyncListRolePermissions ListRolePermissions lists all permissions associated with a role paramctxcontext.Context Request context paramroleNamestring Role name. returnsListRolePermissionsResponse Paginated results. ### AddPermissionsToRole [Section titled “AddPermissionsToRole”](#addpermissionstorole) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#AddPermissionsToRole)asyncAddPermissionsToRole AddPermissionsToRole adds permissions to a role paramctxcontext.Context Request context paramroleNamestring Role name. parampermissionNames\[]string Permission names. returnsAddPermissionsToRoleResponse The created resource. ### RemovePermissionFromRole [Section titled “RemovePermissionFromRole”](#removepermissionfromrole) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#RemovePermissionFromRole)asyncRemovePermissionFromRole RemovePermissionFromRole removes a permission from a role paramctxcontext.Context Request context paramroleNamestring Role name. parampermissionNamestring Permission name. returnserror error ### ListEffectiveRolePermissions [Section titled “ListEffectiveRolePermissions”](#listeffectiverolepermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/permission.go [#](#ListEffectiveRolePermissions)asyncListEffectiveRolePermissions ListEffectiveRolePermissions lists all effective permissions for a role (including inherited permissions) paramctxcontext.Context Request context paramroleNamestring Role name. returnsListEffectiveRolePermissionsResponse Paginated results. --- # DOCUMENT BOUNDARY --- # Resource > Manage resource clients and the consents your end users grant against them Use `scalekitClient.Resources()` to manage resource clients and to read and revoke the consents your end users grant against one. A consent records that one end user allowed a specific resource client to act on their behalf. The same audit and revoke actions are available in the dashboard under [Managing MCP clients](/authenticate/mcp/managing-mcp-clients/). ### GetResource [Section titled “GetResource”](#getresource) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#GetResource)asyncGetResource Retrieves a single resource by id. paramctxcontext.Context Request context paramresourceIdstring The resource to fetch (format: `res_...`). returns\*GetResourceResponse Resource object. ```go resource, err := scalekitClient.Resources().GetResource(ctx, "res_xxx") if err != nil { // handle error } fmt.Println(resource.Resource) ``` ### ListResources [Section titled “ListResources”](#listresources) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#ListResources)asyncListResources Lists resources of a given type in the environment, with pagination. paramctxcontext.Context Request context paramresourceTypescalekit.ResourceType The resource type to filter by. Supported value: `scalekit.ResourceTypeMcpServer`. paramoptionsscalekit.ListResourcesOptions Optional fields: `PageSize` (max 30), `PageToken`. PageSize, PageToken returns\*ListResourcesResponse Paginated resources. ```go resources, err := scalekitClient.Resources().ListResources(ctx, scalekit.ResourceTypeMcpServer, scalekit.ListResourcesOptions{ PageSize: 20, }) if err != nil { // handle error } for _, r := range resources.Resources { fmt.Println(r.Id, r.Scopes) } ``` ### CreateResourceClient [Section titled “CreateResourceClient”](#createresourceclient) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#CreateResourceClient)asyncCreateResourceClient Creates a resource client. Returns the created `Client` and a `PlainSecret` - the plaintext client secret, only available at creation time. paramctxcontext.Context Request context paramresourceIdstring The resource to create the client for (format: `res_...`). paramclient\*clients.ResourceClient The client properties. `Scopes` should be the same or a subset of the scopes available for the resource. `CustomClaims` is a flat key/value structure only. `Expiry` (access token lifetime in seconds) defaults to the resource’s configured expiry. `RedirectUris` are the allowed redirect URIs for a pre-registered client. There is no `Audience` field - audience is always server-determined. Name, Description, Scopes, CustomClaims, Expiry, RedirectUris returns\*CreateResourceClientResponse The created client and its plaintext secret. ```go import ( clients "github.com/scalekit-inc/scalekit-sdk-go/v2/pkg/grpc/scalekit/v1/clients" ) resource, err := scalekitClient.Resources().GetResource(ctx, "res_xxx") if err != nil { // handle error } var allowedScopes []string for _, s := range resource.Resource.Scopes { if s.Enabled { allowedScopes = append(allowedScopes, s.Name) } } created, err := scalekitClient.Resources().CreateResourceClient(ctx, "res_xxx", &clients.ResourceClient{ Name: "My Resource Client", Scopes: allowedScopes, }) if err != nil { // handle error } fmt.Println(created.Client.ClientId) // Store created.PlainSecret in your secret manager now - it is never returned again. // It grants full access as this client, so if it leaks, replace it right away: // create a new secret and delete the compromised one (delete first if you're // already at your secret limit; if it's your only secret, raise the limit // before rotating). ``` ### GetResourceClient [Section titled “GetResourceClient”](#getresourceclient) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#GetResourceClient)asyncGetResourceClient Fetches a single resource client, along with the end-users who have granted it consent. paramctxcontext.Context Request context paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID (format: `m2m_...`). returns\*GetResourceClientResponse The resource client. ```go got, err := scalekitClient.Resources().GetResourceClient(ctx, "res_xxx", "m2m_xxx") if err != nil { // handle error } fmt.Println(got.Client.Name) ``` ### ListResourceClients [Section titled “ListResourceClients”](#listresourceclients) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#ListResourceClients)asyncListResourceClients Lists resource clients. paramctxcontext.Context Request context paramresourceIdstring The resource whose clients to list (format: `res_...`). returns\*ListResourceClientsResponse The resource’s clients, plus `TotalDcrClients` and `TotalStaticClients` counts. ```go list, err := scalekitClient.Resources().ListResourceClients(ctx, "res_xxx") if err != nil { // handle error } fmt.Println(list.TotalDcrClients, list.TotalStaticClients) for _, c := range list.Clients { fmt.Println(c.ClientId, c.Name) } ``` ### UpdateResourceClient [Section titled “UpdateResourceClient”](#updateresourceclient) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#UpdateResourceClient)asyncUpdateResourceClient Updates a resource client. paramctxcontext.Context Request context paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID to update (format: `m2m_...`). paramoptionsscalekit.UpdateResourceClientOptions Pointer fields - only the ones set (non-nil) are changed. `Name`/`Description` are a no-op server-side when set to an empty string, not a clear. `Scopes`, `CustomClaims`, and `RedirectUris` replace their existing values; set an empty (non-nil) slice to clear one of them. There’s no `Audience` field. Name, Description, Scopes, CustomClaims, Expiry, RedirectUris returns\*UpdateResourceClientResponse The updated client. ```go resource, err := scalekitClient.Resources().GetResource(ctx, "res_xxx") if err != nil { // handle error } var allowedScopes []string for _, s := range resource.Resource.Scopes { if s.Enabled { allowedScopes = append(allowedScopes, s.Name) } } newName := "Updated Name" updated, err := scalekitClient.Resources().UpdateResourceClient(ctx, "res_xxx", "m2m_xxx", scalekit.UpdateResourceClientOptions{ Name: &newName, Scopes: &allowedScopes, }) if err != nil { // handle error } fmt.Println(updated.Client.Name, updated.Client.Scopes) ``` ### DeleteResourceClient [Section titled “DeleteResourceClient”](#deleteresourceclient) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#DeleteResourceClient)asyncDeleteResourceClient Deletes resource clients. Returns an error if the client is missing or scoped to a different resource. paramctxcontext.Context Request context paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID to delete (format: `m2m_...`). returnserror `nil` on success. ```go if err := scalekitClient.Resources().DeleteResourceClient(ctx, "res_xxx", "m2m_xxx"); err != nil { // handle error } ``` ### CreateResourceClientSecret [Section titled “CreateResourceClientSecret”](#createresourceclientsecret) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#CreateResourceClientSecret)asyncCreateResourceClientSecret Creates a new secret for a resource client. Only 2 client secrets are recommended to exist at a given point in time - use `DeleteResourceClientSecret` to remove an existing one first if you need more. The plaintext client secret is only ever returned here, at creation time. paramctxcontext.Context Request context paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID to create a secret for (format: `m2m_...`). returns\*CreateClientSecretResponse The new secret, including its plaintext value. ```go secret, err := scalekitClient.Resources().CreateResourceClientSecret(ctx, "res_xxx", "m2m_xxx") if err != nil { // handle error } // Store secret.PlainSecret in your secret manager now - it is never returned again. // It grants full access as this client, so if it leaks, replace it right away: // create a new secret and delete the compromised one (delete first if you're // already at your secret limit; if it's your only secret, raise the limit // before rotating). ``` ### DeleteResourceClientSecret [Section titled “DeleteResourceClientSecret”](#deleteresourceclientsecret) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#DeleteResourceClientSecret)asyncDeleteResourceClientSecret Permanently deletes a secret from a resource client. A client must always keep at least 1 secret - calling this on a client’s last remaining secret fails. paramctxcontext.Context Request context paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID the secret belongs to (format: `m2m_...`). paramsecretIdstring The secret ID to delete (format: `sks_...`). returnserror `nil` on success. ```go if err := scalekitClient.Resources().DeleteResourceClientSecret(ctx, "res_xxx", "m2m_xxx", "sks_xxx"); err != nil { // handle error } ``` ### ListUserConsents [Section titled “ListUserConsents”](#listuserconsents) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#ListUserConsents)asyncListUserConsents Lists the end-user consents granted against a resource, with pagination. Use this to audit who authorized a client, and to find the `ConsentId` you need before revoking. Filter by user in one of two ways. Pass `UserIds` to match external user IDs exactly and case-sensitively. Pass `Search` for a case-insensitive substring match. When you give both, `UserIds` wins and `Search` is ignored. paramctxcontext.Context Request context paramresourceIdstring The resource whose consents to list (format: `res_...`). paramoptionsscalekit.ListUserConsentsOptions Optional fields: `Search`, `PageSize` (max 30), `PageToken`, `UserIds` (max 25, takes precedence over `Search`). Search, PageSize, PageToken, UserIds returns\*ListResourceUserConsentsResponse Consents with `Id`, `ExternalUserId`, `ClientId`, `ClientName`, `Scopes`, and `GrantedAt`, plus `TotalSize` and the `NextPageToken` / `PrevPageToken` cursors. ```go consents, err := scalekitClient.Resources().ListUserConsents(ctx, "res_xxx", scalekit.ListUserConsentsOptions{ PageSize: 20, UserIds: []string{"user_456"}, // optional; takes precedence over Search }) if err != nil { // handle error } fmt.Println(consents.TotalSize, consents.NextPageToken) for _, c := range consents.Consents { fmt.Println(c.Id, c.ExternalUserId, c.ClientId, c.Scopes) } ``` ### RevokeUserConsent [Section titled “RevokeUserConsent”](#revokeuserconsent) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/resource.go [#](#RevokeUserConsent)asyncRevokeUserConsent Revokes a single end-user consent held by a resource client. The client is prompted for consent again on its next authorization attempt, and every active refresh token issued to that client for the same user is revoked. Access tokens that Scalekit already issued stay valid until they expire. See [How revocation affects active access tokens](/authenticate/mcp/managing-mcp-clients/#how-revocation-affects-active-access-tokens) for ways to shorten that window. paramctxcontext.Context Request context paramclientIdstring The resource client that holds the consent (format: `m2m_...`), not the resource ID. paramconsentIdstring The consent to revoke (format: `usrcnst_...`), taken from `ListUserConsents`. returns\*RevokeUserConsentResponse Empty response on success. The call returns an error on failure. ```go if _, err := scalekitClient.Resources().RevokeUserConsent(ctx, "m2m_xxx", "usrcnst_789"); err != nil { // handle error } ``` --- # DOCUMENT BOUNDARY --- # Roles > Define and assign organization roles Use the role client to define organization roles and list role assignments. Create roles and permissions for authorization after you have organizations and users. ### CreateRole [Section titled “CreateRole”](#createrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#CreateRole)asyncCreateRole CreateRole creates a new role in the environment paramctxcontext.Context Request context paramrolerolesv1.CreateRole Role creation payload. returnsCreateRoleResponse The created resource. ### GetRole [Section titled “GetRole”](#getrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#GetRole)asyncGetRole GetRole retrieves a role by name paramctxcontext.Context Request context paramroleNamestring Role name. returnsGetRoleResponse The response payload for this operation. ### ListRoles [Section titled “ListRoles”](#listroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#ListRoles)asyncListRoles ListRoles lists all roles in the environment paramctxcontext.Context Request context returnsListRolesResponse Paginated results. ### UpdateRole [Section titled “UpdateRole”](#updaterole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#UpdateRole)asyncUpdateRole UpdateRole updates an existing role by name paramctxcontext.Context Request context paramroleNamestring Role name. paramrolerolesv1.UpdateRole Role update payload. returnsUpdateRoleResponse The updated resource. ### DeleteRole [Section titled “DeleteRole”](#deleterole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#DeleteRole)asyncDeleteRole DeleteRole deletes a role by name paramctxcontext.Context Request context paramroleNamestring Role name. paramreassignRoleName...string Reassign role name. returnserror error ### GetRoleUsersCount [Section titled “GetRoleUsersCount”](#getroleuserscount) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#GetRoleUsersCount)asyncGetRoleUsersCount GetRoleUsersCount gets the count of users associated with a role paramctxcontext.Context Request context paramroleNamestring Role name. returnsGetRoleUsersCountResponse The response payload for this operation. ### CreateOrganizationRole [Section titled “CreateOrganizationRole”](#createorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#CreateOrganizationRole)asyncCreateOrganizationRole CreateOrganizationRole creates a new role in an organization paramctxcontext.Context Request context paramorgIdstring Org id. paramrolerolesv1.CreateOrganizationRole Role creation payload. returnsCreateOrganizationRoleResponse The created resource. ### GetOrganizationRole [Section titled “GetOrganizationRole”](#getorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#GetOrganizationRole)asyncGetOrganizationRole GetOrganizationRole retrieves an organization role by name paramctxcontext.Context Request context paramroleNamestring Role name. returnsGetOrganizationRoleResponse The response payload for this operation. ### ListOrganizationRoles [Section titled “ListOrganizationRoles”](#listorganizationroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#ListOrganizationRoles)asyncListOrganizationRoles ListOrganizationRoles lists all roles in an organization paramctxcontext.Context Request context paramorgIdstring Org id. returnsListOrganizationRolesResponse Paginated results. ### UpdateOrganizationRole [Section titled “UpdateOrganizationRole”](#updateorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#UpdateOrganizationRole)asyncUpdateOrganizationRole UpdateOrganizationRole updates an existing organization role by name paramctxcontext.Context Request context paramroleNamestring Role name. paramrolerolesv1.UpdateRole Role update payload. returnsUpdateOrganizationRoleResponse The updated resource. ### DeleteOrganizationRole [Section titled “DeleteOrganizationRole”](#deleteorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#DeleteOrganizationRole)asyncDeleteOrganizationRole DeleteOrganizationRole deletes an organization role by name paramctxcontext.Context Request context paramroleNamestring Role name. paramreassignRoleName...string Reassign role name. returnserror error ### GetOrganizationRoleUsersCount [Section titled “GetOrganizationRoleUsersCount”](#getorganizationroleuserscount) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#GetOrganizationRoleUsersCount)asyncGetOrganizationRoleUsersCount GetOrganizationRoleUsersCount gets the count of users associated with an organization role paramctxcontext.Context Request context paramroleNamestring Role name. returnsGetOrganizationRoleUsersCountResponse The response payload for this operation. ### UpdateDefaultOrganizationRoles [Section titled “UpdateDefaultOrganizationRoles”](#updatedefaultorganizationroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#UpdateDefaultOrganizationRoles)asyncUpdateDefaultOrganizationRoles UpdateDefaultOrganizationRoles updates the default roles for an organization paramctxcontext.Context Request context paramdefaultMemberRolestring Default member role. returnsUpdateDefaultOrganizationRolesResponse The updated resource. ### DeleteOrganizationRoleBase [Section titled “DeleteOrganizationRoleBase”](#deleteorganizationrolebase) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#DeleteOrganizationRoleBase)asyncDeleteOrganizationRoleBase DeleteOrganizationRoleBase deletes the base relationship for an organization role paramctxcontext.Context Request context paramroleNamestring Role name. returnserror error ### DeleteRoleBase [Section titled “DeleteRoleBase”](#deleterolebase) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#DeleteRoleBase)asyncDeleteRoleBase DeleteRoleBase deletes the base relationship for an environment-level role paramctxcontext.Context Request context paramroleNamestring Role name. returnserror error ### UpdateDefaultRoles [Section titled “UpdateDefaultRoles”](#updatedefaultroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#UpdateDefaultRoles)asyncUpdateDefaultRoles UpdateDefaultRoles updates the environment-level default creator and member roles paramctxcontext.Context Request context paramdefaultMemberRolestring Default member role. returnsUpdateDefaultRolesResponse The updated resource. ### ListDependentRoles [Section titled “ListDependentRoles”](#listdependentroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/role.go [#](#ListDependentRoles)asyncListDependentRoles ListDependentRoles lists all roles that depend on the specified role paramctxcontext.Context Request context paramroleNamestring Role name. returnsListDependentRolesResponse Paginated results. --- # DOCUMENT BOUNDARY --- # Create client > Create the SDK client and run core auth helpers Create a Scalekit client with your environment URL, client ID, and client secret. All other clients hang off this instance. Store credentials in environment variables. Never hard-code secrets. After you create the client, use [Sessions](/saaskit/sdks/go/sessions/) or domain clients such as [Organizations](/saaskit/sdks/go/organizations/) and [Users](/saaskit/sdks/go/users/). ### GetAuthorizationUrl [Section titled “GetAuthorizationUrl”](#getauthorizationurl) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#GetAuthorizationUrl)asyncGetAuthorizationUrl Runs `GetAuthorizationUrl` and returns the result. paramredirectUristring Redirect uri. paramoptionsAuthorizationUrlOptions Optional request settings. returnsurl.URL url.URL ```go authURL, err := scalekitClient.GetAuthorizationUrl( "https://yourapp.com/auth/callback", scalekit.AuthorizationUrlOptions{ State: "random-state-value", OrganizationId: "org_123", }, ) if err != nil { // handle } // Redirect user to authURL.String() ``` ### AuthenticateWithCode [Section titled “AuthenticateWithCode”](#authenticatewithcode) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#AuthenticateWithCode)asyncAuthenticateWithCode Runs `AuthenticateWithCode` and returns the result. paramctxcontext.Context Request context paramcodestring Authorization code. paramredirectUristring Redirect uri. paramoptionsAuthenticationOptions Optional request settings. returnsAuthenticationResponse Authentication. ```go resp, err := scalekitClient.AuthenticateWithCode( ctx, code, "https://yourapp.com/auth/callback", scalekit.AuthenticationOptions{}, ) if err != nil { // handle } accessToken := resp.AccessToken user := resp.User _ = accessToken _ = user ``` ### GetIdpInitiatedLoginClaims [Section titled “GetIdpInitiatedLoginClaims”](#getidpinitiatedloginclaims) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#GetIdpInitiatedLoginClaims)asyncGetIdpInitiatedLoginClaims Runs `GetIdpInitiatedLoginClaims` and returns the result. paramctxcontext.Context Request context paramidpInitiateLoginTokenstring Idp initiate login token. returnsIdpInitiatedLoginClaims The response payload for this operation. ```go claims, err := scalekitClient.GetIdpInitiatedLoginClaims(ctx, idpInitiatedLoginToken) if err != nil { // handle } // claims.ConnectionID, claims.OrganizationID, claims.LoginHint, claims.RelayState _ = claims ``` ### GetAccessTokenClaims [Section titled “GetAccessTokenClaims”](#getaccesstokenclaims) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#GetAccessTokenClaims)asyncGetAccessTokenClaims Runs `GetAccessTokenClaims` and returns the result. paramctxcontext.Context Request context paramaccessTokenstring Access token. returnsAccessTokenClaims The response payload for this operation. ```go claims, err := scalekitClient.GetAccessTokenClaims(ctx, accessToken) if err != nil { // handle } _ = claims ``` ### ValidateAccessToken [Section titled “ValidateAccessToken”](#validateaccesstoken) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#ValidateAccessToken)asyncValidateAccessToken Runs `ValidateAccessToken` and returns the result. paramctxcontext.Context Request context paramaccessTokenstring Access token. returnsbool bool ```go ok, err := scalekitClient.ValidateAccessToken(ctx, accessToken) if err != nil { // invalid } _ = ok ``` ### RefreshAccessToken [Section titled “RefreshAccessToken”](#refreshaccesstoken) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#RefreshAccessToken)asyncRefreshAccessToken Runs `RefreshAccessToken` and returns the result. paramctxcontext.Context Request context paramrefreshTokenstring Refresh token. returnsTokenResponse Token. ```go tokens, err := scalekitClient.RefreshAccessToken(ctx, refreshToken) if err != nil { // handle } _ = tokens.AccessToken ``` ### GetLogoutUrl [Section titled “GetLogoutUrl”](#getlogouturl) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#GetLogoutUrl)asyncGetLogoutUrl Runs `GetLogoutUrl` and returns the result. paramoptionsLogoutUrlOptions Optional request settings. returnsurl.URL url.URL ```go logoutURL, err := scalekitClient.GetLogoutUrl(scalekit.LogoutUrlOptions{ IdTokenHint: idToken, PostLogoutRedirectUri: "https://yourapp.com/", State: "state", }) if err != nil { // handle } _ = logoutURL ``` ### VerifyWebhookPayload [Section titled “VerifyWebhookPayload”](#verifywebhookpayload) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#VerifyWebhookPayload)asyncVerifyWebhookPayload Runs `VerifyWebhookPayload` and returns the result. paramsecretstring Secret. paramheadersmap\[string]string Headers. parampayload\[]byte Payload. returnsbool bool ```go valid, err := scalekitClient.VerifyWebhookPayload( "whsec_...", map[string]string{ "webhook-id": "webhook_123", "webhook-timestamp": "1730000000", "webhook-signature": "v1,base64sig", }, []byte(`{"type":"organization.created","data":{"id":"org_123"}}`), ) if err != nil { // handle } _ = valid ``` ### VerifyInterceptorPayload [Section titled “VerifyInterceptorPayload”](#verifyinterceptorpayload) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#VerifyInterceptorPayload)asyncVerifyInterceptorPayload Runs `VerifyInterceptorPayload` and returns the result. paramsecretstring Secret. paramheadersmap\[string]string Headers. parampayload\[]byte Payload. returnsbool bool ```go valid, err := scalekitClient.VerifyInterceptorPayload(secret, headers, payload) if err != nil { // handle } _ = valid ``` ### Connection [Section titled “Connection”](#connection) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Connection)asyncConnection Runs `Connection` and returns the result. returnsConnection Connection. ### Organization [Section titled “Organization”](#organization) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Organization)asyncOrganization Runs `Organization` and returns the result. returnsOrganization Organization. ### User [Section titled “User”](#user) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#User)asyncUser Runs `User` and returns the result. returnsUserService User service. ### Domain [Section titled “Domain”](#domain) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Domain)asyncDomain Runs `Domain` and returns the result. returnsDomain Domain. ### Directory [Section titled “Directory”](#directory) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Directory)asyncDirectory Runs `Directory` and returns the result. returnsDirectory Directory. ### Session [Section titled “Session”](#session) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Session)asyncSession Runs `Session` and returns the result. returnsSessionService Session service. ### Role [Section titled “Role”](#role) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Role)asyncRole Runs `Role` and returns the result. returnsRoleService Role service. ### Permission [Section titled “Permission”](#permission) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Permission)asyncPermission Runs `Permission` and returns the result. returnsPermissionService Permission service. ### Passwordless [Section titled “Passwordless”](#passwordless) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Passwordless)asyncPasswordless Runs `Passwordless` and returns the result. returnsPasswordlessService Passwordless service. ### WebAuthn [Section titled “WebAuthn”](#webauthn) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#WebAuthn)asyncWebAuthn Runs `WebAuthn` and returns the result. returnsWebAuthnService Web authn service. ### Auth [Section titled “Auth”](#auth) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Auth)asyncAuth Runs `Auth` and returns the result. returnsAuthService Auth service. ### Client [Section titled “Client”](#client) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Client)asyncClient Runs `Client` and returns the result. returnsClientService Client service. ### Token [Section titled “Token”](#token) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#Token)asyncToken Runs `Token` and returns the result. returnsTokenService Token service. ### ValidateTokenWithOptions [Section titled “ValidateTokenWithOptions”](#validatetokenwithoptions) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#ValidateTokenWithOptions)asyncValidateTokenWithOptions ValidateTokenWithOptions validates a signed JWT (access token or ID token) and enforces optional checks such as issuer, audience and scope validation. paramctxcontext.Context Request context paramtokenstring Token string. paramoptionsValidateTokenOptions Optional validation settings. Set `Issuer` or a list in `Issuers`; the token is valid if its `iss` claim equals any of them. A mismatch returns an error that wraps `ErrIssuerMismatch`. returnsbool bool ```go valid, err := scalekitClient.ValidateTokenWithOptions(ctx, accessToken, &scalekit.ValidateTokenOptions{ Audience: []string{"my-api"}, Scopes: []string{"read", "write"}, }) if err != nil { // handle } _ = valid // Accept more than one issuer, for example the environment URL and the server's own issuer valid, err = scalekitClient.ValidateTokenWithOptions(ctx, accessToken, &scalekit.ValidateTokenOptions{ Issuers: []string{envURL, envURL + "/resources/" + resourceID}, Audience: []string{expectedAudience}, }) if errors.Is(err, scalekit.ErrIssuerMismatch) { // The token's iss matched none of the accepted issuers } ``` ### ValidateToken [Section titled “ValidateToken”](#validatetoken) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#ValidateToken)asyncValidateToken Runs `ValidateToken` and returns the result. paramctxcontext.Context Request context paramtokenstring Token string. returnsClaims Verified claims or result. ```go claims, err := scalekitClient.ValidateToken(ctx, idToken) if err != nil { // handle } _ = claims ``` ### WithSecret [Section titled “WithSecret”](#withsecret) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#WithSecret)asyncWithSecret Runs `WithSecret` and returns the result. paramclientSecretstring Client secret. returnsScalekit Scalekit. ```go clientWithNewSecret := scalekitClient.WithSecret("new_client_secret") ``` ### GenerateClientToken [Section titled “GenerateClientToken”](#generateclienttoken) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#GenerateClientToken)asyncGenerateClientToken GenerateClientToken creates a client-credentials access token. paramctxcontext.Context Request context paramoptionsGenerateClientTokenOptions Optional request settings. returnsClientTokenResponse Client token. ```go resp, err := scalekitClient.GenerateClientToken(ctx, scalekit.GenerateClientTokenOptions{ ClientID: "service_client_id", ClientSecret: "service_client_secret", Scopes: []string{"read", "write"}, }) if err != nil { // handle } _ = resp.AccessToken ``` ### GetClientAccessToken [Section titled “GetClientAccessToken”](#getclientaccesstoken) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#GetClientAccessToken)asyncGetClientAccessToken Runs `GetClientAccessToken` and returns the result. paramctxcontext.Context Request context returnsstring string ```go accessToken, err := scalekitClient.GetClientAccessToken(ctx) if err != nil { // handle } _ = accessToken ``` ### UnmarshalJSON [Section titled “UnmarshalJSON”](#unmarshaljson) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#UnmarshalJSON)asyncUnmarshalJSON Runs `UnmarshalJSON` and returns the result. paramdata\[]byte Data. returnserror error ### M2M [Section titled “M2M”](#m2m) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#M2M)asyncM2M Runs `M2M` and returns the result. returnsM2MService M2mservice. ### VerifyPayloadSignature [Section titled “VerifyPayloadSignature”](#verifypayloadsignature) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/scalekit.go [#](#VerifyPayloadSignature)asyncVerifyPayloadSignature Runs `VerifyPayloadSignature` and returns the result. paramsecretstring Secret. paramheadersmap\[string]string Headers. parampayload\[]byte Payload. returnsbool bool --- # DOCUMENT BOUNDARY --- # Sessions > Validate tokens and manage user sessions Use the Session client to inspect session metadata and revoke sessions when users sign out or you detect risk. Typical flows: load a session by ID after authentication, list a user’s sessions for a security UI, and revoke one session or all of a user’s sessions on logout. ### GetSession [Section titled “GetSession”](#getsession) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/sessions.go [#](#GetSession)asyncGetSession GetSession retrieves session details by session ID paramctxcontext.Context Request context paramsessionIdstring Session ID. returnsSessionDetails The response payload for this operation. ```go session, err := scalekitClient.Session().GetSession(ctx, "ses_123") if err != nil { // handle } _ = session ``` ### GetUserSessions [Section titled “GetUserSessions”](#getusersessions) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/sessions.go [#](#GetUserSessions)asyncGetUserSessions GetUserSessions retrieves all session details for a user paramctxcontext.Context Request context paramuserIdstring Scalekit user ID (`usr_...`). parampageSizeuint32 Page size. parampageTokenstring Page token. paramfilterUserSessionFilter Result filter. returnsUserSessionDetails The response payload for this operation. ```go resp, err := scalekitClient.Session().GetUserSessions(ctx, "usr_123", 10, "", nil) if err != nil { // handle } _ = resp.Sessions ``` ### RevokeSession [Section titled “RevokeSession”](#revokesession) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/sessions.go [#](#RevokeSession)asyncRevokeSession RevokeSession revokes a session for a user paramctxcontext.Context Request context paramsessionIdstring Session ID. returnsRevokeSessionResponse Empty on success. ```go resp, err := scalekitClient.Session().RevokeSession(ctx, "ses_123") if err != nil { // handle } _ = resp ``` ### RevokeAllUserSessions [Section titled “RevokeAllUserSessions”](#revokeallusersessions) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/sessions.go [#](#RevokeAllUserSessions)asyncRevokeAllUserSessions RevokeAllUserSessions revokes all sessions for a user paramctxcontext.Context Request context paramuserIdstring Scalekit user ID (`usr_...`). returnsRevokeAllUserSessionsResponse Empty on success. ```go resp, err := scalekitClient.Session().RevokeAllUserSessions(ctx, "usr_123") if err != nil { // handle } _ = resp ``` --- # DOCUMENT BOUNDARY --- # Users > Provision and manage users and memberships Use the User client to provision people, manage organization memberships, and assign roles. Common flows: invite or create a user, attach them to an organization, update profile fields, and look up users by Scalekit ID or your external ID. ### ListUsers [Section titled “ListUsers”](#listusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#ListUsers)asyncListUsers ListUsers retrieves all users across the environment with optional pagination. paramctxcontext.Context Request context paramoptionsListUsersOptions Optional request settings. returnsListUsersResponse Paginated results. ```go users, err := scalekitClient.User().ListUsers(ctx, &scalekit.ListUsersOptions{ PageSize: 50, PageToken: "", }) if err != nil { // handle } for _, u := range users.Users { _ = u } ``` ### ListOrganizationUsers [Section titled “ListOrganizationUsers”](#listorganizationusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#ListOrganizationUsers)asyncListOrganizationUsers ListOrganizationUsers retrieves a list of users in the organization paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramoptionsListUsersOptions Optional request settings. returnsListOrganizationUsersResponse Paginated results. ```go users, err := scalekitClient.User().ListOrganizationUsers(ctx, "org_123", &scalekit.ListUsersOptions{ PageSize: 10, PageToken: "", }) if err != nil { // handle } _ = users.Users ``` ### GetUser [Section titled “GetUser”](#getuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#GetUser)asyncGetUser GetUser retrieves a user by ID paramctxcontext.Context Request context paramuserIdstring Scalekit user ID (`usr_...`). returnsGetUserResponse The response payload for this operation. ```go user, err := scalekitClient.User().GetUser(ctx, "usr_123") if err != nil { // handle } _ = user.User ``` ### UpdateUser [Section titled “UpdateUser”](#updateuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#UpdateUser)asyncUpdateUser UpdateUser updates an existing user paramctxcontext.Context Request context paramuserIdstring Scalekit user ID (`usr_...`). paramupdateUserusersv1.UpdateUser Update user. returnsUpdateUserResponse The updated resource. ```go firstName := "Test" lastName := "User" name := "Test User" locale := "en-US" updated, err := scalekitClient.User().UpdateUser(ctx, "usr_123", &users.UpdateUser{ UserProfile: &users.UpdateUserProfile{ FirstName: &firstName, LastName: &lastName, Name: &name, Locale: &locale, }, }) if err != nil { // handle } _ = updated.User ``` ### CreateUserAndMembership [Section titled “CreateUserAndMembership”](#createuserandmembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#CreateUserAndMembership)asyncCreateUserAndMembership CreateUserAndMembership creates a new user with membership in the organization paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramuserusersv1.CreateUser User fields. paramsendInvitationEmailbool Send invitation email. returnsCreateUserAndMembershipResponse The created resource. ```go created, err := scalekitClient.User().CreateUserAndMembership(ctx, "org_123", &users.CreateUser{ Email: "test.user@example.com", Metadata: map[string]string{ "source": "test", }, }, true) if err != nil { // handle } _ = created.User.Id ``` ### DeleteUser [Section titled “DeleteUser”](#deleteuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#DeleteUser)asyncDeleteUser DeleteUser deletes a user paramctxcontext.Context Request context paramuserIdstring Scalekit user ID (`usr_...`). returnserror error ```go if err := scalekitClient.User().DeleteUser(ctx, "usr_123"); err != nil { // handle } ``` ### CreateMembership [Section titled “CreateMembership”](#createmembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#CreateMembership)asyncCreateMembership CreateMembership creates a membership for a user in an organization paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramuserIdstring Scalekit user ID (`usr_...`). parammembershipusersv1.CreateMembership Membership. paramsendInvitationEmailbool Send invitation email. returnsCreateMembershipResponse The created resource. ```go resp, err := scalekitClient.User().CreateMembership(ctx, "org_123", "usr_123", &users.CreateMembership{ Roles: []*commons.Role{{Name: "admin"}}, Metadata: map[string]string{ "membership_type": "test", }, }, false) if err != nil { // handle } _ = resp.User ``` ### UpdateMembership [Section titled “UpdateMembership”](#updatemembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#UpdateMembership)asyncUpdateMembership UpdateMembership updates a user’s membership in an organization paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramuserIdstring Scalekit user ID (`usr_...`). parammembershipusersv1.UpdateMembership Membership. returnsUpdateMembershipResponse The updated resource. ```go resp, err := scalekitClient.User().UpdateMembership(ctx, "org_123", "usr_123", &users.UpdateMembership{ Roles: []*commons.Role{{Name: "member"}}, }) if err != nil { // handle } _ = resp.User ``` ### DeleteMembership [Section titled “DeleteMembership”](#deletemembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#DeleteMembership)asyncDeleteMembership DeleteMembership deletes a user’s membership from an organization. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramuserIdstring Scalekit user ID (`usr_...`). paramcascadebool Cascade. returnserror error ```go if err := scalekitClient.User().DeleteMembership(ctx, "org_123", "usr_123", false); err != nil { // handle } ``` ### ResendInvite [Section titled “ResendInvite”](#resendinvite) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#ResendInvite)asyncResendInvite ResendInvite resends an invitation email to a user who has a pending invitation paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramuserIdstring Scalekit user ID (`usr_...`). returnsusersv1.ResendInviteResponse usersv1.ResendInviteResponse ```go resp, err := scalekitClient.User().ResendInvite(ctx, "org_123", "usr_123") if err != nil { // handle } _ = resp.Invite ``` ### GetUserByExternalId [Section titled “GetUserByExternalId”](#getuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#GetUserByExternalId)asyncGetUserByExternalId GetUserByExternalId retrieves a user by their external ID paramctxcontext.Context Request context paramexternalIdstring External ID. returnsGetUserResponse The response payload for this operation. ### UpdateUserByExternalId [Section titled “UpdateUserByExternalId”](#updateuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#UpdateUserByExternalId)asyncUpdateUserByExternalId UpdateUserByExternalId updates an existing user identified by their external ID paramctxcontext.Context Request context paramexternalIdstring External ID. paramupdateUserusersv1.UpdateUser Update user. returnsUpdateUserResponse The updated resource. ### DeleteUserByExternalId [Section titled “DeleteUserByExternalId”](#deleteuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#DeleteUserByExternalId)asyncDeleteUserByExternalId DeleteUserByExternalId deletes a user identified by their external ID paramctxcontext.Context Request context paramexternalIdstring External ID. returnserror error ### CreateMembershipByExternalId [Section titled “CreateMembershipByExternalId”](#createmembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#CreateMembershipByExternalId)asyncCreateMembershipByExternalId CreateMembershipByExternalId creates a membership for a user (identified by external ID) in an organization paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramexternalIdstring External ID. parammembershipusersv1.CreateMembership Membership. paramsendInvitationEmailbool Send invitation email. returnsCreateMembershipResponse The created resource. ### UpdateMembershipByExternalId [Section titled “UpdateMembershipByExternalId”](#updatemembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#UpdateMembershipByExternalId)asyncUpdateMembershipByExternalId UpdateMembershipByExternalId updates a user’s membership (identified by external ID) in an organization paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramexternalIdstring External ID. parammembershipusersv1.UpdateMembership Membership. returnsUpdateMembershipResponse The updated resource. ### DeleteMembershipByExternalId [Section titled “DeleteMembershipByExternalId”](#deletemembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#DeleteMembershipByExternalId)asyncDeleteMembershipByExternalId DeleteMembershipByExternalId deletes a user’s membership (identified by external ID) from an organization. paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramexternalIdstring External ID. paramcascadebool Cascade. returnserror error ### ListUserRoles [Section titled “ListUserRoles”](#listuserroles) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#ListUserRoles)asyncListUserRoles ListUserRoles returns all roles assigned to the specified user within the given organization paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramuserIdstring Scalekit user ID (`usr_...`). returnsListUserRolesResponse Paginated results. ### ListUserPermissions [Section titled “ListUserPermissions”](#listuserpermissions) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/users.go [#](#ListUserPermissions)asyncListUserPermissions ListUserPermissions returns all permissions granted to the specified user within the given organization paramctxcontext.Context Request context paramorganizationIdstring Organization ID. paramuserIdstring Scalekit user ID (`usr_...`). returnsListUserPermissionsResponse Paginated results. --- # DOCUMENT BOUNDARY --- # Passkeys > Passkey and WebAuthn credentials Use the WebAuthn client to register and authenticate passkeys (WebAuthn credentials). Build registration and authentication ceremonies with the options and verification methods below. ### ListCredentials [Section titled “ListCredentials”](#listcredentials) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/webauthn.go [#](#ListCredentials)asyncListCredentials ListCredentials retrieves all registered passkeys for a user paramctxcontext.Context Request context paramuserIdstring Scalekit user ID (`usr_...`). returnsListCredentialsResponse Paginated results. ### UpdateCredential [Section titled “UpdateCredential”](#updatecredential) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/webauthn.go [#](#UpdateCredential)asyncUpdateCredential UpdateCredential updates the display name of a passkey credential paramctxcontext.Context Request context paramcredentialIdstring Credential id. paramdisplayNamestring Display name. returnsUpdateCredentialResponse The updated resource. ### DeleteCredential [Section titled “DeleteCredential”](#deletecredential) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-go/blob/main/webauthn.go [#](#DeleteCredential)asyncDeleteCredential DeleteCredential deletes a specific passkey credential paramctxcontext.Context Request context paramcredentialIdstring Credential id. returnsDeleteCredentialResponse Empty on success. --- # DOCUMENT BOUNDARY --- # Authorization > Authorization helpers for protected resources Use auth helpers for authorization code exchange, token validation, and related login token flows on the Scalekit client. These methods sit next to session management—use them when implementing hosted login callbacks and token checks. ### getAuthorizationUrl [Section titled “getAuthorizationUrl”](#getauthorizationurl) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/AuthClient.java [#](#getAuthorizationUrl)asyncgetAuthorizationUrl Runs `getAuthorizationUrl` and returns the result. paramredirectUriString Redirect uri. paramoptionsAuthorizationUrlOptions Optional request settings. returnsURL The response payload for this operation. ```java import com.scalekit.internal.http.AuthorizationUrlOptions; import java.net.URL; AuthorizationUrlOptions options = new AuthorizationUrlOptions(); options.setOrganizationId("org_123"); options.setState("random-state-value"); URL authUrl = scalekitClient.authentication().getAuthorizationUrl( "https://yourapp.com/auth/callback", options ); ``` ### validateAccessToken [Section titled “validateAccessToken”](#validateaccesstoken) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/AuthClient.java [#](#validateAccessToken)asyncvalidateAccessToken Runs `validateAccessToken` and returns the result. paramjwtString Jwt. paramoptionsTokenValidationOptions Optional validation settings for the `validateAccessToken(jwt, options)` overload. Set `issuer` or a list in `issuers`; the token is valid if its `iss` claim equals any of them. A failed check throws an `APIException`. returnsboolean boolean ```java boolean ok = scalekitClient.authentication().validateAccessToken(""); // Accept more than one issuer, for example the environment URL and the server's own issuer TokenValidationOptions options = TokenValidationOptions.builder() .issuers(Arrays.asList(envUrl, envUrl + "/resources/" + resourceId)) .audience(Collections.singletonList(expectedAudience)) .build(); boolean okForServer = scalekitClient.authentication().validateAccessToken("", options); ``` ### validateAccessTokenAndGetClaims [Section titled “validateAccessTokenAndGetClaims”](#validateaccesstokenandgetclaims) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/AuthClient.java [#](#validateAccessTokenAndGetClaims)asyncvalidateAccessTokenAndGetClaims Validates an access token and returns the decoded claims. Pass `TokenValidationOptions` to enforce the issuer and audience with the same rules as `validateAccessToken`. paramjwtString Jwt. paramoptionsTokenValidationOptions Optional validation settings. Accepts the same `issuer`, `issuers`, and `audience` as `validateAccessToken`. Pass `null` to check the signature and expiry only. returnsMap\ The decoded claims. Throws an `APIException` if the token is invalid. ```java Map claims = scalekitClient.authentication().validateAccessTokenAndGetClaims("", options); ``` ### generateClientToken [Section titled “generateClientToken”](#generateclienttoken) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/AuthClient.java [#](#generateClientToken)asyncgenerateClientToken Runs `generateClientToken` and returns the result. paramclientIdString Client ID. paramclientSecretString Client secret. returnsString String. ### getClientAccessToken [Section titled “getClientAccessToken”](#getclientaccesstoken) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/AuthClient.java [#](#getClientAccessToken)asyncgetClientAccessToken Runs `getClientAccessToken` and returns the result. returnsString The response payload for this operation. ```java String token = scalekitClient.authentication().getClientAccessToken(); ``` ### authenticateWithCode [Section titled “authenticateWithCode”](#authenticatewithcode) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/AuthClient.java [#](#authenticateWithCode)asyncauthenticateWithCode Runs `authenticateWithCode` and returns the result. paramcodeString Authorization code. paramredirectUriString Redirect uri. paramoptionsAuthenticationOptions Optional request settings. returnsAuthenticationResponse Authentication. ```java import com.scalekit.internal.http.AuthenticationOptions; import com.scalekit.internal.http.AuthenticationResponse; AuthenticationResponse result = scalekitClient.authentication().authenticateWithCode( "", "https://yourapp.com/auth/callback", new AuthenticationOptions() ); String accessToken = result.getAccessToken(); ``` ### updateLoginUserDetails [Section titled “updateLoginUserDetails”](#updateloginuserdetails) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/LoginClient.java [#](#updateLoginUserDetails)updateLoginUserDetails Updates the user details for an in-progress login request, for example after collecting additional profile information, or marking the login as failed with `User.newBuilder().setLoginFailed(true)`. paramconnectionIdString The connection ID the login request belongs to. paramloginRequestIdString The login request ID being updated. paramuserUser The user details to apply to the login request. returnsUpdateLoginUserDetailsResult Result carrying the auth request ID (`result.getAuthRequestId()`), which you can use to look up the user’s authentication journey in the auth logs. ```java import com.scalekit.grpc.scalekit.v1.auth.User; import com.scalekit.internal.http.UpdateLoginUserDetailsResult; User user = User.newBuilder() .setSub("unique_user_id_456") .setEmail("john.doe@company.com") .build(); UpdateLoginUserDetailsResult result = scalekitClient.login().updateLoginUserDetails( "conn_abc123", "login_xyz789", user ); // The auth request ID can be used to look up the authentication journey via auth logs. String authRequestId = result.getAuthRequestId(); ``` --- # DOCUMENT BOUNDARY --- # Connections > Configure enterprise SSO connections Use `scalekitClient.connections()` to configure enterprise SSO (SAML/OIDC) for an organization. Enable SSO after the organization exists: create a connection, complete IdP setup, and test sign-in for that org. ### getConnectionById [Section titled “getConnectionById”](#getconnectionbyid) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ConnectionClient.java [#](#getConnectionById)asyncgetConnectionById Runs `getConnectionById` and returns the result. paramconnectionIdString Connection ID. paramorganizationIdString Organization ID. returnsConnection The response payload for this operation. ```java scalekitClient.connections().getConnectionById("conn_123", "org_123"); ``` ### listConnectionsByDomain [Section titled “listConnectionsByDomain”](#listconnectionsbydomain) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ConnectionClient.java [#](#listConnectionsByDomain)asynclistConnectionsByDomain Runs `listConnectionsByDomain` and returns the result. paramdomainString Domain name. returnsListConnectionsResponse Paginated connections. ```java scalekitClient.connections().listConnectionsByDomain("acme.com"); ``` ### listConnectionsByOrganization [Section titled “listConnectionsByOrganization”](#listconnectionsbyorganization) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ConnectionClient.java [#](#listConnectionsByOrganization)asynclistConnectionsByOrganization Runs `listConnectionsByOrganization` and returns the result. paramorganizationIdString Organization ID. returnsListConnectionsResponse Paginated connections. ```java scalekitClient.connections().listConnectionsByOrganization("org_123"); ``` ### enableConnection [Section titled “enableConnection”](#enableconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ConnectionClient.java [#](#enableConnection)asyncenableConnection Runs `enableConnection` and returns the result. paramconnectionIdString Connection ID. paramorganizationIdString Organization ID. returnsToggleConnectionResponse The updated resource. ```java scalekitClient.connections().enableConnection("conn_123", "org_123"); ``` ### disableConnection [Section titled “disableConnection”](#disableconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ConnectionClient.java [#](#disableConnection)asyncdisableConnection Runs `disableConnection` and returns the result. paramconnectionIdString Connection ID. paramorganizationIdString Organization ID. returnsToggleConnectionResponse The updated resource. ```java scalekitClient.connections().disableConnection("conn_123", "org_123"); ``` ### createConnection [Section titled “createConnection”](#createconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ConnectionClient.java [#](#createConnection)asynccreateConnection Runs `createConnection` and returns the result. paramorganizationIdString Organization ID. paramconnectionCreateConnection Connection. returnsConnection The created resource. ```java import com.scalekit.grpc.scalekit.v1.connections.CreateConnection; CreateConnection req = CreateConnection.newBuilder() .setDisplayName("Acme Okta") .build(); scalekitClient.connections().createConnection("org_123", req); ``` ### deleteConnection [Section titled “deleteConnection”](#deleteconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ConnectionClient.java [#](#deleteConnection)asyncdeleteConnection Runs `deleteConnection` and returns the result. paramconnectionIdString Connection ID. paramorganizationIdString Organization ID. returnsvoid No return value ```java scalekitClient.connections().deleteConnection("conn_123", "org_123"); ``` --- # DOCUMENT BOUNDARY --- # Directory > Sync users and groups via SCIM directories Use `scalekitClient.directories()` for SCIM directory sync—listing providers, users, and groups provisioned from an IdP. Use this after SSO is in place when the customer provisions users via SCIM. ### getDirectory [Section titled “getDirectory”](#getdirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#getDirectory)asyncgetDirectory Runs `getDirectory` and returns the result. paramdirectoryIdString Directory ID. paramorganizationIdString Organization ID. returnsDirectory The response payload for this operation. ```java scalekitClient.directories().getDirectory("dir_123", "org_123"); ``` ### listDirectories [Section titled “listDirectories”](#listdirectories) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#listDirectories)asynclistDirectories Runs `listDirectories` and returns the result. paramorganizationIdString Organization ID. returnsListDirectoriesResponse Paginated results. ```java scalekitClient.directories().listDirectories("org_123"); ``` ### listDirectoryUsers [Section titled “listDirectoryUsers”](#listdirectoryusers) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#listDirectoryUsers)asynclistDirectoryUsers Runs `listDirectoryUsers` and returns the result. paramdirectoryIdString Directory ID. paramorganizationIdString Organization ID. paramoptionsListDirectoryResourceOptions Optional request settings. returnsListDirectoryUserResponse Paginated results. ```java import com.scalekit.api.util.ListDirectoryResourceOptions; import com.scalekit.api.util.ListDirectoryUserResponse; ListDirectoryResourceOptions options = ListDirectoryResourceOptions.builder() .pageSize(50) .includeDetail(true) .build(); ListDirectoryUserResponse res = scalekitClient.directories().listDirectoryUsers("dir_123", "org_123", options); ``` ### listDirectoryGroups [Section titled “listDirectoryGroups”](#listdirectorygroups) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#listDirectoryGroups)asynclistDirectoryGroups Runs `listDirectoryGroups` and returns the result. paramdirectoryIdString Directory ID. paramorganizationIdString Organization ID. paramoptionsListDirectoryResourceOptions Optional request settings. returnsListDirectoryGroupResponse Paginated results. ```java import com.scalekit.api.util.ListDirectoryGroupResponse; import com.scalekit.api.util.ListDirectoryResourceOptions; ListDirectoryResourceOptions options = ListDirectoryResourceOptions.builder() .pageSize(50) .includeDetail(true) .build(); ListDirectoryGroupResponse res = scalekitClient.directories().listDirectoryGroups("dir_123", "org_123", options); ``` ### enableDirectory [Section titled “enableDirectory”](#enabledirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#enableDirectory)asyncenableDirectory Runs `enableDirectory` and returns the result. paramdirectoryIdString Directory ID. paramorganizationIdString Organization ID. returnsToggleDirectoryResponse The updated resource. ```java scalekitClient.directories().enableDirectory("dir_123", "org_123"); ``` ### disableDirectory [Section titled “disableDirectory”](#disabledirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#disableDirectory)asyncdisableDirectory Runs `disableDirectory` and returns the result. paramdirectoryIdString Directory ID. paramorganizationIdString Organization ID. returnsToggleDirectoryResponse The updated resource. ```java scalekitClient.directories().disableDirectory("dir_123", "org_123"); ``` ### getPrimaryDirectoryByOrganizationId [Section titled “getPrimaryDirectoryByOrganizationId”](#getprimarydirectorybyorganizationid) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#getPrimaryDirectoryByOrganizationId)asyncgetPrimaryDirectoryByOrganizationId Runs `getPrimaryDirectoryByOrganizationId` and returns the result. paramorganizationIdString Organization ID. returnsDirectory The response payload for this operation. ```java scalekitClient.directories().getPrimaryDirectoryByOrganizationId("org_123"); ``` ### createDirectory [Section titled “createDirectory”](#createdirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#createDirectory)asynccreateDirectory Runs `createDirectory` and returns the result. paramorganizationIdString Organization ID. paramdirectoryCreateDirectory Directory. returnsDirectory The created resource. ```java import com.scalekit.grpc.scalekit.v1.directories.CreateDirectory; CreateDirectory req = CreateDirectory.newBuilder() .setDisplayName("Acme SCIM Directory") .build(); scalekitClient.directories().createDirectory("org_123", req); ``` ### deleteDirectory [Section titled “deleteDirectory”](#deletedirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DirectoryClient.java [#](#deleteDirectory)asyncdeleteDirectory Runs `deleteDirectory` and returns the result. paramdirectoryIdString Directory ID. paramorganizationIdString Organization ID. returnsvoid No return value ```java scalekitClient.directories().deleteDirectory("dir_123", "org_123"); ``` --- # DOCUMENT BOUNDARY --- # Domains > Manage organization email domains Use `scalekitClient.domains()` to manage verified email domains for an organization (SSO routing and domain-based policies). Add a domain, complete verification, then use it with SSO and organization policies. ### createDomain [Section titled “createDomain”](#createdomain) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DomainClient.java [#](#createDomain)asynccreateDomain createDomain creates a new domain using a request object paramrequestCreateDomainRequest Request payload. returnsDomain The created resource. ```java import com.scalekit.grpc.scalekit.v1.domains.CreateDomainRequest; CreateDomainRequest req = CreateDomainRequest.newBuilder() .setOrganizationId("org_123") .setDomain("acme.com") .build(); scalekitClient.domains().createDomain(req); ``` ### getDomainById [Section titled “getDomainById”](#getdomainbyid) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DomainClient.java [#](#getDomainById)asyncgetDomainById Runs `getDomainById` and returns the result. paramorganizationIdString Organization ID. paramdomainIdString Domain id. returnsDomain The response payload for this operation. ```java scalekitClient.domains().getDomainById("org_123", "dom_123"); ``` ### listDomainsByOrganizationId [Section titled “listDomainsByOrganizationId”](#listdomainsbyorganizationid) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DomainClient.java [#](#listDomainsByOrganizationId)asynclistDomainsByOrganizationId Runs `listDomainsByOrganizationId` and returns the result. paramorganizationIdString Organization ID. paramdomainTypeDomainType Domain type. returnsList(Domain) List(Domain) ```java import com.scalekit.grpc.scalekit.v1.domains.DomainType; scalekitClient.domains().listDomainsByOrganizationId("org_123", DomainType.DOMAIN_TYPE_PRIMARY); ``` ### deleteDomain [Section titled “deleteDomain”](#deletedomain) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/DomainClient.java [#](#deleteDomain)asyncdeleteDomain deleteDomain deletes a domain by its ID paramorganizationIdString Organization ID. paramdomainIdString Domain id. returnsvoid No return value ```java scalekitClient.domains().deleteDomain("org_123", "dom_123"); ``` --- # DOCUMENT BOUNDARY --- # M2M clients > Machine-to-machine clients and tokens Use M2M client APIs to create and manage machine-to-machine OAuth clients and secrets for service-to-service auth. Create an M2M client, store the secret securely, then obtain tokens with client credentials. ### createOrganizationClient [Section titled “createOrganizationClient”](#createorganizationclient) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/M2MClient.java [#](#createOrganizationClient)asynccreateOrganizationClient Interface for managing M2M (machine-to-machine) API clients per organization. paramorganizationIdString The organization ID to create the client for paramclientcom.scalekit.grpc.scalekit.v1.clients.Organizat… Client. returnsCreateOrganizationClientResponse The created resource. ```java import com.scalekit.grpc.scalekit.v1.clients.OrganizationClient; OrganizationClient orgClient = OrganizationClient.newBuilder() .setName("Production Service Account") .build(); scalekitClient.m2m().createOrganizationClient("org_123", orgClient); ``` ### getOrganizationClient [Section titled “getOrganizationClient”](#getorganizationclient) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/M2MClient.java [#](#getOrganizationClient)asyncgetOrganizationClient Retrieves details of a specific M2M scalekitClient. paramorganizationIdString The organization ID paramclientIdString The client ID (format: skc_xxxxx) returnsGetOrganizationClientResponse The response payload for this operation. ```java scalekitClient.m2m().getOrganizationClient("org_123", "skc_xxxxx"); ``` ### updateOrganizationClient [Section titled “updateOrganizationClient”](#updateorganizationclient) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/M2MClient.java [#](#updateOrganizationClient)asyncupdateOrganizationClient Updates the configuration of an existing M2M scalekitClient. paramorganizationIdString The organization ID paramclientIdString The client ID to update paramclientcom.scalekit.grpc.scalekit.v1.clients.Organizat… OrganizationClient proto with fields to update returnsUpdateOrganizationClientResponse The updated resource. ```java import com.scalekit.grpc.scalekit.v1.clients.OrganizationClient; OrganizationClient updates = OrganizationClient.newBuilder() .setName("Updated Service Account Name") .build(); scalekitClient.m2m().updateOrganizationClient("org_123", "skc_xxxxx", updates); ``` ### deleteOrganizationClient [Section titled “deleteOrganizationClient”](#deleteorganizationclient) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/M2MClient.java [#](#deleteOrganizationClient)asyncdeleteOrganizationClient Permanently deletes an M2M client from an organization. paramorganizationIdString The organization ID paramclientIdString The client ID to delete returnsvoid No return value ```java scalekitClient.m2m().deleteOrganizationClient("org_123", "skc_xxxxx"); ``` ### addOrganizationClientSecret [Section titled “addOrganizationClientSecret”](#addorganizationclientsecret) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/M2MClient.java [#](#addOrganizationClientSecret)asyncaddOrganizationClientSecret Adds a new secret to an M2M scalekitClient. paramorganizationIdString The organization ID paramclientIdString The client ID to add a secret to returnsCreateOrganizationClientSecretResponse The created resource. ### removeOrganizationClientSecret [Section titled “removeOrganizationClientSecret”](#removeorganizationclientsecret) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/M2MClient.java [#](#removeOrganizationClientSecret)asyncremoveOrganizationClientSecret Permanently removes a secret from an M2M scalekitClient. paramorganizationIdString The organization ID paramclientIdString The client ID paramsecretIdString The secret ID to remove returnsvoid No return value ### listOrganizationClients [Section titled “listOrganizationClients”](#listorganizationclients) clientM2Mhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/M2MClient.java [#](#listOrganizationClients)asynclistOrganizationClients Lists all M2M clients for an organization with pagination. paramorganizationIdString The organization ID parampageSizeint Page size (between 10 and 100; 0 uses server default) parampageTokenString Pagination cursor for next page (null or empty for first page). returnsListOrganizationClientsResponse Paginated results. ```java scalekitClient.m2m().listOrganizationClients("org_123", 20, ""); ``` --- # DOCUMENT BOUNDARY --- # Organizations > Manage your customer organizations Use `scalekitClient.organizations()` to create and manage customer tenants (organizations) in your Scalekit application. Start here when onboarding a new customer: create an organization, set external IDs and branding, then add users and SSO for that org. ### create [Section titled “create”](#create) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#create)asynccreate Runs `create` and returns the result. paramorganizationCreateOrganization Fields to update. returnsOrganization The created resource. ```java // displayName: Display name // metadata: Custom key-value metadata import com.scalekit.grpc.scalekit.v1.organizations.CreateOrganization; import com.scalekit.grpc.scalekit.v1.organizations.Organization; CreateOrganization req = CreateOrganization.newBuilder() .setName("Acme Corporation") .build(); Organization org = scalekitClient.organizations().create(req); ``` ### getById [Section titled “getById”](#getbyid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#getById)asyncgetById Runs `getById` and returns the result. paramidString Resource ID. returnsOrganization The response payload for this operation. ```java scalekitClient.organizations().getById("org_123"); ``` ### getByExternalId [Section titled “getByExternalId”](#getbyexternalid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#getByExternalId)asyncgetByExternalId Runs `getByExternalId` and returns the result. paramexternalIdString External ID. returnsOrganization The response payload for this operation. ```java scalekitClient.organizations().getByExternalId("customer_123"); ``` ### updateById [Section titled “updateById”](#updatebyid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#updateById)asyncupdateById Runs `updateById` and returns the result. paramidString Resource ID. paramorganizationUpdateOrganization Fields to update. returnsOrganization The updated resource. ```java // displayName: Display name // metadata: Custom key-value metadata import com.scalekit.grpc.scalekit.v1.organizations.UpdateOrganization; UpdateOrganization req = UpdateOrganization.newBuilder() .setName("Acme Corp (Updated)") .build(); scalekitClient.organizations().updateById("org_123", req); ``` ### updateByExternalId [Section titled “updateByExternalId”](#updatebyexternalid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#updateByExternalId)asyncupdateByExternalId Runs `updateByExternalId` and returns the result. paramexternalIdString External ID. paramorganizationUpdateOrganization Fields to update. returnsOrganization The updated resource. ```java // displayName: Display name // metadata: Custom key-value metadata import com.scalekit.grpc.scalekit.v1.organizations.UpdateOrganization; UpdateOrganization req = UpdateOrganization.newBuilder() .setName("Acme Corp (Updated)") .build(); scalekitClient.organizations().updateByExternalId("customer_123", req); ``` ### deleteById [Section titled “deleteById”](#deletebyid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#deleteById)asyncdeleteById Runs `deleteById` and returns the result. paramidString Resource ID. returnsvoid No return value ```java scalekitClient.organizations().deleteById("org_123"); ``` ### deleteByExternalId [Section titled “deleteByExternalId”](#deletebyexternalid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#deleteByExternalId)asyncdeleteByExternalId Runs `deleteByExternalId` and returns the result. paramexternalIdString External ID. returnsvoid No return value ```java scalekitClient.organizations().deleteByExternalId("customer_123"); ``` ### listOrganizations [Section titled “listOrganizations”](#listorganizations) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#listOrganizations)asynclistOrganizations Runs `listOrganizations` and returns the result. parampageSizeint Page size. parampageTokenString Page token. returnsListOrganizationsResponse Paginated organizations. ```java scalekitClient.organizations().listOrganizations(20, ""); ``` ### generatePortalLink [Section titled “generatePortalLink”](#generateportallink) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#generatePortalLink)asyncgeneratePortalLink Runs `generatePortalLink` and returns the result. paramorganizationIdString Organization ID. paramfeaturesList(Feature) Features. returnsLink Authorization link. ```java import java.util.Arrays; import com.scalekit.grpc.scalekit.v1.organizations.Feature; scalekitClient.organizations().generatePortalLink( "org_123", Arrays.asList(Feature.FEATURE_USERS, Feature.FEATURE_CONNECTIONS) ); ``` ### updateOrganizationSettings [Section titled “updateOrganizationSettings”](#updateorganizationsettings) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#updateOrganizationSettings)asyncupdateOrganizationSettings Runs `updateOrganizationSettings` and returns the result. paramorganizationIdString Organization ID. paramsettingsList(OrganizationSettingsFeature) Settings. returnsOrganization The updated resource. ```java import java.util.Collections; import com.scalekit.grpc.scalekit.v1.organizations.OrganizationSettingsFeature; scalekitClient.organizations().updateOrganizationSettings( "org_123", Collections.emptyList() ); ``` ### upsertUserManagementSettings [Section titled “upsertUserManagementSettings”](#upsertusermanagementsettings) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#upsertUserManagementSettings)asyncupsertUserManagementSettings Runs `upsertUserManagementSettings` and returns the result. paramorganizationIdString Organization ID. paramsettingsOrganizationUserManagementSettings Settings. returnsOrganizationUserManagementSettings Organization user management settings. ```java import com.scalekit.grpc.scalekit.v1.organizations.OrganizationUserManagementSettings; OrganizationUserManagementSettings settings = OrganizationUserManagementSettings.newBuilder().build(); scalekitClient.organizations().upsertUserManagementSettings("org_123", settings); ``` ### getOrganizationSessionPolicy [Section titled “getOrganizationSessionPolicy”](#getorganizationsessionpolicy) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#getOrganizationSessionPolicy)asyncgetOrganizationSessionPolicy Runs `getOrganizationSessionPolicy` and returns the result. paramorganizationIdString Organization ID. returnsOrganizationSessionPolicySettings The response payload for this operation. ### updateOrganizationSessionPolicy [Section titled “updateOrganizationSessionPolicy”](#updateorganizationsessionpolicy) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/OrganizationClient.java [#](#updateOrganizationSessionPolicy)asyncupdateOrganizationSessionPolicy Runs `updateOrganizationSessionPolicy` and returns the result. paramorganizationIdString Organization ID. parampolicyOrganizationSessionPolicySettings Policy. returnsOrganizationSessionPolicySettings The updated resource. --- # DOCUMENT BOUNDARY --- # Magic link & OTP > Send and verify passwordless login codes Use `scalekitClient.passwordless()` to send and verify email magic links or one-time codes for sign-in. Call send, then verify with the code or link token from the user’s email to complete login. ### sendPasswordlessEmail [Section titled “sendPasswordlessEmail”](#sendpasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PasswordlessClient.java [#](#sendPasswordlessEmail)asyncsendPasswordlessEmail Send a passwordless authentication email with default options paramemailString The email address to send the passwordless link to returnsSendPasswordlessResponse Send passwordless. ```java scalekitClient.passwordless().sendPasswordlessEmail("user@acme.com"); ``` ### verifyPasswordlessEmail [Section titled “verifyPasswordlessEmail”](#verifypasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PasswordlessClient.java [#](#verifyPasswordlessEmail)asyncverifyPasswordlessEmail Verify a passwordless authentication code or link token without auth request ID paramcredentialVerifyPasswordlessOptions The credential to verify (code or linkToken) returnsVerifyPasswordLessResponse Verified claims or result. ```java import com.scalekit.internal.http.VerifyPasswordlessOptions; VerifyPasswordlessOptions credential = new VerifyPasswordlessOptions(); credential.setLinkToken(""); scalekitClient.passwordless().verifyPasswordlessEmail(credential); ``` ### resendPasswordlessEmail [Section titled “resendPasswordlessEmail”](#resendpasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PasswordlessClient.java [#](#resendPasswordlessEmail)asyncresendPasswordlessEmail Resend a passwordless authentication email paramauthRequestIdString The auth request ID from the original send response returnsSendPasswordlessResponse Send passwordless. ```java scalekitClient.passwordless().resendPasswordlessEmail("authreq_123"); ``` --- # DOCUMENT BOUNDARY --- # Permissions > Manage fine-grained permissions Use `scalekitClient.permissions()` to define fine-grained permissions that roles can grant. Model permissions for resources in your app, then attach them to roles. ### createPermission [Section titled “createPermission”](#createpermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#createPermission)asynccreatePermission Runs `createPermission` and returns the result. paramrequestCreatePermissionRequest Request payload. returnsCreatePermissionResponse Permission management CreatePermissionResponse ```java import com.scalekit.grpc.scalekit.v1.roles.CreatePermissionRequest; CreatePermissionRequest req = CreatePermissionRequest.newBuilder().build(); scalekitClient.permissions().createPermission(req); ``` ### getPermission [Section titled “getPermission”](#getpermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#getPermission)asyncgetPermission Runs `getPermission` and returns the result. parampermissionNameString Permission name. returnsGetPermissionResponse The response payload for this operation. ```java scalekitClient.permissions().getPermission("read:documents"); ``` ### listPermissions [Section titled “listPermissions”](#listpermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#listPermissions)asynclistPermissions Runs `listPermissions` and returns the result. parampageTokenString Page token. returnsListPermissionsResponse Deprecated ListPermissionsResponse ```java scalekitClient.permissions().listPermissions("next_page_token"); ``` ### updatePermission [Section titled “updatePermission”](#updatepermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#updatePermission)asyncupdatePermission Runs `updatePermission` and returns the result. parampermissionNameString Permission name. paramrequestUpdatePermissionRequest Request payload. returnsUpdatePermissionResponse The updated resource. ```java import com.scalekit.grpc.scalekit.v1.roles.UpdatePermissionRequest; UpdatePermissionRequest req = UpdatePermissionRequest.newBuilder().build(); scalekitClient.permissions().updatePermission("read:documents", req); ``` ### deletePermission [Section titled “deletePermission”](#deletepermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#deletePermission)asyncdeletePermission Runs `deletePermission` and returns the result. parampermissionNameString Permission name. returnsvoid No return value ```java scalekitClient.permissions().deletePermission("read:documents"); ``` ### listRolePermissions [Section titled “listRolePermissions”](#listrolepermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#listRolePermissions)asynclistRolePermissions Runs `listRolePermissions` and returns the result. paramroleNameString Role name. returnsListRolePermissionsResponse Permission relationships ListRolePermissionsResponse ```java scalekitClient.permissions().listRolePermissions("admin"); ``` ### addPermissionsToRole [Section titled “addPermissionsToRole”](#addpermissionstorole) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#addPermissionsToRole)asyncaddPermissionsToRole Runs `addPermissionsToRole` and returns the result. paramroleNameString Role name. paramrequestAddPermissionsToRoleRequest Request payload. returnsAddPermissionsToRoleResponse The created resource. ```java import com.scalekit.grpc.scalekit.v1.roles.AddPermissionsToRoleRequest; AddPermissionsToRoleRequest req = AddPermissionsToRoleRequest.newBuilder().build(); scalekitClient.permissions().addPermissionsToRole("admin", req); ``` ### removePermissionFromRole [Section titled “removePermissionFromRole”](#removepermissionfromrole) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#removePermissionFromRole)asyncremovePermissionFromRole Runs `removePermissionFromRole` and returns the result. paramroleNameString Role name. parampermissionNameString Permission name. returnsvoid No return value ```java scalekitClient.permissions().removePermissionFromRole("admin", "read:documents"); ``` ### listEffectiveRolePermissions [Section titled “listEffectiveRolePermissions”](#listeffectiverolepermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/PermissionClient.java [#](#listEffectiveRolePermissions)asynclistEffectiveRolePermissions Runs `listEffectiveRolePermissions` and returns the result. paramroleNameString Role name. returnsListEffectiveRolePermissionsResponse Paginated results. ```java scalekitClient.permissions().listEffectiveRolePermissions("admin"); ``` --- # DOCUMENT BOUNDARY --- # Resource > Manage resource clients and the consents your end users grant against them Use `scalekitClient.resources()` to manage resource clients and to read and revoke the consents your end users grant against one. A consent records that one end user allowed a specific resource client to act on their behalf. The same audit and revoke actions are available in the dashboard under [Managing MCP clients](/authenticate/mcp/managing-mcp-clients/). ### getResource [Section titled “getResource”](#getresource) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#getResource)asyncgetResource Retrieves a single resource by id. paramresourceIdString The resource to fetch (format: `res_...`). returnsGetResourceResponse Resource object. ```java GetResourceResponse resource = scalekitClient.resources().getResource("res_xxx"); System.out.println(resource.getResource()); ``` ### listResources [Section titled “listResources”](#listresources) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#listResources)asynclistResources Lists resources of a given type in the environment, with pagination. paramresourceTypeResourceType The resource type to filter by. Supported value: `ResourceType.MCP_SERVER`. parampageSizeint Page size for pagination (max 30). `0` uses the server default. parampageTokenString Page token for pagination; empty for the first page. returnsListResourcesResponse Paginated resources. ```java ListResourcesResponse resources = scalekitClient.resources().listResources(ResourceType.MCP_SERVER, 20, ""); resources.getResourcesList().forEach(r -> System.out.println(r.getId() + " " + r.getScopesList())); ``` ### createResourceClient [Section titled “createResourceClient”](#createresourceclient) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#createResourceClient)asynccreateResourceClient Creates a resource client. Returns the created `client` and a `plainSecret` - the plaintext client secret, only available at creation time. paramresourceIdString The resource to create the client for (format: `res_...`). paramclientResourceClient The client properties, built via `ResourceClient.newBuilder()`. `scopes` should be the same or a subset of the scopes available for the resource. `customClaims` is a flat key/value structure only. `expiry` (access token lifetime in seconds) defaults to the resource’s configured expiry. `redirectUris` are the allowed redirect URIs for a pre-registered client. There is no `audience` field - a non-empty `audience` list throws `IllegalArgumentException`, since audience is always server-determined. name, description, scopes, customClaims, expiry, redirectUris returnsCreateResourceClientResponse The created client and its plaintext secret. ```java import com.scalekit.grpc.scalekit.v1.clients.CreateResourceClientResponse; import com.scalekit.grpc.scalekit.v1.clients.GetResourceResponse; import com.scalekit.grpc.scalekit.v1.clients.ResourceClient; import com.scalekit.grpc.scalekit.v1.clients.Scope; import java.util.List; import java.util.stream.Collectors; GetResourceResponse resource = scalekitClient.resources().getResource("res_xxx"); List allowedScopes = resource.getResource().getScopesList().stream() .filter(Scope::getEnabled) .map(Scope::getName) .collect(Collectors.toList()); CreateResourceClientResponse created = scalekitClient.resources().createResourceClient( "res_xxx", ResourceClient.newBuilder().setName("My Resource Client").addAllScopes(allowedScopes).build() ); System.out.println(created.getClient().getClientId()); // Store created.getPlainSecret() in your secret manager now - it is never // returned again. It grants full access as this client, so if it leaks, // replace it right away: create a new secret and delete the compromised one // (delete first if you're already at your secret limit; if it's your only // secret, raise the limit before rotating). ``` ### getResourceClient [Section titled “getResourceClient”](#getresourceclient) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#getResourceClient)asyncgetResourceClient Fetches a single resource client, along with the end-users who have granted it consent. paramresourceIdString The resource the client must belong to (format: `res_...`). paramclientIdString The client ID (format: `m2m_...`). returnsGetResourceClientResponse The resource client. ```java GetResourceClientResponse got = scalekitClient.resources().getResourceClient("res_xxx", "m2m_xxx"); System.out.println(got.getClient().getName()); ``` ### listResourceClients [Section titled “listResourceClients”](#listresourceclients) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#listResourceClients)asynclistResourceClients Lists resource clients. paramresourceIdString The resource whose clients to list (format: `res_...`). returnsListResourceClientsResponse The resource’s clients, plus `totalDcrClients` and `totalStaticClients` counts. ```java ListResourceClientsResponse list = scalekitClient.resources().listResourceClients("res_xxx"); System.out.println(list.getTotalDcrClients() + " " + list.getTotalStaticClients()); list.getClientsList().forEach(c -> System.out.println(c.getClientId() + " " + c.getName())); ``` ### updateResourceClient [Section titled “updateResourceClient”](#updateresourceclient) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#updateResourceClient)asyncupdateResourceClient Updates a resource client. paramresourceIdString The resource the client must belong to (format: `res_...`). paramclientIdString The client ID to update (format: `m2m_...`). paramoptionsUpdateResourceClientOptions Built via `UpdateResourceClientOptions.builder()`. Only the fields set (non-null) are changed. `name`/`description` are a no-op server-side when set to an empty string, not a clear. `scopes`, `customClaims`, and `redirectUris` replace their existing values; set an empty list to clear one of them. There’s no `audience` field. name, description, scopes, customClaims, expiry, redirectUris returnsUpdateResourceClientResponse The updated client. ```java GetResourceResponse resource = scalekitClient.resources().getResource("res_xxx"); List allowedScopes = resource.getResource().getScopesList().stream() .filter(Scope::getEnabled) .map(Scope::getName) .collect(Collectors.toList()); UpdateResourceClientResponse updated = scalekitClient.resources().updateResourceClient( "res_xxx", "m2m_xxx", UpdateResourceClientOptions.builder().name("Updated Name").scopes(allowedScopes).build() ); System.out.println(updated.getClient().getName() + " " + updated.getClient().getScopesList()); ``` ### deleteResourceClient [Section titled “deleteResourceClient”](#deleteresourceclient) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#deleteResourceClient)asyncdeleteResourceClient Deletes resource clients. Throws if the client is missing or scoped to a different resource. paramresourceIdString The resource the client must belong to (format: `res_...`). paramclientIdString The client ID to delete (format: `m2m_...`). returnsDeleteResourceClientResponse Empty response on success. ```java scalekitClient.resources().deleteResourceClient("res_xxx", "m2m_xxx"); ``` ### createResourceClientSecret [Section titled “createResourceClientSecret”](#createresourceclientsecret) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#createResourceClientSecret)asynccreateResourceClientSecret Creates a new secret for a resource client. Only 2 client secrets are recommended to exist at a given point in time - use `deleteResourceClientSecret` to remove an existing one first if you need more. The plaintext client secret is only ever returned here, at creation time. paramresourceIdString The resource the client must belong to (format: `res_...`). paramclientIdString The client ID to create a secret for (format: `m2m_...`). returnsCreateClientSecretResponse The new secret, including its plaintext value. ```java CreateClientSecretResponse secret = scalekitClient.resources().createResourceClientSecret("res_xxx", "m2m_xxx"); // Store secret.getPlainSecret() in your secret manager now - it is never // returned again. It grants full access as this client, so if it leaks, // replace it right away: create a new secret and delete the compromised one // (delete first if you're already at your secret limit; if it's your only // secret, raise the limit before rotating). ``` ### deleteResourceClientSecret [Section titled “deleteResourceClientSecret”](#deleteresourceclientsecret) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#deleteResourceClientSecret)asyncdeleteResourceClientSecret Permanently deletes a secret from a resource client. A client must always keep at least 1 secret - calling this on a client’s last remaining secret throws an error. paramresourceIdString The resource the client must belong to (format: `res_...`). paramclientIdString The client ID the secret belongs to (format: `m2m_...`). paramsecretIdString The secret ID to delete (format: `sks_...`). returnsvoid Nothing on success. ```java scalekitClient.resources().deleteResourceClientSecret("res_xxx", "m2m_xxx", "sks_xxx"); ``` ### listUserConsents [Section titled “listUserConsents”](#listuserconsents) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#listUserConsents)asynclistUserConsents Lists the end-user consents granted against a resource, with pagination. Use this to audit who authorized a client, and to find the `consentId` you need before revoking. Filter by user in one of two ways. Pass `userIds` to match external user IDs exactly and case-sensitively. Pass `search` for a case-insensitive substring match. When you give both, `userIds` wins and `search` is ignored. paramresourceIdString The resource whose consents to list (format: `res_...`). paramoptionsListUserConsentsOptions Built via `ListUserConsentsOptions.builder()`. Optional fields: `search`, `pageSize` (max 30), `pageToken`, `userIds` (max 25, takes precedence over `search`). search, pageSize, pageToken, userIds returnsListResourceUserConsentsResponse Consents with `id`, `externalUserId`, `clientId`, `clientName`, `scopes`, and `grantedAt`, plus `totalSize` and the `nextPageToken` / `prevPageToken` cursors. ```java ListResourceUserConsentsResponse consents = scalekitClient.resources().listUserConsents( "res_xxx", ListUserConsentsOptions.builder().pageSize(20).userIds(List.of("user_456")).build() // userIds optional; takes precedence over search ); System.out.println(consents.getTotalSize() + " " + consents.getNextPageToken()); consents.getConsentsList().forEach(c -> System.out.println(c.getId() + " " + c.getExternalUserId() + " " + c.getClientId() + " " + c.getScopesList()) ); ``` ### revokeUserConsent [Section titled “revokeUserConsent”](#revokeuserconsent) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/ResourceConsentClient.java [#](#revokeUserConsent)asyncrevokeUserConsent Revokes a single end-user consent held by a resource client. The client is prompted for consent again on its next authorization attempt, and every active refresh token issued to that client for the same user is revoked. Access tokens that Scalekit already issued stay valid until they expire. See [How revocation affects active access tokens](/authenticate/mcp/managing-mcp-clients/#how-revocation-affects-active-access-tokens) for ways to shorten that window. paramclientIdString The resource client that holds the consent (format: `m2m_...`), not the resource ID. paramconsentIdString The consent to revoke (format: `usrcnst_...`), taken from `listUserConsents`. returnsRevokeUserConsentResponse Empty response on success. The call throws on failure. ```java scalekitClient.resources().revokeUserConsent("m2m_xxx", "usrcnst_789"); ``` --- # DOCUMENT BOUNDARY --- # Roles > Define and assign organization roles Use `scalekitClient.roles()` to define organization roles and list role assignments. Create roles and permissions for authorization after you have organizations and users. ### createRole [Section titled “createRole”](#createrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#createRole)asynccreateRole Runs `createRole` and returns the result. paramrequestCreateRoleRequest Request payload. returnslevel role management CreateRoleResponse level role management CreateRoleResponse ```java import com.scalekit.grpc.scalekit.v1.roles.CreateRoleRequest; CreateRoleRequest req = CreateRoleRequest.newBuilder().build(); scalekitClient.roles().createRole(req); ``` ### getRole [Section titled “getRole”](#getrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#getRole)asyncgetRole Runs `getRole` and returns the result. paramroleNameString Role name. returnsGetRoleResponse The response payload for this operation. ```java scalekitClient.roles().getRole("admin"); ``` ### listRoles [Section titled “listRoles”](#listroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#listRoles)asynclistRoles Runs `listRoles` and returns the result. returnsListRolesResponse Paginated results. ```java scalekitClient.roles().listRoles(); ``` ### updateRole [Section titled “updateRole”](#updaterole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#updateRole)asyncupdateRole Runs `updateRole` and returns the result. paramroleNameString Role name. paramrequestUpdateRoleRequest Request payload. returnsUpdateRoleResponse The updated resource. ```java import com.scalekit.grpc.scalekit.v1.roles.UpdateRoleRequest; UpdateRoleRequest req = UpdateRoleRequest.newBuilder().build(); scalekitClient.roles().updateRole("admin", req); ``` ### deleteRole [Section titled “deleteRole”](#deleterole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#deleteRole)asyncdeleteRole Runs `deleteRole` and returns the result. paramroleNameString Role name. paramreassignRoleNameString Reassign role name. returnsvoid No return value ```java scalekitClient.roles().deleteRole("old_role", "new_role"); ``` ### getRoleUsersCount [Section titled “getRoleUsersCount”](#getroleuserscount) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#getRoleUsersCount)asyncgetRoleUsersCount Runs `getRoleUsersCount` and returns the result. paramroleNameString Role name. returnsGetRoleUsersCountResponse The response payload for this operation. ```java scalekitClient.roles().getRoleUsersCount("admin"); ``` ### createOrganizationRole [Section titled “createOrganizationRole”](#createorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#createOrganizationRole)asynccreateOrganizationRole Runs `createOrganizationRole` and returns the result. paramorgIdString Org id. paramrequestCreateOrganizationRoleRequest Request payload. returnslevel role management CreateOrganizationRoleResponse level role management CreateOrganizationRoleResponse ```java import com.scalekit.grpc.scalekit.v1.roles.CreateOrganizationRoleRequest; CreateOrganizationRoleRequest req = CreateOrganizationRoleRequest.newBuilder().build(); scalekitClient.roles().createOrganizationRole("org_123", req); ``` ### getOrganizationRole [Section titled “getOrganizationRole”](#getorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#getOrganizationRole)asyncgetOrganizationRole Runs `getOrganizationRole` and returns the result. paramorgIdString Org id. paramroleNameString Role name. returnsGetOrganizationRoleResponse The response payload for this operation. ```java scalekitClient.roles().getOrganizationRole("org_123", "org_admin"); ``` ### listOrganizationRoles [Section titled “listOrganizationRoles”](#listorganizationroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#listOrganizationRoles)asynclistOrganizationRoles Runs `listOrganizationRoles` and returns the result. paramorgIdString Org id. returnsListOrganizationRolesResponse Paginated results. ```java scalekitClient.roles().listOrganizationRoles("org_123"); ``` ### updateOrganizationRole [Section titled “updateOrganizationRole”](#updateorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#updateOrganizationRole)asyncupdateOrganizationRole Runs `updateOrganizationRole` and returns the result. paramorgIdString Org id. paramroleNameString Role name. paramrequestUpdateOrganizationRoleRequest Request payload. returnsUpdateOrganizationRoleResponse The updated resource. ```java import com.scalekit.grpc.scalekit.v1.roles.UpdateOrganizationRoleRequest; UpdateOrganizationRoleRequest req = UpdateOrganizationRoleRequest.newBuilder().build(); scalekitClient.roles().updateOrganizationRole("org_123", "org_admin", req); ``` ### deleteOrganizationRole [Section titled “deleteOrganizationRole”](#deleteorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#deleteOrganizationRole)asyncdeleteOrganizationRole Runs `deleteOrganizationRole` and returns the result. paramorgIdString Org id. paramroleNameString Role name. paramreassignRoleNameString Reassign role name. returnsvoid No return value ```java scalekitClient.roles().deleteOrganizationRole("org_123", "old_role", "new_role"); ``` ### getOrganizationRoleUsersCount [Section titled “getOrganizationRoleUsersCount”](#getorganizationroleuserscount) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#getOrganizationRoleUsersCount)asyncgetOrganizationRoleUsersCount Runs `getOrganizationRoleUsersCount` and returns the result. paramorgIdString Org id. paramroleNameString Role name. returnsGetOrganizationRoleUsersCountResponse The response payload for this operation. ### updateDefaultOrganizationRoles [Section titled “updateDefaultOrganizationRoles”](#updatedefaultorganizationroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#updateDefaultOrganizationRoles)asyncupdateDefaultOrganizationRoles Runs `updateDefaultOrganizationRoles` and returns the result. paramorgIdString Org id. paramrequestUpdateDefaultOrganizationRolesRequest Request payload. returnsUpdateDefaultOrganizationRolesResponse The updated resource. ```java import com.scalekit.grpc.scalekit.v1.roles.UpdateDefaultOrganizationRolesRequest; UpdateDefaultOrganizationRolesRequest req = UpdateDefaultOrganizationRolesRequest.newBuilder().build(); scalekitClient.roles().updateDefaultOrganizationRoles("org_123", req); ``` ### updateDefaultRoles [Section titled “updateDefaultRoles”](#updatedefaultroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#updateDefaultRoles)asyncupdateDefaultRoles Runs `updateDefaultRoles` and returns the result. paramrequestUpdateDefaultRolesRequest Request payload. returnsUpdateDefaultRolesResponse The updated resource. ### listDependentRoles [Section titled “listDependentRoles”](#listdependentroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#listDependentRoles)asynclistDependentRoles Runs `listDependentRoles` and returns the result. paramroleNameString Role name. returnsListDependentRolesResponse Paginated results. ### deleteRoleBase [Section titled “deleteRoleBase”](#deleterolebase) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#deleteRoleBase)asyncdeleteRoleBase Runs `deleteRoleBase` and returns the result. paramroleNameString Role name. returnsDeprecated void Deprecated void ```java scalekitClient.roles().deleteRoleBase("admin"); ``` ### deleteOrganizationRoleBase [Section titled “deleteOrganizationRoleBase”](#deleteorganizationrolebase) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/RoleClient.java [#](#deleteOrganizationRoleBase)asyncdeleteOrganizationRoleBase Runs `deleteOrganizationRoleBase` and returns the result. paramorgIdString Org id. paramroleNameString Role name. returnsvoid No return value --- # DOCUMENT BOUNDARY --- # Create client > Create the SDK client and run core auth helpers Create a `ScalekitClient` with your environment URL, client ID, and client secret. All other clients hang off this instance. Store credentials in environment variables. Never hard-code secrets. After you create the client, use [Sessions](/saaskit/sdks/java/sessions/) or domain clients such as [Organizations](/saaskit/sdks/java/organizations/) and [Users](/saaskit/sdks/java/users/). ### ScalekitClient [Section titled “ScalekitClient”](#scalekitclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitClient)asyncScalekitClient Creates a new Scalekit client for your environment. paramsiteNameString Site name. paramclientIdString Client ID. paramclientSecretString Client secret. returnsScalekitClient Configured client instance ```java import com.scalekit.ScalekitClient; ScalekitClient scalekitClient = new ScalekitClient( "https://.scalekit.com", "", "" ); ``` ### ScalekitAuthClient [Section titled “ScalekitAuthClient”](#scalekitauthclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitAuthClient)asyncScalekitAuthClient Runs `ScalekitAuthClient` and returns the result. returnsnew new ### ScalekitCredentials [Section titled “ScalekitCredentials”](#scalekitcredentials) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitCredentials)asyncScalekitCredentials Runs `ScalekitCredentials` and returns the result. returnsnew new ### ScalekitOrganizationClient [Section titled “ScalekitOrganizationClient”](#scalekitorganizationclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitOrganizationClient)asyncScalekitOrganizationClient Runs `ScalekitOrganizationClient` and returns the result. returnsnew new ### ScalekitDomainClient [Section titled “ScalekitDomainClient”](#scalekitdomainclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitDomainClient)asyncScalekitDomainClient Runs `ScalekitDomainClient` and returns the result. returnsnew new ### ScalekitConnectionClient [Section titled “ScalekitConnectionClient”](#scalekitconnectionclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitConnectionClient)asyncScalekitConnectionClient Runs `ScalekitConnectionClient` and returns the result. returnsnew new ### ScalekitDirectoryClient [Section titled “ScalekitDirectoryClient”](#scalekitdirectoryclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitDirectoryClient)asyncScalekitDirectoryClient Runs `ScalekitDirectoryClient` and returns the result. returnsnew new ### ScalekitUserClient [Section titled “ScalekitUserClient”](#scalekituserclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitUserClient)asyncScalekitUserClient Runs `ScalekitUserClient` and returns the result. returnsnew new ### ScalekitPasswordlessClient [Section titled “ScalekitPasswordlessClient”](#scalekitpasswordlessclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitPasswordlessClient)asyncScalekitPasswordlessClient Runs `ScalekitPasswordlessClient` and returns the result. returnsnew new ### ScalekitRoleClient [Section titled “ScalekitRoleClient”](#scalekitroleclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitRoleClient)asyncScalekitRoleClient Runs `ScalekitRoleClient` and returns the result. returnsnew new ### ScalekitPermissionClient [Section titled “ScalekitPermissionClient”](#scalekitpermissionclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitPermissionClient)asyncScalekitPermissionClient Runs `ScalekitPermissionClient` and returns the result. returnsnew new ### ScalekitSessionClient [Section titled “ScalekitSessionClient”](#scalekitsessionclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitSessionClient)asyncScalekitSessionClient Runs `ScalekitSessionClient` and returns the result. returnsnew new ### ScalekitWebAuthnClient [Section titled “ScalekitWebAuthnClient”](#scalekitwebauthnclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitWebAuthnClient)asyncScalekitWebAuthnClient Runs `ScalekitWebAuthnClient` and returns the result. returnsnew new ### ScalekitTokenClient [Section titled “ScalekitTokenClient”](#scalekittokenclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitTokenClient)asyncScalekitTokenClient Runs `ScalekitTokenClient` and returns the result. returnsnew new ### ScalekitM2MClient [Section titled “ScalekitM2MClient”](#scalekitm2mclient) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitM2MClient)asyncScalekitM2MClient Runs `ScalekitM2MClient` and returns the result. returnsnew new ### ScalekitWebhook [Section titled “ScalekitWebhook”](#scalekitwebhook) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#ScalekitWebhook)asyncScalekitWebhook Runs `ScalekitWebhook` and returns the result. returnsnew new ### APIException [Section titled “APIException”](#apiexception) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#APIException)asyncAPIException Runs `APIException` and returns the result. returnsthrow new throw new ### organizations [Section titled “organizations”](#organizations) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#organizations)asyncorganizations Runs `organizations` and returns the result. returnspublic OrganizationClient public OrganizationClient ```java scalekitClient.organizations().getById("org_123"); ``` ### domains [Section titled “domains”](#domains) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#domains)asyncdomains Runs `domains` and returns the result. returnspublic DomainClient public DomainClient ```java scalekitClient.domains().listDomainsByOrganizationId("org_123"); ``` ### connections [Section titled “connections”](#connections) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#connections)asyncconnections Runs `connections` and returns the result. returnspublic ConnectionClient public ConnectionClient ```java scalekitClient.connections().listConnectionsByOrganization("org_123"); ``` ### authentication [Section titled “authentication”](#authentication) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#authentication)asyncauthentication Runs `authentication` and returns the result. returnspublic AuthClient public AuthClient ```java scalekitClient.authentication().getClientAccessToken(); ``` ### webhook [Section titled “webhook”](#webhook) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#webhook)asyncwebhook Runs `webhook` and returns the result. returnspublic Webhook public Webhook ```java import java.util.Map; boolean ok = scalekitClient.webhook().verifyWebhookPayload( "whsec_...", headers, payloadBytes ); ``` ### directories [Section titled “directories”](#directories) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#directories)asyncdirectories Runs `directories` and returns the result. returnspublic DirectoryClient public DirectoryClient ```java scalekitClient.directories().listDirectories("org_123"); ``` ### users [Section titled “users”](#users) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#users)asyncusers Runs `users` and returns the result. returnspublic UserClient public UserClient ```java scalekitClient.users().getUser("user_123"); ``` ### passwordless [Section titled “passwordless”](#passwordless) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#passwordless)asyncpasswordless Runs `passwordless` and returns the result. returnspublic PasswordlessClient public PasswordlessClient ```java scalekitClient.passwordless().sendPasswordlessEmail("user@acme.com"); ``` ### roles [Section titled “roles”](#roles) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#roles)asyncroles Runs `roles` and returns the result. returnspublic RoleClient public RoleClient ```java scalekitClient.roles().listRoles(); ``` ### permissions [Section titled “permissions”](#permissions) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#permissions)asyncpermissions Runs `permissions` and returns the result. returnspublic PermissionClient public PermissionClient ```java scalekitClient.permissions().listPermissions(); ``` ### sessions [Section titled “sessions”](#sessions) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#sessions)asyncsessions Runs `sessions` and returns the result. returnspublic SessionClient public SessionClient ```java scalekitClient.sessions().getSession("sess_123"); ``` ### webAuthn [Section titled “webAuthn”](#webauthn) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#webAuthn)asyncwebAuthn Runs `webAuthn` and returns the result. returnspublic WebAuthnClient public WebAuthnClient ```java scalekitClient.webAuthn().listCredentials("user_123"); ``` ### tokens [Section titled “tokens”](#tokens) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#tokens)asynctokens Runs `tokens` and returns the result. returnspublic TokenClient public TokenClient ```java scalekitClient.tokens().create("org_123"); ``` ### m2m [Section titled “m2m”](#m2m) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/ScalekitClient.java [#](#m2m)asyncm2m Runs `m2m` and returns the result. returnspublic M2MClient public M2MClient ```java scalekitClient.m2m().listOrganizationClients("org_123", 20, ""); ``` --- # DOCUMENT BOUNDARY --- # Sessions > Validate tokens and manage user sessions Use `scalekitClient.sessions()` to inspect session metadata and revoke sessions when users sign out or you detect risk. Typical flows: load a session by ID after authentication, list a user’s sessions for a security UI, and revoke one session or all of a user’s sessions on logout. ### getSession [Section titled “getSession”](#getsession) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/SessionClient.java [#](#getSession)asyncgetSession Runs `getSession` and returns the result. paramsessionIdString Session ID. returnsSessionDetails The response payload for this operation. ```java scalekitClient.sessions().getSession("sess_123"); ``` ### getUserSessions [Section titled “getUserSessions”](#getusersessions) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/SessionClient.java [#](#getUserSessions)asyncgetUserSessions Runs `getUserSessions` and returns the result. paramuserIdString Scalekit user ID (`usr_...`). parampageSizeInteger Page size. parampageTokenString Page token. paramfilterUserSessionFilter Result filter. returnsUserSessionDetails The response payload for this operation. ```java import com.scalekit.grpc.scalekit.v1.sessions.UserSessionFilter; UserSessionFilter filter = UserSessionFilter.newBuilder().build(); scalekitClient.sessions().getUserSessions("user_123", 20, "", filter); ``` ### revokeSession [Section titled “revokeSession”](#revokesession) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/SessionClient.java [#](#revokeSession)asyncrevokeSession Runs `revokeSession` and returns the result. paramsessionIdString Session ID. returnsRevokeSessionResponse Empty on success. ```java scalekitClient.sessions().revokeSession("sess_123"); ``` ### revokeAllUserSessions [Section titled “revokeAllUserSessions”](#revokeallusersessions) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/SessionClient.java [#](#revokeAllUserSessions)asyncrevokeAllUserSessions Runs `revokeAllUserSessions` and returns the result. paramuserIdString Scalekit user ID (`usr_...`). returnsRevokeAllUserSessionsResponse Empty on success. ```java scalekitClient.sessions().revokeAllUserSessions("user_123"); ``` --- # DOCUMENT BOUNDARY --- # Token helpers > Token helpers and verification Create and validate API tokens for programmatic access to Scalekit APIs in Java. Use tokens for service access that is not a full end-user session. Create a token, store it securely, and validate it on incoming requests. ### create [Section titled “create”](#create) clientTokenshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/TokenClient.java [#](#create)create Creates a new API token for an organization with custom options. paramorganizationIdString The organization ID to scope the token to paramuserIdString Optional user ID to scope the token to a specific user paramcustomClaimsMap(String, String) Optional custom claims key-value pairs paramexpiryTimestamp Optional expiry timestamp paramdescriptionString Optional human-readable description returnsCreateTokenResponse The created resource. ```java import java.util.HashMap; import java.util.Map; import com.google.protobuf.Timestamp; Map claims = new HashMap<>(); claims.put("environment", "production"); Timestamp expiry = Timestamp.newBuilder() .setSeconds(System.currentTimeMillis() / 1000 + 86400) .build(); scalekitClient.tokens().create("org_123", "user_123", claims, expiry, "Production access token"); ``` ### validate [Section titled “validate”](#validate) clientTokenshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/TokenClient.java [#](#validate)validate Validates an API token and returns associated context. paramtokenString The opaque token string or token_id (apit_xxxxx) returnsValidateTokenResponse Verified claims or result. ```java scalekitClient.tokens().validate("apit_xxxxx"); ``` ### invalidate [Section titled “invalidate”](#invalidate) clientTokenshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/TokenClient.java [#](#invalidate)invalidate Invalidates (soft deletes) an API token. paramtokenString The opaque token string or token_id (apit_xxxxx) returnsvoid No return value ```java scalekitClient.tokens().invalidate("apit_xxxxx"); ``` ### list [Section titled “list”](#list) clientTokenshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/TokenClient.java [#](#list)list Lists API tokens for an organization and user with pagination. paramorganizationIdString The organization ID to list tokens for paramuserIdString The user ID to filter tokens for parampageSizeint Page size (default 10, max 30) parampageTokenString Pagination cursor for next page returnsListTokensResponse Paginated results. ```java scalekitClient.tokens().list("org_123", "user_123", 20, ""); ``` ### update [Section titled “update”](#update) clientTokenshttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/TokenClient.java [#](#update)update Updates the custom claims and/or description of an existing API token. paramtokenString The opaque token string or token_id (apit_xxxxx) paramcustomClaimsMap(String, String) Claims to merge; set value to "" to remove a claim paramdescriptionString Replacement description; null leaves unchanged, empty. returnsUpdateTokenResponse The updated resource. ```java import java.util.HashMap; import java.util.Map; Map newClaims = new HashMap<>(); newClaims.put("environment", "staging"); newClaims.put("old_claim", ""); // Remove this claim scalekitClient.tokens().update("apit_xxxxx", newClaims, "Updated description"); ``` --- # DOCUMENT BOUNDARY --- # Users > Provision and manage users and memberships Use `scalekitClient.users()` to provision people, manage organization memberships, and assign roles. Common flows: invite or create a user, attach them to an organization, update profile fields, and look up users by Scalekit ID or your external ID. ### createUserAndMembership [Section titled “createUserAndMembership”](#createuserandmembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#createUserAndMembership)asynccreateUserAndMembership Runs `createUserAndMembership` and returns the result. paramorganizationIdString Organization ID. paramrequestCreateUserAndMembershipRequest Request payload. returnsCreateUserAndMembershipResponse The created resource. ```java import com.scalekit.grpc.scalekit.v1.users.CreateUserAndMembershipRequest; CreateUserAndMembershipRequest req = CreateUserAndMembershipRequest.newBuilder() .setEmail("user@acme.com") .build(); scalekitClient.users().createUserAndMembership("org_123", req); ``` ### getUser [Section titled “getUser”](#getuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#getUser)asyncgetUser Runs `getUser` and returns the result. paramuserIdString Scalekit user ID (`usr_...`). returnsGetUserResponse The response payload for this operation. ```java scalekitClient.users().getUser("user_123"); ``` ### listUsers [Section titled “listUsers”](#listusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#listUsers)asynclistUsers Runs `listUsers` and returns the result. paramrequestListUsersRequest Request payload. returnsListUsersResponse Paginated results. ```java import com.scalekit.grpc.scalekit.v1.users.ListUsersRequest; ListUsersRequest req = ListUsersRequest.newBuilder() .setPageSize(20) .build(); scalekitClient.users().listUsers(req); ``` ### updateUser [Section titled “updateUser”](#updateuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#updateUser)asyncupdateUser Runs `updateUser` and returns the result. paramuserIdString Scalekit user ID (`usr_...`). paramrequestUpdateUserRequest Request payload. returnsUpdateUserResponse The updated resource. ```java import com.scalekit.grpc.scalekit.v1.users.UpdateUserRequest; UpdateUserRequest req = UpdateUserRequest.newBuilder() .setGivenName("Jane") .build(); scalekitClient.users().updateUser("user_123", req); ``` ### deleteUser [Section titled “deleteUser”](#deleteuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#deleteUser)asyncdeleteUser Runs `deleteUser` and returns the result. paramuserIdString Scalekit user ID (`usr_...`). returnsvoid No return value ```java scalekitClient.users().deleteUser("user_123"); ``` ### createMembership [Section titled “createMembership”](#createmembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#createMembership)asynccreateMembership Runs `createMembership` and returns the result. paramorganizationIdString Organization ID. paramuserIdString Scalekit user ID (`usr_...`). paramrequestCreateMembershipRequest Request payload. returnsCreateMembershipResponse The created resource. ```java import com.scalekit.grpc.scalekit.v1.users.CreateMembershipRequest; CreateMembershipRequest req = CreateMembershipRequest.newBuilder().build(); scalekitClient.users().createMembership("org_123", "user_123", req); ``` ### deleteMembership [Section titled “deleteMembership”](#deletemembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#deleteMembership)asyncdeleteMembership Runs `deleteMembership` and returns the result. paramorganizationIdString Organization ID. paramuserIdString Scalekit user ID (`usr_...`). returnsvoid No return value ```java scalekitClient.users().deleteMembership("org_123", "user_123"); ``` ### updateMembership [Section titled “updateMembership”](#updatemembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#updateMembership)asyncupdateMembership Runs `updateMembership` and returns the result. paramorganizationIdString Organization ID. paramuserIdString Scalekit user ID (`usr_...`). paramrequestUpdateMembershipRequest Request payload. returnsUpdateMembershipResponse The updated resource. ```java import com.scalekit.grpc.scalekit.v1.users.UpdateMembershipRequest; UpdateMembershipRequest req = UpdateMembershipRequest.newBuilder().build(); scalekitClient.users().updateMembership("org_123", "user_123", req); ``` ### listOrganizationUsers [Section titled “listOrganizationUsers”](#listorganizationusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#listOrganizationUsers)asynclistOrganizationUsers Runs `listOrganizationUsers` and returns the result. paramorganizationIdString Organization ID. paramrequestListOrganizationUsersRequest Request payload. returnsListOrganizationUsersResponse Paginated results. ```java import com.scalekit.grpc.scalekit.v1.users.ListOrganizationUsersRequest; ListOrganizationUsersRequest req = ListOrganizationUsersRequest.newBuilder() .setPageSize(20) .build(); scalekitClient.users().listOrganizationUsers("org_123", req); ``` ### searchUsers [Section titled “searchUsers”](#searchusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#searchUsers)asyncsearchUsers Runs `searchUsers` and returns the result. paramrequestSearchUsersRequest Request payload. returnsSearchUsersResponse Paginated results. ### searchOrganizationUsers [Section titled “searchOrganizationUsers”](#searchorganizationusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#searchOrganizationUsers)asyncsearchOrganizationUsers Runs `searchOrganizationUsers` and returns the result. paramorganizationIdString Organization ID. paramrequestSearchOrganizationUsersRequest Request payload. returnsSearchOrganizationUsersResponse Paginated results. ### resendInvite [Section titled “resendInvite”](#resendinvite) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#resendInvite)asyncresendInvite Runs `resendInvite` and returns the result. paramorganizationIdString Organization ID. paramuserIdString Scalekit user ID (`usr_...`). returnsResendInviteResponse Resend invite. ```java scalekitClient.users().resendInvite("org_123", "user_123"); ``` ### listUserRoles [Section titled “listUserRoles”](#listuserroles) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#listUserRoles)asynclistUserRoles Runs `listUserRoles` and returns the result. paramorganizationIdString Organization ID. paramuserIdString Scalekit user ID (`usr_...`). returnsListUserRolesResponse Paginated results. ### listUserPermissions [Section titled “listUserPermissions”](#listuserpermissions) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#listUserPermissions)asynclistUserPermissions Runs `listUserPermissions` and returns the result. paramorganizationIdString Organization ID. paramuserIdString Scalekit user ID (`usr_...`). returnsListUserPermissionsResponse Paginated results. ### getUserByExternalId [Section titled “getUserByExternalId”](#getuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#getUserByExternalId)asyncgetUserByExternalId Runs `getUserByExternalId` and returns the result. paramexternalIdString External ID. returnsGetUserResponse The response payload for this operation. ### updateUserByExternalId [Section titled “updateUserByExternalId”](#updateuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#updateUserByExternalId)asyncupdateUserByExternalId Runs `updateUserByExternalId` and returns the result. paramexternalIdString External ID. paramrequestUpdateUserRequest Request payload. returnsUpdateUserResponse The updated resource. ### deleteUserByExternalId [Section titled “deleteUserByExternalId”](#deleteuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#deleteUserByExternalId)asyncdeleteUserByExternalId Runs `deleteUserByExternalId` and returns the result. paramexternalIdString External ID. returnsvoid No return value ### createMembershipByExternalId [Section titled “createMembershipByExternalId”](#createmembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#createMembershipByExternalId)asynccreateMembershipByExternalId Runs `createMembershipByExternalId` and returns the result. paramorganizationIdString Organization ID. paramexternalIdString External ID. paramrequestCreateMembershipRequest Request payload. returnsCreateMembershipResponse The created resource. ### deleteMembershipByExternalId [Section titled “deleteMembershipByExternalId”](#deletemembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#deleteMembershipByExternalId)asyncdeleteMembershipByExternalId Runs `deleteMembershipByExternalId` and returns the result. paramorganizationIdString Organization ID. paramexternalIdString External ID. returnsvoid No return value ### updateMembershipByExternalId [Section titled “updateMembershipByExternalId”](#updatemembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/UserClient.java [#](#updateMembershipByExternalId)asyncupdateMembershipByExternalId Runs `updateMembershipByExternalId` and returns the result. paramorganizationIdString Organization ID. paramexternalIdString External ID. paramrequestUpdateMembershipRequest Request payload. returnsUpdateMembershipResponse The updated resource. --- # DOCUMENT BOUNDARY --- # Passkeys > Passkey and WebAuthn credentials Use `scalekitClient.webAuthn()` to register and authenticate passkeys (WebAuthn credentials). Build registration and authentication ceremonies with the options and verification methods below. ### listCredentials [Section titled “listCredentials”](#listcredentials) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/WebAuthnClient.java [#](#listCredentials)asynclistCredentials Lists all WebAuthn credentials for a user paramuserIdString The ID of the user whose credentials to list returnsListCredentialsResponse Paginated results. ```java scalekitClient.webAuthn().listCredentials("user_123"); ``` ### updateCredential [Section titled “updateCredential”](#updatecredential) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/WebAuthnClient.java [#](#updateCredential)asyncupdateCredential Updates a WebAuthn credential’s display name paramcredentialIdString The ID of the credential to update paramdisplayNameString The new display name for the credential returnsUpdateCredentialResponse The updated resource. ```java scalekitClient.webAuthn().updateCredential("cred_123", "My laptop key"); ``` ### deleteCredential [Section titled “deleteCredential”](#deletecredential) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-java/blob/main/src/main/java/com/scalekit/api/WebAuthnClient.java [#](#deleteCredential)asyncdeleteCredential Deletes a WebAuthn credential paramcredentialIdString The ID of the credential to delete returnsDeleteCredentialResponse Empty on success. ```java scalekitClient.webAuthn().deleteCredential("cred_123"); ``` --- # DOCUMENT BOUNDARY --- # Next.js session middleware > Add hosted login and an encrypted session cookie to the Next.js App Router Use `ScalekitAuthNext` from `@scalekit-sdk/node/next` to add hosted login, an encrypted `sk_session` cookie, token refresh, and logout to the App Router. Typical flow: create one `auth` instance, export login/callback/logout Route Handlers, and wrap a protected handler with `withAuth`. Use `createMiddleware()` to fail closed on every other path. For Edge Runtime, pass `ScalekitEdgeClient` instead of `ScalekitClient`. Requires `@scalekit-sdk/node` 2.12.0 or later. Register these URLs in the Scalekit Dashboard under **Authentication > Redirects** before you test: | Dashboard field | Must match | | ---------------------------- | -------------------------------------------------------------------- | | **Redirect URI** | `redirectUri` exactly, for example `http://localhost:3000/callback` | | **Post Logout Redirect URI** | Absolute URL after full logout, for example `http://localhost:3000/` | | **Initiate Login URL** | Login path, for example `http://localhost:3000/login` | Store credentials in environment variables. Never hard-code secrets. .env ```bash SCALEKIT_ENVIRONMENT_URL=https://your-env.scalekit.com SCALEKIT_CLIENT_ID=skc_... SCALEKIT_CLIENT_SECRET=... COOKIE_ENCRYPTION_SECRET= # openssl rand -base64 32 REDIRECT_URI=http://localhost:3000/callback ``` Keep `COOKIE_ENCRYPTION_SECRET` identical on every server instance. ## Install the package [Section titled “Install the package”](#install-the-package) Terminal ```bash npm install @scalekit-sdk/node ``` ## Protect a route [Section titled “Protect a route”](#protect-a-route) `ScalekitAuthNext` requires a `client`. It does not accept `envUrl` alone. lib/auth.ts ```typescript import ScalekitClient from '@scalekit-sdk/node'; import { ScalekitAuthNext } from '@scalekit-sdk/node/next'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET! ); export const auth = new ScalekitAuthNext({ client: scalekit, redirectUri: process.env.REDIRECT_URI!, cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET!, }); ``` app/login/route.ts ```typescript import { auth } from '../../lib/auth'; export const GET = auth.createLoginHandler(); ``` app/callback/route.ts ```typescript import { auth } from '../../lib/auth'; export const GET = auth.createCallbackHandler(); ``` app/logout/route.ts ```typescript import { auth } from '../../lib/auth'; export const GET = auth.createLogoutHandler(); ``` app/account/route.ts ```typescript import { auth } from '../../lib/auth'; export const GET = auth.withAuth(async (request, { user }) => { return Response.json({ sub: user?.sub }); }); ``` Open `http://localhost:3000/account`. A missing session returns **302** to `/login`, not a JSON 401. `user` is access-token claims. `sub` is always present. `email` appears only when you add it as a custom access-token claim. Pin runtime: 'nodejs' unless you use ScalekitEdgeClient `createMiddleware()` uses `ScalekitClient` by default. That client is Node-only. Export `runtime: 'nodejs'` in `middleware.ts`, or the Next.js build fails. See [`ScalekitEdgeClient`](#scalekitedgeclient) for Edge Runtime. ### constructor [Section titled “constructor”](#constructor) classScalekitAuthNexthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/nextjs.ts [#](#constructor)constructor Creates the App Router session helper. Pass a `ScalekitClient` or `ScalekitEdgeClient`. paramclientScalekitClient | ScalekitEdgeClient Auth client. Required. `ScalekitClient` is Node-only; use `ScalekitEdgeClient` on Edge Runtime. paramredirectUristring Exact **Redirect URI** registered in the dashboard. paramcookieEncryptionSecretstring Secret used to encrypt `sk_session`. Generate with `openssl rand -base64 32`. paramcookieNamestring Session cookie name. optional, default sk_session paramloginPathstring Login route path. optional, default /login paramcallbackPathstring Callback route path. Also excluded from `createMiddleware()` gating. optional, default /callback paramlogoutPathstring Logout route path. Also excluded from `createMiddleware()` gating. optional, default /logout parampostLoginRedirectstring Fallback path after login when `returnTo` is absent. optional, default / parampostLogoutRedirectUristring Where logout lands. Defaults to `postLoginRedirect`. Register the absolute URL as **Post Logout Redirect URI**. optional paramfullLogoutboolean When `true`, logout ends the Scalekit session with `id_token_hint`. optional, default true returnsScalekitAuthNext Helper used by Route Handlers and middleware. ```typescript export const auth = new ScalekitAuthNext({ client: scalekit, redirectUri: process.env.REDIRECT_URI!, cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET!, }); ``` ### createLoginHandler [Section titled “createLoginHandler”](#createloginhandler) classScalekitAuthNexthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/nextjs.ts [#](#createLoginHandler)createLoginHandler Returns a `GET` Route Handler that starts hosted login and sets the CSRF state cookie. returns(request: NextRequest) => Promise\ Handler to re-export from `app/login/route.ts`. ```typescript export const GET = auth.createLoginHandler(); ``` ### createCallbackHandler [Section titled “createCallbackHandler”](#createcallbackhandler) classScalekitAuthNexthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/nextjs.ts [#](#createCallbackHandler)createCallbackHandler Returns a `GET` Route Handler that exchanges the authorization code and sets `sk_session`. returns(request: NextRequest) => Promise\ Handler to re-export from `app/callback/route.ts`. ```typescript export const GET = auth.createCallbackHandler(); ``` ### createLogoutHandler [Section titled “createLogoutHandler”](#createlogouthandler) classScalekitAuthNexthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/nextjs.ts [#](#createLogoutHandler)createLogoutHandler Returns a `GET` Route Handler that clears `sk_session`. With `fullLogout: true`, also ends the Scalekit session. returns(request: NextRequest) => Promise\ Handler to re-export from `app/logout/route.ts`. ```typescript export const GET = auth.createLogoutHandler(); ``` ### withAuth [Section titled “withAuth”](#withauth) classScalekitAuthNexthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/nextjs.ts [#](#withAuth)asyncwithAuth Wraps a Route Handler so it runs only with a valid session. Refreshes the cookie about 10 seconds before expiry. Redirects to `loginPath` when the session is missing. paramhandler(request, context) => NextResponse Route Handler. `context.user` is access-token claims. returns(request: NextRequest) => Promise\ Wrapped handler. Missing session → 302, never JSON 401. ```typescript export const GET = auth.withAuth(async (request, { user }) => { return Response.json({ sub: user?.sub }); }); ``` ### createMiddleware [Section titled “createMiddleware”](#createmiddleware) classScalekitAuthNexthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/nextjs.ts [#](#createMiddleware)createMiddleware Fail-closed middleware. Every matched path redirects to login unless it is listed in `publicRoutes` or is `loginPath`, `callbackPath`, or `logoutPath`. Next.js reads `export const config` as a static export. This method cannot generate that object. parampublicRoutesstring\[] Paths that stay public, for example `['/', '/pricing']`. optional returns(request: NextRequest) => Promise\ Middleware function to export as the default from `middleware.ts`. ```typescript export default auth.createMiddleware({ publicRoutes: ['/', '/pricing'], }); export const config = { runtime: 'nodejs', matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], }; ``` ### getSession [Section titled “getSession”](#getsession) classScalekitAuthNexthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/nextjs.ts [#](#getSession)asyncgetSession Read-only session lookup for Server Components, Route Handlers, and Server Actions. Does not refresh or write a cookie. Only `createMiddleware()` and `withAuth()` write a new cookie. returns{ user, expiresAt } | null Access-token claims and expiry, or `null`. Never includes `accessToken` or `refreshToken`. ```typescript const session = await auth.getSession(); if (session) { console.log(session.user.sub, session.expiresAt); } ``` ### currentUser [Section titled “currentUser”](#currentuser) classScalekitAuthNexthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/frameworks/nextjs.ts [#](#currentUser)asynccurrentUser Shortcut for `getSession()` when only claims are needed. returnsRecord\ | undefined Access-token claims, or `undefined` when there is no valid session. ```typescript const user = await auth.currentUser(); ``` Same-request expiry after a refresh If middleware refreshes the cookie on the current request, `getSession()` on that same request can still read the old `expiresAt`. The next request sees the new cookie. ### ScalekitEdgeClient [Section titled “ScalekitEdgeClient”](#scalekitedgeclient) classScalekitEdgeClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/edge.ts [#](#constructor)constructor Fetch + `jose` client for the auth methods `ScalekitAuthNext` needs on Edge Runtime. Not a full `ScalekitClient`. Use [Create client](/saaskit/sdks/node/scalekit-client/) for Organizations, Users, and other API clients. paramenvUrlstring Scalekit environment URL. paramclientIdstring Application client ID. paramclientSecretstring Application client secret. returnsScalekitEdgeClient Drop-in `client` for `ScalekitAuthNext`. ```typescript import { ScalekitEdgeClient } from '@scalekit-sdk/node/edge'; import { ScalekitAuthNext } from '@scalekit-sdk/node/next'; const scalekit = new ScalekitEdgeClient( process.env.SCALEKIT_ENVIRONMENT_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET! ); export const auth = new ScalekitAuthNext({ client: scalekit, redirectUri: process.env.REDIRECT_URI!, cookieEncryptionSecret: process.env.COOKIE_ENCRYPTION_SECRET!, }); ``` middleware.ts ```typescript import { auth } from './lib/auth'; export default auth.createMiddleware({ publicRoutes: ['/', '/pricing'], }); export const config = { runtime: 'experimental-edge', matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'], }; ``` Use experimental-edge, not edge This Next.js version rejects the string `'edge'` at build time. Set `runtime: 'experimental-edge'`. --- # DOCUMENT BOUNDARY --- # Authorization > Authorization helpers for protected resources Use auth helpers for authorization code exchange, token validation, and related login token flows on the Scalekit client. These methods sit next to session management—use them when implementing hosted login callbacks and token checks. ### getLoginRequestDetails [Section titled “getLoginRequestDetails”](#getloginrequestdetails) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/auth.ts [#](#getLoginRequestDetails)asyncgetLoginRequestDetails Resolves a login request ID into the authorization request it was issued for, the OAuth client that started it, and the resource being accessed. Call it when your login page needs to know which client is asking and which scopes were requested. Available in `@scalekit-sdk/node` 2.17.0 and later. paramloginRequestIdstring The login request identifier from the authorize redirect, in `lri_` format. It expires 15 minutes after the authorization request is handed off. returnsGetLoginRequestDetailsResponse The authorization request and its requested scopes, the client that started it, and the resource it targets. `resource` is absent when the authorization request is not scoped to a resource, and also when Scalekit cannot resolve the resource — the call succeeds either way. Treat an absent `resource` as nothing to display, not as proof the request was unscoped. ```typescript const details = await scalekit.auth.getLoginRequestDetails('lri_73415099636808061'); // Which client is asking, and what it requested console.log(details.client?.clientName); console.log(details.client?.clientId); // CIMD metadata URL for a CIMD client, otherwise m2m_xxx console.log(details.client?.skClientId); // always m2m_xxx, for every client console.log(details.authRequest?.scopes); ``` For a client registered through a Client ID Metadata Document (CIMD), `clientId` is the HTTPS URL of that document while `skClientId` is the identifier Scalekit issued, in `m2m_xxx` format. For every other client both are `m2m_xxx`. Use `skClientId` to correlate the client across Scalekit APIs and the dashboard. An unknown login request ID, an expired one, and one belonging to another environment all fail the same way, so the error cannot tell you which of the three it was. ### updateLoginUserDetails [Section titled “updateLoginUserDetails”](#updateloginuserdetails) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/auth.ts [#](#updateLoginUserDetails)asyncupdateLoginUserDetails Updates user details for an ongoing authentication request. paramconnectionIdstring The SSO connection ID being used for authentication paramloginRequestIdstring The unique login request identifier from the auth flow paramuserUserInput User profile fields. returnsUpdateLoginUserDetailsResponse Response containing the auth request ID (`response.authRequestId`), which you can use to look up the user’s authentication journey in the auth logs. ```typescript const response = await scalekit.auth.updateLoginUserDetails( 'conn_abc123', 'lri_73415099636808061', { email: 'john.doe@company.com', sub: 'unique_user_id_456', } ); // The auth request ID can be used to look up the authentication journey via auth logs. console.log(response.authRequestId); ``` --- # DOCUMENT BOUNDARY --- # Connections > Configure enterprise SSO connections Use the `connection` client to configure enterprise SSO (SAML/OIDC) for an organization. Enable SSO after the organization exists: create a connection, complete IdP setup, and test sign-in for that org. ### getConnection [Section titled “getConnection”](#getconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/connection.ts [#](#getConnection)asyncgetConnection Retrieves complete configuration and status details for a specific SSO connection. paramorganizationIdstring Organization ID. paramidstring The connection identifier to retrieve (format: “conn\_…”) returnsGetConnectionResponse The connection. ```typescript const response = await scalekit.connection.getConnection( 'org_123456', 'conn_abc123' ); const conn = response.connection; console.log('Provider:', conn.provider); console.log('Type:', conn.type); console.log('Status:', conn.enabled ? 'Enabled' : 'Disabled'); ``` ### listConnectionsByDomain [Section titled “listConnectionsByDomain”](#listconnectionsbydomain) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/connection.ts [#](#listConnectionsByDomain)asynclistConnectionsByDomain Lists all SSO connections associated with a specific email domain. paramdomainstring The email domain to search for (e.g., “acme.com”) returnsListConnectionsResponse Paginated connections. ```typescript const response = await scalekit.connection.listConnectionsByDomain('acme.com'); if (response.connections.length > 0) { console.log('SSO available for domain acme.com'); const connection = response.connections[0]; console.log('Organization:', connection.organizationId); } ``` ### listConnections [Section titled “listConnections”](#listconnections) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/connection.ts [#](#listConnections)asynclistConnections Lists all SSO connections configured for an organization. paramorganizationIdstring The organization ID returnsListConnectionsResponse Paginated connections. ```typescript const response = await scalekit.connection.listConnections('org_123456'); console.log(`Found ${response.connections.length} connections`); response.connections.forEach(conn => { console.log(`- ${conn.provider} (${conn.type}): ${conn.enabled ? 'Enabled' : 'Disabled'}`); }); ``` ### listAppConnections [Section titled “listAppConnections”](#listappconnections) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/connection.ts [#](#listAppConnections)asynclistAppConnections Lists app-level connections configured for the account (not scoped to an organization). paramparamsobject Common fields: `pageSize`, `pageToken`, `provider`. Pass `query` for a free-text search that matches connections by connection name (key ID) or provider (3–100 characters). pageSize, pageToken, provider, query returnsListAppConnectionsResponse Paginated connections. ### enableConnection [Section titled “enableConnection”](#enableconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/connection.ts [#](#enableConnection)asyncenableConnection Enables an SSO connection for an organization. paramorganizationIdstring The organization ID paramidstring The connection ID to enable returnsToggleConnectionResponse Response with updated connection status ```typescript const response = await scalekit.connection.enableConnection( 'org_123456', 'conn_abc123' ); console.log('Connection enabled:', response.connection.enabled); ``` ### disableConnection [Section titled “disableConnection”](#disableconnection) clientConnectionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/connection.ts [#](#disableConnection)asyncdisableConnection Disables an SSO connection for an organization. paramorganizationIdstring The organization ID paramidstring The connection ID to disable returnsToggleConnectionResponse Response with updated connection status ```typescript const response = await scalekit.connection.disableConnection( 'org_123456', 'conn_abc123' ); console.log('Connection disabled:', !response.connection.enabled); ``` --- # DOCUMENT BOUNDARY --- # Directory > Sync users and groups via SCIM directories Use the `directory` client for SCIM directory sync—listing providers, users, and groups provisioned from an IdP. Use this after SSO is in place when the customer provisions users via SCIM. ### listDirectories [Section titled “listDirectories”](#listdirectories) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/directory.ts [#](#listDirectories)asynclistDirectories Retrieves all SCIM directories configured for an organization. paramorganizationIdstring Organization ID. returnsListDirectoriesResponse Directories for the org. ```typescript const response = await scalekit.directory.listDirectories('org_123456'); console.log(`Found ${response.directories.length} directories`); response.directories.forEach(dir => { console.log(`- ${dir.provider}: ${dir.enabled ? 'Active' : 'Inactive'}`); console.log(` Users: ${dir.userCount}, Groups: ${dir.groupCount}`); }); ``` ### getDirectory [Section titled “getDirectory”](#getdirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/directory.ts [#](#getDirectory)asyncgetDirectory Retrieves comprehensive details about a specific SCIM directory. paramorganizationIdstring Organization ID. paramdirectoryIdstring The directory identifier to retrieve (format: “dir\_…”) returnsGetDirectoryResponse The directory. ```typescript const response = await scalekit.directory.getDirectory( 'org_123456', 'dir_abc123' ); const dir = response.directory; console.log('Provider:', dir.provider); console.log('Status:', dir.enabled ? 'Active' : 'Inactive'); console.log('Users:', dir.userCount); ``` ### getPrimaryDirectoryByOrganizationId [Section titled “getPrimaryDirectoryByOrganizationId”](#getprimarydirectorybyorganizationid) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/directory.ts [#](#getPrimaryDirectoryByOrganizationId)asyncgetPrimaryDirectoryByOrganizationId Retrieves the primary (first) directory for an organization. paramorganizationIdstring Organization ID. returnsDirectory The first directory object for the organization ```typescript const directory = await scalekit.directory.getPrimaryDirectoryByOrganizationId('org_123456'); console.log('Primary Directory:', directory.provider); console.log('Users:', directory.userCount); ``` ### listDirectoryUsers [Section titled “listDirectoryUsers”](#listdirectoryusers) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/directory.ts [#](#listDirectoryUsers)asynclistDirectoryUsers Lists users synchronized from an identity provider via SCIM directory sync. paramorganizationIdstring The organization ID paramdirectoryIdstring The directory ID paramoptionsobject Optional fields: `pageSize`, `pageToken`, `includeDetail`, `directoryGroupId`, `updatedAfter`. pageSize, pageToken, includeDetail, directoryGroupId, updatedAfter returnsListDirectoryUsersResponse Directory users. ```typescript // pageSize: Results per page (default 10, max 100) // pageToken: Token for the next page // includeDetail: include detail // directoryGroupId: directory group id // updatedAfter: Only resources updated after this time const response = await scalekit.directory.listDirectoryUsers( 'org_123456', 'dir_abc123', { pageSize: 50 } ); console.log(`Found ${response.totalSize} users`); ``` ### listDirectoryGroups [Section titled “listDirectoryGroups”](#listdirectorygroups) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/directory.ts [#](#listDirectoryGroups)asynclistDirectoryGroups Retrieves groups synchronized from an identity provider via SCIM directory sync. paramorganizationIdstring The organization ID (format: “org\_…”) paramdirectoryIdstring The directory ID (format: “dir\_…”) paramoptionsobject Optional fields: `pageSize`, `pageToken`, `includeDetail`, `updatedAfter`. pageSize, pageToken, includeDetail, updatedAfter returnsListDirectoryGroupsResponse Directory groups. ```typescript // pageSize: Results per page (default 10, max 100) // pageToken: Token for the next page // includeDetail: include detail // updatedAfter: Only resources updated after this time const response = await scalekit.directory.listDirectoryGroups( 'org_123456', 'dir_abc123', { pageSize: 30 } ); console.log(`Found ${response.totalSize} groups`); ``` ### enableDirectory [Section titled “enableDirectory”](#enabledirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/directory.ts [#](#enableDirectory)asyncenableDirectory Enables SCIM directory synchronization for an organization. paramorganizationIdstring The organization ID (format: “org\_…”) paramdirectoryIdstring The directory ID to enable (format: “dir\_…”) returnsToggleDirectoryResponse The updated resource. ```typescript const response = await scalekit.directory.enableDirectory( 'org_123456', 'dir_abc123' ); console.log('Directory enabled:', response.directory.enabled); ``` ### disableDirectory [Section titled “disableDirectory”](#disabledirectory) clientDirectorieshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/directory.ts [#](#disableDirectory)asyncdisableDirectory Disables SCIM directory synchronization for an organization. paramorganizationIdstring The organization ID (format: “org\_…”) paramdirectoryIdstring The directory ID to disable (format: “dir\_…”) returnsToggleDirectoryResponse The updated resource. ```typescript const response = await scalekit.directory.disableDirectory( 'org_123456', 'dir_abc123' ); console.log('Directory disabled:', !response.directory.enabled); ``` --- # DOCUMENT BOUNDARY --- # Domains > Manage organization email domains Use the `domain` client to manage verified email domains for an organization (SSO routing and domain-based policies). Add a domain, complete verification, then use it with SSO and organization policies. ### createDomain [Section titled “createDomain”](#createdomain) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/domain.ts [#](#createDomain)asynccreateDomain Adds a new domain configuration to an organization for SSO routing or user suggestions. paramorganizationIdstring The organization ID (format: “org\_…”) paramnamestring The domain name (e.g., “acme.com”, “example.org”) paramoptionsobject Optional fields: `domainType`. domainType returnsCreateDomainResponse The created domain with ID and verification status ```typescript // domainType: Domain type filter const response = await scalekit.domain.createDomain( 'org_123456', 'acme.com', { domainType: 'ORGANIZATION_DOMAIN' } ); console.log('Domain created:', response.domain.id); ``` ### getDomain [Section titled “getDomain”](#getdomain) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/domain.ts [#](#getDomain)asyncgetDomain Retrieves detailed information about a specific domain configuration. paramorganizationIdstring The organization ID (format: “org\_…”) paramdomainIdstring The domain ID (format: “domain\_…”) returnsGetDomainResponse Complete domain configuration and status ```typescript const response = await scalekit.domain.getDomain( 'org_123456', 'domain_abc123' ); console.log('Domain:', response.domain.domain); console.log('Type:', response.domain.domainType); console.log('Verified:', response.domain.verificationStatus); ``` ### listDomains [Section titled “listDomains”](#listdomains) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/domain.ts [#](#listDomains)asynclistDomains Retrieves all domain configurations for an organization. paramorganizationIdstring The organization ID (format: “org\_…”) paramoptionsobject Optional fields: `domainType`. domainType returnsListDomainResponse List of all domains with their configurations ```typescript // domainType: Domain type filter const response = await scalekit.domain.listDomains('org_123456'); console.log(`Found ${response.domains.length} domains`); response.domains.forEach(domain => { console.log(`- ${domain.domain} (${domain.domainType}): ${domain.verificationStatus}`); }); ``` ### deleteDomain [Section titled “deleteDomain”](#deletedomain) clientDomainshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/domain.ts [#](#deleteDomain)asyncdeleteDomain Deletes a domain from an organization. paramorganizationIdstring The organization ID paramdomainIdstring The domain ID to delete returnsobject Empty response on successful deletion MessageShape(typeof EmptySchema) ```typescript await scalekit.domain.deleteDomain('org_123456', 'domain_abc123'); console.log('Domain deleted successfully'); ``` --- # DOCUMENT BOUNDARY --- # Organizations > Manage your customer organizations Use the `organization` client to create and manage customer tenants (organizations) in your Scalekit application. Start here when onboarding a new customer: create an organization, set external IDs and branding, then add users and SSO for that org. ### createOrganization [Section titled “createOrganization”](#createorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#createOrganization)asynccreateOrganization Creates a new organization (tenant) in your Scalekit application. paramnamestring Display name for the organization (e.g., “Acme Corporation”) paramoptionsobject Optional fields: `externalId`, `logoUrl`, `slug`. externalId, logoUrl, slug returnsCreateOrganizationResponse The created organization. ```typescript // externalId: Your system's ID for this org // logoUrl: Public URL of the org logo // slug: URL-safe org slug const org = await scalekit.organization.createOrganization( 'Acme Corporation', { externalId: 'customer_12345' } ); console.log('Organization ID:', org.organization.id); ``` ### listOrganization [Section titled “listOrganization”](#listorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#listOrganization)asynclistOrganization Retrieves a paginated list of all organizations in your Scalekit environment. paramoptionsobject Optional fields: `pageSize`, `pageToken`. pageSize, pageToken returnsListOrganizationsResponse Paginated organizations. ```typescript // pageSize: Results per page (default 10, max 100) // pageToken: Token for the next page const response = await scalekit.organization.listOrganization({ pageSize: 20 }); console.log('Organizations:', response.organizations); console.log('Total:', response.totalSize); ``` ### getOrganization [Section titled “getOrganization”](#getorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#getOrganization)asyncgetOrganization Retrieves detailed information about a specific organization using its Scalekit ID. paramidstring Scalekit resource ID. returnsGetOrganizationResponse The organization. ```typescript const response = await scalekit.organization.getOrganization('org_12345'); const org = response.organization; console.log('Organization:', org.displayName); console.log('External ID:', org.externalId); ``` ### getOrganizationByExternalId [Section titled “getOrganizationByExternalId”](#getorganizationbyexternalid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#getOrganizationByExternalId)asyncgetOrganizationByExternalId Retrieves detailed information about an organization using your system’s external identifier. paramexternalIdstring Your system’s unique identifier for the organization returnsGetOrganizationResponse The organization. ```typescript const response = await scalekit.organization.getOrganizationByExternalId('customer_12345'); const org = response.organization; console.log('Scalekit ID:', org.id); console.log('Organization:', org.displayName); ``` ### updateOrganization [Section titled “updateOrganization”](#updateorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#updateOrganization)asyncupdateOrganization Updates an organization’s properties using its Scalekit ID. paramidstring The Scalekit organization identifier (format: “org\_…”) paramorganizationobject Fields to update. Partial(UpdateOrganization) returnsUpdateOrganizationResponse The updated organization. ```typescript // displayName: Display name // metadata: Custom key-value metadata const response = await scalekit.organization.updateOrganization('org_12345', { displayName: 'Acme Corporation (Updated)', metadata: { industry: 'Technology', size: 'Enterprise' } }); ``` ### updateOrganizationByExternalId [Section titled “updateOrganizationByExternalId”](#updateorganizationbyexternalid) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#updateOrganizationByExternalId)asyncupdateOrganizationByExternalId Updates an organization’s properties using your system’s external identifier. paramexternalIdstring Your system’s unique identifier for the organization paramorganizationobject Fields to update. Partial(UpdateOrganization) returnsUpdateOrganizationResponse The updated organization. ```typescript // displayName: Display name const response = await scalekit.organization.updateOrganizationByExternalId( 'customer_12345', { displayName: 'New Company Name' } ); ``` ### deleteOrganization [Section titled “deleteOrganization”](#deleteorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#deleteOrganization)asyncdeleteOrganization Permanently deletes an organization from your Scalekit environment. paramorganizationIdstring The Scalekit organization identifier to delete returnsobject Empty response on successful deletion MessageShape(typeof EmptySchema) ```typescript await scalekit.organization.deleteOrganization('org_12345'); console.log('Organization deleted successfully'); ``` ### generatePortalLink [Section titled “generatePortalLink”](#generateportallink) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#generatePortalLink)asyncgeneratePortalLink Creates a single use Admin Portal URL valid for 1 minute. paramorganizationIdstring The Scalekit organization ID returnsLink Portal or auth URL. ```typescript app.get('/admin/sso-settings', async (req, res) => { const organizationId = req.user.organizationId; const link = await scalekit.organization.generatePortalLink(organizationId); res.redirect(link.location); }); ``` ### updateOrganizationSettings [Section titled “updateOrganizationSettings”](#updateorganizationsettings) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#updateOrganizationSettings)asyncupdateOrganizationSettings Updates configuration settings and feature flags for an organization. paramorganizationIdstring Organization ID. paramsettingsOrganizationSettings Configuration settings object containing: returnsGetOrganizationResponse The organization. ```typescript const response = await scalekit.organization.updateOrganizationSettings('org_12345', { features: [ { name: 'sso', enabled: true }, { name: 'dir_sync', enabled: true } ] }); ``` ### upsertUserManagementSettings [Section titled “upsertUserManagementSettings”](#upsertusermanagementsettings) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#upsertUserManagementSettings)asyncupsertUserManagementSettings Creates or updates user management settings for an organization. paramorganizationIdstring Organization ID. paramsettingsOrganizationUserManagementSettingsInput User management configuration: returnsOrganizationUserManagementSettingsMessage | undefined Organization user management settings message | undefined. OrganizationUserManagementSettingsMessage | undefined ```typescript const settings = await scalekit.organization.upsertUserManagementSettings( 'org_12345', { maxAllowedUsers: 100 } ); console.log('Max users allowed:', settings?.maxAllowedUsers); ``` ### searchOrganization [Section titled “searchOrganization”](#searchorganization) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#searchOrganization)asyncsearchOrganization Searches for organizations matching a query string. paramquerystring Search query string parampageSizenumber Optional. Number of results per page parampageTokenstring Optional. Pagination token for the next page returnsSearchOrganizationsResponse Paginated results. ### getOrganizationUserManagementSetting [Section titled “getOrganizationUserManagementSetting”](#getorganizationusermanagementsetting) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#getOrganizationUserManagementSetting)asyncgetOrganizationUserManagementSetting Retrieves the user management settings for a specific organization. paramorganizationIdstring The Scalekit organization identifier returnsGetOrganizationUserManagementSettingsResponse The response payload for this operation. ### updateOrganizationSessionPolicy [Section titled “updateOrganizationSessionPolicy”](#updateorganizationsessionpolicy) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#updateOrganizationSessionPolicy)asyncupdateOrganizationSessionPolicy Sets a custom session policy for an organization or reverts it to application defaults. paramorganizationIdstring Organization ID. parampolicyOrganizationSessionPolicyInput The policy to apply. returnsOrganizationSessionPolicySettings The updated session policy. ### getOrganizationSessionPolicy [Section titled “getOrganizationSessionPolicy”](#getorganizationsessionpolicy) clientOrganizationshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/organization.ts [#](#getOrganizationSessionPolicy)asyncgetOrganizationSessionPolicy Retrieves the session policy for a specific organization. paramorganizationIdstring The Scalekit organization identifier returnsOrganizationSessionPolicySettings The organization’s session policy settings --- # DOCUMENT BOUNDARY --- # Magic link & OTP > Send and verify passwordless login codes Use the `passwordless` client to send and verify email magic links or one-time codes for sign-in. Call send, then verify with the code or link token from the user’s email to complete login. ### sendPasswordlessEmail [Section titled “sendPasswordlessEmail”](#sendpasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/passwordless.ts [#](#sendPasswordlessEmail)asyncsendPasswordlessEmail Send a passwordless authentication email paramemailstring The email address to send the passwordless link to paramoptionsobject Optional fields: `template`, `state`, `magiclinkAuthUri`, `expiresIn`, `templateVariables`. template, state, magiclinkAuthUri, expiresIn, templateVariables returnsSendPasswordlessResponse Send passwordless. ```typescript // template: template // state: Opaque state string // magiclinkAuthUri: magiclink auth uri // expiresIn: expires in // templateVariables: template variables const response = await scalekit.passwordless.sendPasswordlessEmail( 'user@example.com', { template: 'SIGNIN', state: 'random-state', expiresIn: 3600 } ); console.log('Auth Request ID:', response.authRequestId); ``` ### verifyPasswordlessEmail [Section titled “verifyPasswordlessEmail”](#verifypasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/passwordless.ts [#](#verifyPasswordlessEmail)asyncverifyPasswordlessEmail Verify a passwordless authentication code or link token paramcredentialobject code, linkToken. code, linkToken paramauthRequestIdstring Optional. Optional auth request ID from the send response returnsVerifyPasswordLessResponse Verified claims. ```typescript // code: Authorization code // linkToken: link token const response = await scalekit.passwordless.verifyPasswordlessEmail( { code: '123456' }, 'auth_request_id' ); console.log('Email:', response.email); ``` ### resendPasswordlessEmail [Section titled “resendPasswordlessEmail”](#resendpasswordlessemail) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/passwordless.ts [#](#resendPasswordlessEmail)asyncresendPasswordlessEmail Resend a passwordless authentication email paramauthRequestIdstring The auth request ID from the original send response returnsSendPasswordlessResponse Send passwordless. ```typescript const response = await scalekit.passwordless.resendPasswordlessEmail('auth_request_id'); ``` --- # DOCUMENT BOUNDARY --- # Permissions > Manage fine-grained permissions Use the `permission` client to define fine-grained permissions that roles can grant. Model permissions for resources in your app, then attach them to roles. ### createPermission [Section titled “createPermission”](#createpermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#createPermission)asynccreatePermission Creates a new permission defining a specific action users can perform. parampermissionCreatePermission Permission object containing: returnsCreatePermissionResponse Created permission with ID and timestamps ```typescript await scalekit.permission.createPermission({ name: 'read:invoices', description: 'View invoice details' }); ``` ### getPermission [Section titled “getPermission”](#getpermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#getPermission)asyncgetPermission Retrieves complete information for a specific permission. parampermissionNamestring Permission identifier (e.g., ‘read:documents’) returnsGetPermissionResponse Permission details including description and timestamps ```typescript const response = await scalekit.permission.getPermission('read:invoices'); console.log('Description:', response.permission.description); ``` ### listPermissions [Section titled “listPermissions”](#listpermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#listPermissions)asynclistPermissions Lists all permissions with pagination support. parampageTokenstring Optional. Token for retrieving the next page parampageSizenumber Optional. Number of permissions per page (max: 100) returnsListPermissionsResponse Paginated list of permissions ```typescript const response = await scalekit.permission.listPermissions(); response.permissions.forEach(perm => { console.log(`${perm.name}: ${perm.description}`); }); ``` ### updatePermission [Section titled “updatePermission”](#updatepermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#updatePermission)asyncupdatePermission Updates an existing permission’s attributes. parampermissionNamestring Permission to update parampermissionCreatePermission Updated permission properties returnsUpdatePermissionResponse Updated permission details ```typescript await scalekit.permission.updatePermission('read:invoices', { name: 'read:invoices', description: 'View invoice details and history (updated)' }); ``` ### deletePermission [Section titled “deletePermission”](#deletepermission) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#deletePermission)asyncdeletePermission Permanently removes a permission. parampermissionNamestring Permission identifier to delete returnsobject Empty response on success MessageShape(typeof EmptySchema) ```typescript await scalekit.permission.deletePermission('deprecated:feature'); ``` ### listRolePermissions [Section titled “listRolePermissions”](#listrolepermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#listRolePermissions)asynclistRolePermissions Lists direct permissions assigned to a role (excluding inherited permissions). paramroleNamestring Role to examine returnsListRolePermissionsResponse List of directly assigned permissions only ```typescript const response = await scalekit.permission.listRolePermissions('editor'); console.log('Direct permissions:', response.permissions); ``` ### addPermissionsToRole [Section titled “addPermissionsToRole”](#addpermissionstorole) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#addPermissionsToRole)asyncaddPermissionsToRole Grants additional permissions to a role without removing existing assignments. paramroleNamestring Target role to enhance parampermissionNamesstring\[] Array of permission identifiers to add returnsAddPermissionsToRoleResponse Updated list of all role permissions ```typescript await scalekit.permission.addPermissionsToRole('editor', [ 'read:documents', 'write:documents', 'edit:documents' ]); ``` ### removePermissionFromRole [Section titled “removePermissionFromRole”](#removepermissionfromrole) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#removePermissionFromRole)asyncremovePermissionFromRole Revokes a specific permission from a role, restricting access for all assigned users. paramroleNamestring Role to modify parampermissionNamestring Permission to remove returnsobject Empty response on success MessageShape(typeof EmptySchema) ```typescript await scalekit.permission.removePermissionFromRole('editor', 'delete:documents'); ``` ### listEffectiveRolePermissions [Section titled “listEffectiveRolePermissions”](#listeffectiverolepermissions) clientPermissionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/permission.ts [#](#listEffectiveRolePermissions)asynclistEffectiveRolePermissions Lists all effective permissions for a role including both direct and inherited permissions. paramroleNamestring Role to analyze returnsListEffectiveRolePermissionsResponse Complete list including inherited permissions ```typescript const response = await scalekit.permission.listEffectiveRolePermissions('senior_editor'); console.log('Total effective permissions:', response.permissions.length); ``` --- # DOCUMENT BOUNDARY --- # Resource > Manage resource clients and the consents your end users grant against them Use `scalekit.resources` to manage resource clients and to read and revoke the consents your end users grant against one. A consent records that one end user allowed a specific resource client to act on their behalf. The same audit and revoke actions are available in the dashboard under [Managing MCP clients](/authenticate/mcp/managing-mcp-clients/). ### getResource [Section titled “getResource”](#getresource) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#getResource)asyncgetResource Retrieves a single resource by id. paramresourceIdstring The resource to fetch (format: `res_...`). returnsGetResourceResponse Resource object. ```typescript const res = await scalekit.resources.getResource('res_xxx'); console.log(res.resource); ``` ### listResources [Section titled “listResources”](#listresources) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#listResources)asynclistResources Lists resources of a given type in the environment, with pagination. paramresourceTypeResourceType The resource type to filter by. Supported value: `ResourceType.MCP_SERVER`. paramoptionsListResourcesOptions Optional fields: `pageSize` (max 30), `pageToken`. pageSize, pageToken returnsListResourcesResponse Paginated resources. ```typescript import { ResourceType } from '@scalekit-sdk/node'; const res = await scalekit.resources.listResources(ResourceType.MCP_SERVER, { pageSize: 20, }); for (const resource of res.resources) { console.log(resource.id, resource.scopes); } ``` ### createResourceClient [Section titled “createResourceClient”](#createresourceclient) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#createResourceClient)asynccreateResourceClient Creates a resource client. Returns the created `client` and a `plainSecret` - the plaintext client secret, only available at creation time. paramresourceIdstring The resource to create the client for (format: `res_...`). paramoptionsCreateResourceClientOptions Optional client properties. `name` defaults to “Resource Client” if omitted. `scopes` should be the same or a subset of the scopes available for the resource. `customClaims` is a flat JSON structure only. `expiry` (access token lifetime in seconds) defaults to the resource’s configured expiry. `redirectUris` are the allowed redirect URIs for a pre-registered client. name, description, scopes, customClaims, expiry, redirectUris returnsCreateResourceClientResponse The created client and its plaintext secret. ```typescript const resResource = await scalekit.resources.getResource('res_xxx'); const allowedScopes = resResource.resource?.scopes.filter((s) => s.enabled).map((s) => s.name); const res = await scalekit.resources.createResourceClient('res_xxx', { name: 'My Resource Client', scopes: allowedScopes, }); console.log(res.client?.clientId); // Store res.plainSecret in your secret manager now - it is never returned again. // It grants full access as this client, so if it leaks, replace it right away: // create a new secret and delete the compromised one (delete first if you're // already at your secret limit; if it's your only secret, raise the limit // before rotating). ``` ### getResourceClient [Section titled “getResourceClient”](#getresourceclient) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#getResourceClient)asyncgetResourceClient Fetches a single resource client, along with the end-users who have granted it consent. paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID (format: `m2m_...`). returnsGetResourceClientResponse The resource client. ```typescript const res = await scalekit.resources.getResourceClient('res_xxx', 'm2m_xxx'); console.log(res.client?.name); ``` ### listResourceClients [Section titled “listResourceClients”](#listresourceclients) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#listResourceClients)asynclistResourceClients Lists resource clients. paramresourceIdstring The resource whose clients to list (format: `res_...`). returnsListResourceClientsResponse The resource’s clients, plus `totalDcrClients` and `totalStaticClients` counts. ```typescript const res = await scalekit.resources.listResourceClients('res_xxx'); console.log(res.totalDcrClients, res.totalStaticClients); for (const c of res.clients) { console.log(c.clientId, c.name); } ``` ### updateResourceClient [Section titled “updateResourceClient”](#updateresourceclient) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#updateResourceClient)asyncupdateResourceClient Updates a resource client. paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID to update (format: `m2m_...`). paramoptionsUpdateResourceClientOptions Fields to update - only fields present are changed. `name`/`description` are a no-op server-side when passed as an empty string, not a clear. `scopes`, `customClaims`, and `redirectUris` replace their existing values; pass an empty value (`[]` or `{}`) to clear one of them. name, description, scopes, customClaims, expiry, redirectUris returnsUpdateResourceClientResponse The updated client. ```typescript const resResource = await scalekit.resources.getResource('res_xxx'); const allowedScopes = resResource.resource?.scopes.filter((s) => s.enabled).map((s) => s.name); const res = await scalekit.resources.updateResourceClient('res_xxx', 'm2m_xxx', { name: 'Updated Name', scopes: allowedScopes, }); console.log(res.client?.name, res.client?.scopes); ``` ### deleteResourceClient [Section titled “deleteResourceClient”](#deleteresourceclient) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#deleteResourceClient)asyncdeleteResourceClient Deletes resource clients. Throws if the client is missing or scoped to a different resource. paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID to delete (format: `m2m_...`). returnsDeleteResourceClientResponse Empty response on success. ```typescript await scalekit.resources.deleteResourceClient('res_xxx', 'm2m_xxx'); ``` ### createResourceClientSecret [Section titled “createResourceClientSecret”](#createresourceclientsecret) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#createResourceClientSecret)asynccreateResourceClientSecret Creates a new secret for a resource client. Only 2 client secrets are recommended to exist at a given point in time - use `deleteResourceClientSecret` to remove an existing one first if you need more. The plaintext client secret is only ever returned here, at creation time. paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID to create a secret for (format: `m2m_...`). returnsCreateClientSecretResponse The new secret, including its plaintext value. ```typescript const res = await scalekit.resources.createResourceClientSecret('res_xxx', 'm2m_xxx'); // Store res.plainSecret in your secret manager now - it is never returned again. // It grants full access as this client, so if it leaks, replace it right away: // create a new secret and delete the compromised one (delete first if you're // already at your secret limit; if it's your only secret, raise the limit // before rotating). ``` ### deleteResourceClientSecret [Section titled “deleteResourceClientSecret”](#deleteresourceclientsecret) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#deleteResourceClientSecret)asyncdeleteResourceClientSecret Permanently deletes a secret from a resource client. A client must always keep at least 1 secret - calling this on a client’s last remaining secret throws an error. paramresourceIdstring The resource the client must belong to (format: `res_...`). paramclientIdstring The client ID the secret belongs to (format: `m2m_...`). paramsecretIdstring The secret ID to delete (format: `sks_...`). returnsEmpty Empty response on success. ```typescript await scalekit.resources.deleteResourceClientSecret('res_xxx', 'm2m_xxx', 'sks_xxx'); ``` ### listUserConsents [Section titled “listUserConsents”](#listuserconsents) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#listUserConsents)asynclistUserConsents Lists the end-user consents granted against a resource, with pagination. Use this to audit who authorized a client, and to find the `consentId` you need before revoking. Filter by user in one of two ways. Pass `userIds` to match external user IDs exactly and case-sensitively. Pass `search` for a case-insensitive substring match. When you give both, `userIds` wins and `search` is ignored. paramresourceIdstring The resource whose consents to list (format: `res_...`). paramoptionsListUserConsentsOptions Optional fields: `search`, `pageSize` (max 30), `pageToken`, `userIds` (max 25, takes precedence over `search`). search, pageSize, pageToken, userIds returnsListResourceUserConsentsResponse Consents with `id`, `externalUserId`, `clientId`, `clientName`, `scopes`, and `grantedAt`, plus `totalSize` and the `nextPageToken` / `prevPageToken` cursors. ```typescript const res = await scalekit.resources.listUserConsents('res_xxx', { pageSize: 20, userIds: ['user_456'], // optional; takes precedence over search }); console.log(res.totalSize, res.nextPageToken); for (const consent of res.consents) { console.log(consent.id, consent.externalUserId, consent.clientId, consent.scopes); } ``` ### revokeUserConsent [Section titled “revokeUserConsent”](#revokeuserconsent) classResourceClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/resource.ts [#](#revokeUserConsent)asyncrevokeUserConsent Revokes a single end-user consent held by a resource client. The client is prompted for consent again on its next authorization attempt, and every active refresh token issued to that client for the same user is revoked. Access tokens that Scalekit already issued stay valid until they expire. See [How revocation affects active access tokens](/authenticate/mcp/managing-mcp-clients/#how-revocation-affects-active-access-tokens) for ways to shorten that window. paramclientIdstring The resource client that holds the consent (format: `m2m_...`), not the resource ID. paramconsentIdstring The consent to revoke (format: `usrcnst_...`), taken from `listUserConsents`. returnsRevokeUserConsentResponse Empty response on success. The call throws on failure. ```typescript await scalekit.resources.revokeUserConsent('m2m_xxx', 'usrcnst_789'); ``` --- # DOCUMENT BOUNDARY --- # Roles > Define and assign organization roles Use the `role` client to define organization roles and list role assignments. Create roles and permissions for authorization after you have organizations and users. ### createRole [Section titled “createRole”](#createrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#createRole)asynccreateRole Creates a new environment-level role with specified permissions. paramroleCreateRole Role creation object containing: returnsCreateRoleResponse The created role with metadata ```typescript const response = await scalekit.role.createRole({ name: 'content_editor', displayName: 'Content Editor', description: 'Can create and edit content', permissions: ['content:read', 'content:write', 'content:edit'] }); ``` ### getRole [Section titled “getRole”](#getrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#getRole)asyncgetRole Retrieves complete information for a specific environment role. paramroleNamestring Unique role identifier (alphanumeric with underscores, hyphens allowed). returnsGetRoleResponse Role details including permissions and inheritance ```typescript const response = await scalekit.role.getRole('content_editor'); console.log('Role:', response.role.displayName); console.log('Permissions:', response.role.permissions); ``` ### listRoles [Section titled “listRoles”](#listroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#listRoles)asynclistRoles Lists all environment-level roles available in your Scalekit environment. returnsListRolesResponse Array of all roles with their metadata ```typescript const response = await scalekit.role.listRoles(); response.roles.forEach(role => { console.log(`${role.displayName}: ${role.permissions.length} permissions`); }); ``` ### updateRole [Section titled “updateRole”](#updaterole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#updateRole)asyncupdateRole Updates an existing environment role’s properties and permissions. paramroleNamestring Role to update paramroleUpdateRole Updated role properties returnsUpdateRoleResponse Updated role details ```typescript await scalekit.role.updateRole('content_editor', { displayName: 'Content Editor (Updated)', permissions: ['content:read', 'content:write', 'content:edit', 'content:review'] }); ``` ### deleteRole [Section titled “deleteRole”](#deleterole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#deleteRole)asyncdeleteRole Deletes an environment role and reassigns its users to another role. paramroleNamestring Role to delete paramreassignRoleNamestring Optional. Target role for user migration returnsobject Empty response on success MessageShape(typeof EmptySchema) ```typescript await scalekit.role.deleteRole('old_role', 'new_role'); ``` ### deleteRoleBase [Section titled “deleteRoleBase”](#deleterolebase) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#deleteRoleBase)asyncdeleteRoleBase Removes an environment role’s inheritance relationship, eliminating inherited permissions from its base role. paramroleNamestring Role to remove inheritance from returnsobject Empty response on success MessageShape(typeof EmptySchema) ### getRoleUsersCount [Section titled “getRoleUsersCount”](#getroleuserscount) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#getRoleUsersCount)asyncgetRoleUsersCount Gets the number of users assigned to an environment role. paramroleNamestring Role to count users for returnsGetRoleUsersCountResponse Total user count (direct and inherited) ```typescript const response = await scalekit.role.getRoleUsersCount('admin'); console.log(`${response.count} users have admin role`); ``` ### createOrganizationRole [Section titled “createOrganizationRole”](#createorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#createOrganizationRole)asynccreateOrganizationRole Creates a new organization-specific role with custom permissions. paramorgIdstring Organization identifier (format: “org\_…”) paramroleCreateOrganizationRole Role configuration containing: returnsCreateOrganizationRoleResponse Created organization role ```typescript await scalekit.role.createOrganizationRole('org_123456', { name: 'department_lead', displayName: 'Department Lead', description: 'Manages department members', permissions: ['dept:members:read', 'dept:members:invite'] }); ``` ### getOrganizationRole [Section titled “getOrganizationRole”](#getorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#getOrganizationRole)asyncgetOrganizationRole Retrieves details for a specific organization role. paramorgIdstring Organization identifier paramroleNamestring Role name to retrieve returnsGetOrganizationRoleResponse Organization role details ```typescript const response = await scalekit.role.getOrganizationRole( 'org_123456', 'department_lead' ); console.log('Permissions:', response.role.permissions); ``` ### listOrganizationRoles [Section titled “listOrganizationRoles”](#listorganizationroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#listOrganizationRoles)asynclistOrganizationRoles Lists all roles available to an organization (environment + organization-specific). paramorgIdstring Organization identifier returnsListOrganizationRolesResponse Array of available roles ```typescript const response = await scalekit.role.listOrganizationRoles('org_123456'); response.roles.forEach(role => { console.log(`${role.displayName} (${role.scope})`); }); ``` ### updateOrganizationRole [Section titled “updateOrganizationRole”](#updateorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#updateOrganizationRole)asyncupdateOrganizationRole Updates an organization role’s properties and permissions. paramorgIdstring Organization identifier paramroleNamestring Role to update paramroleUpdateRole Updated role properties returnsUpdateOrganizationRoleResponse Updated role ```typescript await scalekit.role.updateOrganizationRole('org_123456', 'department_lead', { permissions: ['dept:members:read', 'dept:members:invite', 'dept:members:remove'] }); ``` ### deleteOrganizationRole [Section titled “deleteOrganizationRole”](#deleteorganizationrole) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#deleteOrganizationRole)asyncdeleteOrganizationRole Deletes an organization role and reassigns its users. paramorgIdstring Organization identifier paramroleNamestring Role to delete paramreassignRoleNamestring Optional. Target role for user migration returnsobject Empty response on success MessageShape(typeof EmptySchema) ```typescript await scalekit.role.deleteOrganizationRole( 'org_123456', 'old_role', 'new_role' ); ``` ### getOrganizationRoleUsersCount [Section titled “getOrganizationRoleUsersCount”](#getorganizationroleuserscount) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#getOrganizationRoleUsersCount)asyncgetOrganizationRoleUsersCount Gets the number of users assigned to an organization role. paramorgIdstring Organization identifier paramroleNamestring Role to count users for returnsGetOrganizationRoleUsersCountResponse User count ```typescript const response = await scalekit.role.getOrganizationRoleUsersCount( 'org_123456', 'admin' ); console.log(`${response.count} admins in this organization`); ``` ### updateDefaultRoles [Section titled “updateDefaultRoles”](#updatedefaultroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#updateDefaultRoles)asyncupdateDefaultRoles Sets the default creator and/or member roles for the environment. paramoptionsobject Optional fields: `defaultCreatorRole`, `defaultMemberRole`. defaultCreatorRole, defaultMemberRole returnsUpdateDefaultRolesResponse Updated default roles configuration ### listDependentRoles [Section titled “listDependentRoles”](#listdependentroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#listDependentRoles)asynclistDependentRoles Lists all roles that extend (depend on) the specified role. paramroleNamestring Role to find dependents for returnsListDependentRolesResponse Array of roles that depend on the given role ### updateDefaultOrganizationRoles [Section titled “updateDefaultOrganizationRoles”](#updatedefaultorganizationroles) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#updateDefaultOrganizationRoles)asyncupdateDefaultOrganizationRoles Sets the default role automatically assigned to new organization members. paramorgIdstring Organization identifier paramdefaultMemberRolestring Role name to assign by default returnsUpdateDefaultOrganizationRolesResponse Updated configuration ```typescript await scalekit.role.updateDefaultOrganizationRoles('org_123456', 'member'); ``` ### deleteOrganizationRoleBase [Section titled “deleteOrganizationRoleBase”](#deleteorganizationrolebase) clientRoleshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/role.ts [#](#deleteOrganizationRoleBase)asyncdeleteOrganizationRoleBase Removes a role’s inheritance relationship, eliminating inherited permissions from base role. paramorgIdstring Organization identifier paramroleNamestring Role to remove inheritance from returnsobject Empty response on success MessageShape(typeof EmptySchema) ```typescript await scalekit.role.deleteOrganizationRoleBase('org_123456', 'custom_role'); ``` --- # DOCUMENT BOUNDARY --- # Create client > Create the SDK client and run core auth helpers Create a `ScalekitClient` with your environment URL, client ID, and client secret. All other clients hang off this instance. Store credentials in environment variables. Never hard-code secrets. After you create the client, use [Sessions](/saaskit/sdks/node/sessions/) to validate tokens or open a domain client such as [Organizations](/saaskit/sdks/node/organizations/) or [Users](/saaskit/sdks/node/users/). ### validateAccessToken [Section titled “validateAccessToken”](#validateaccesstoken) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/scalekit.ts [#](#validateAccessToken)asyncvalidateAccessToken Validates the access token and returns a boolean result. paramtokenstring The token to be validated. paramoptionsTokenValidationOptions Optional validation settings. `issuer` accepts a string or an array; the token is valid if its `iss` claim equals any entry. returnsboolean Returns true if the token is valid, false otherwise. ```typescript const isValid = await scalekit.validateAccessToken(accessToken); // Accept more than one issuer, for example the environment URL and the server's own issuer const isValidForServer = await scalekit.validateAccessToken(accessToken, { issuer: [envUrl, `${envUrl}/resources/${resourceId}`], audience: [expectedAudience], }); ``` ### generateClientToken [Section titled “generateClientToken”](#generateclienttoken) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/scalekit.ts [#](#generateClientToken)asyncgenerateClientToken Generates an M2M access token using the client credentials grant for the given clientId and clientSecret. paramclientIdstring The client ID to authenticate with paramclientSecretstring The client secret to authenticate with returnsstring The access token string ### authenticateWithCode [Section titled “authenticateWithCode”](#authenticatewithcode) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/scalekit.ts [#](#authenticateWithCode)asyncauthenticateWithCode Exchanges an authorization code for access tokens and user information. paramcodestring The authorization code received in the callback URL after. paramredirectUristring The same redirect URI used in getAuthorizationUrl(). paramoptionsAuthenticationOptions Optional request settings. returnsAuthenticationResponse Tokens and user claims. ```typescript app.get('/auth/callback', async (req, res) => { const { code } = req.query; const result = await scalekit.authenticateWithCode( code, 'https://yourapp.com/auth/callback' ); req.session.accessToken = result.accessToken; req.session.user = result.user; res.redirect('/dashboard'); }); ``` ### getIdpInitiatedLoginClaims [Section titled “getIdpInitiatedLoginClaims”](#getidpinitiatedloginclaims) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/scalekit.ts [#](#getIdpInitiatedLoginClaims)asyncgetIdpInitiatedLoginClaims Extracts and validates claims from an IdP-initiated login token. paramidpInitiatedLoginTokenstring The token received in the ‘idp_initiated_login’ query. paramoptionsTokenValidationOptions Optional request settings. returnsIdpInitiatedLoginClaims The connection id. ```typescript app.get('/auth/callback', async (req, res) => { const { idp_initiated_login } = req.query; if (idp_initiated_login) { const claims = await scalekit.getIdpInitiatedLoginClaims(idp_initiated_login); const authUrl = scalekit.getAuthorizationUrl( 'https://yourapp.com/auth/callback', { connectionId: claims.connection_id, organizationId: claims.organization_id, loginHint: claims.login_hint, ...(claims.relay_state && { state: claims.relay_state }) } ); return res.redirect(authUrl); } }); ``` ### refreshAccessToken [Section titled “refreshAccessToken”](#refreshaccesstoken) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/scalekit.ts [#](#refreshAccessToken)asyncrefreshAccessToken Obtains a new access token using a refresh token. paramrefreshTokenstring The refresh token obtained from a previous authentication returnsRefreshTokenResponse Tokens and claims. ```typescript const result = await scalekit.refreshAccessToken(oldRefreshToken); // Store the new tokens await storeTokens(userId, { accessToken: result.accessToken, refreshToken: result.refreshToken }); ``` --- # DOCUMENT BOUNDARY --- # Sessions > Validate tokens and manage user sessions Use the `session` client to inspect session metadata and revoke sessions when users sign out or you detect risk. Typical flows: load a session by ID after authentication, list a user’s sessions for a security UI, and revoke one session or all of a user’s sessions on logout. ### getSession [Section titled “getSession”](#getsession) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/session.ts [#](#getSession)asyncgetSession Retrieves comprehensive metadata and status for a specific user session. paramsessionIdstring The session identifier to retrieve (format: “ses\_…”) returnsSessionDetails Session metadata including status, user, device, and location. ```typescript const session = await scalekit.session.getSession('ses_123456'); console.log('Status:', session.status); console.log('User:', session.userId); console.log('Device:', session.deviceInfo); console.log('Location:', session.geoLocation); ``` ### getUserSessions [Section titled “getUserSessions”](#getusersessions) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/session.ts [#](#getUserSessions)asyncgetUserSessions Runs `getUserSessions` and returns the result. paramuserIdstring Scalekit user ID (`usr_...`). paramoptionsobject Optional fields: `pageSize`, `pageToken`, `filter`, `status`, `startTime`, `endTime`. pageSize, pageToken, filter, status, startTime, endTime returnsUserSessionDetails The response payload for this operation. ```typescript // pageSize: Results per page (default 10, max 100) // pageToken: Token for the next page // filter: Scoped tool filter // status: status // startTime: start time // endTime: end time const response = await scalekit.session.getUserSessions('usr_123456', { filter: { status: ['active'] }, pageSize: 20 }); console.log(`User has ${response.totalSize} active sessions`); ``` ### revokeSession [Section titled “revokeSession”](#revokesession) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/session.ts [#](#revokeSession)asyncrevokeSession Runs `revokeSession` and returns the result. paramsessionIdstring Session ID. returnsRevokeSessionResponse Empty on success. ```typescript const response = await scalekit.session.revokeSession('ses_123456'); console.log('Session revoked at:', response.revokedAt); ``` ### revokeAllUserSessions [Section titled “revokeAllUserSessions”](#revokeallusersessions) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/session.ts [#](#revokeAllUserSessions)asyncrevokeAllUserSessions Immediately invalidates all active sessions for a user across all devices and browsers. paramuserIdstring The user identifier whose sessions should be revoked. returnsRevokeAllUserSessionsResponse Empty on success. ```typescript const response = await scalekit.session.revokeAllUserSessions('usr_123456'); console.log(`Revoked ${response.totalCount} sessions`); ``` --- # DOCUMENT BOUNDARY --- # Users > Provision and manage users and memberships Use the `user` client to provision people, manage organization memberships, and assign roles. Common flows: invite or create a user, attach them to an organization, update profile fields, and look up users by Scalekit ID or your external ID. ### createUserAndMembership [Section titled “createUserAndMembership”](#createuserandmembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#createUserAndMembership)asynccreateUserAndMembership Creates a new user and adds them as a member of an organization in a single operation. paramorganizationIdstring The organization ID to add the user to paramoptionsCreateUserRequest Optional request settings. returnsCreateUserAndMembershipResponse The created resource. ```typescript const response = await scalekit.user.createUserAndMembership( 'org_123456', { email: 'john.doe@company.com', userProfile: { firstName: 'John', lastName: 'Doe' }, sendInvitationEmail: true, metadata: { department: 'Engineering' } } ); console.log('User created:', response.user.id); ``` ### getUser [Section titled “getUser”](#getuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#getUser)asyncgetUser Retrieves comprehensive details about a specific user including their profile and memberships. paramuserIdstring Scalekit user ID (`usr_...`). returnsGetUserResponse The user. ```typescript const response = await scalekit.user.getUser('usr_123456'); const user = response.user; console.log('User:', user.email); console.log('Name:', user.userProfile?.firstName, user.userProfile?.lastName); console.log('Organizations:', user.memberships?.length); ``` ### listUsers [Section titled “listUsers”](#listusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#listUsers)asynclistUsers Retrieves a paginated list of all users across your Scalekit environment. paramoptionsobject Optional fields: `pageSize`, `pageToken`. pageSize, pageToken returnsListUsersResponse Paginated users. ```typescript // pageSize: Results per page (default 10, max 100) // pageToken: Token for the next page const response = await scalekit.user.listUsers({ pageSize: 20 }); console.log('Users:', response.users.length); console.log('Total users:', response.totalSize); ``` ### updateUser [Section titled “updateUser”](#updateuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#updateUser)asyncupdateUser Updates a user’s profile information and custom metadata. paramuserIdstring The Scalekit user identifier (format: “usr\_…”) paramoptionsUpdateUserRequestType Fields to update. returnsUpdateUserResponse The updated user. ```typescript // metadata: Custom key-value metadata const response = await scalekit.user.updateUser('usr_123456', { userProfile: { firstName: 'John', lastName: 'Smith' }, metadata: { department: 'Engineering', title: 'Senior Developer' } }); ``` ### deleteUser [Section titled “deleteUser”](#deleteuser) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#deleteUser)asyncdeleteUser Permanently deletes a user from your Scalekit environment. paramuserIdstring Scalekit user ID (`usr_...`). returnsobject Empty response on successful deletion MessageShape(typeof EmptySchema) ```typescript await scalekit.user.deleteUser('usr_123456'); console.log('User deleted successfully'); ``` ### createMembership [Section titled “createMembership”](#createmembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#createMembership)asynccreateMembership Adds an existing user as a member of a new organization with specified roles. paramorganizationIdstring Organization ID. paramuserIdstring The user ID to add as a member (format: “usr\_…”) paramoptionsobject Optional fields: `roles`, `metadata`, `sendInvitationEmail`. roles, metadata, sendInvitationEmail returnsCreateMembershipResponse The created resource. ```typescript // roles: Assigned roles // sendInvitationEmail: send invitation email const response = await scalekit.user.createMembership( 'org_123456', 'usr_789012', { roles: ['admin'], sendInvitationEmail: true } ); ``` ### deleteMembership [Section titled “deleteMembership”](#deletemembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#deleteMembership)asyncdeleteMembership Removes a user’s membership from a specific organization. paramorganizationIdstring Organization ID. paramuserIdstring The user ID to remove (format: “usr\_…”) returnsobject Empty response on successful removal MessageShape(typeof EmptySchema) ```typescript await scalekit.user.deleteMembership('org_123456', 'usr_789012'); console.log('User removed from organization'); ``` ### updateMembership [Section titled “updateMembership”](#updatemembership) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#updateMembership)asyncupdateMembership Updates a user’s roles and metadata within a specific organization. paramorganizationIdstring Organization ID. paramuserIdstring Scalekit user ID (`usr_...`). paramoptionsobject Optional fields: `roles`, `metadata`. roles, metadata returnsUpdateMembershipResponse The updated resource. ```typescript // roles: Assigned roles const response = await scalekit.user.updateMembership( 'org_123456', 'usr_789012', { roles: ['admin'] } ); ``` ### listOrganizationUsers [Section titled “listOrganizationUsers”](#listorganizationusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#listOrganizationUsers)asynclistOrganizationUsers Retrieves a paginated list of all users who are members of a specific organization. paramorganizationIdstring Organization ID. paramoptionsobject Optional fields: `pageSize`, `pageToken`. pageSize, pageToken returnsListOrganizationUsersResponse Paginated users. ```typescript // pageSize: Results per page (default 10, max 100) // pageToken: Token for the next page const response = await scalekit.user.listOrganizationUsers('org_123456', { pageSize: 25 }); console.log('Organization users:', response.users.length); console.log('Total members:', response.totalSize); ``` ### resendInvite [Section titled “resendInvite”](#resendinvite) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#resendInvite)asyncresendInvite Resends an invitation email to a user for a specific organization. paramorganizationIdstring The organization ID for which to resend the invite. paramuserIdstring Scalekit user ID (`usr_...`). returnsResendInviteResponse Resend invite. ```typescript const response = await scalekit.user.resendInvite('org_123456', 'usr_789012'); console.log('Invitation resent:', response.invite); ``` ### listUserRoles [Section titled “listUserRoles”](#listuserroles) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#listUserRoles)asynclistUserRoles Lists all roles assigned to a user within a specific organization. paramorganizationIdstring Organization ID. paramuserIdstring The user ID whose roles to list (format: “usr\_…”) returnsListUserRolesResponse Paginated roles. ### listUserPermissions [Section titled “listUserPermissions”](#listuserpermissions) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#listUserPermissions)asynclistUserPermissions Lists all effective permissions for a user within a specific organization. paramorganizationIdstring Organization ID. paramuserIdstring Scalekit user ID (`usr_...`). returnsListUserPermissionsResponse Paginated permissions. ### searchUsers [Section titled “searchUsers”](#searchusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#searchUsers)asyncsearchUsers Searches for users matching a query string across the environment. paramquerystring Search query string parampageSizenumber Optional. Number of results per page parampageTokenstring Optional. Pagination token for the next page returnsSearchUsersResponse Paginated results. ### searchOrganizationUsers [Section titled “searchOrganizationUsers”](#searchorganizationusers) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#searchOrganizationUsers)asyncsearchOrganizationUsers Searches for users matching a query string within a specific organization. paramorganizationIdstring The organization ID to search within paramquerystring Search query string parampageSizenumber Optional. Number of results per page parampageTokenstring Optional. Pagination token for the next page returnsSearchOrganizationUsersResponse Paginated results. ### assignUserRoles [Section titled “assignUserRoles”](#assignuserroles) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#assignUserRoles)asyncassignUserRoles Assigns roles to a user within a specific organization. paramorganizationIdstring The organization ID paramuserIdstring The user ID to assign roles to paramrolesstring\[] Array of role names to assign returnsAssignUserRolesResponse Assign user roles. ### removeUserRole [Section titled “removeUserRole”](#removeuserrole) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#removeUserRole)asyncremoveUserRole Removes a specific role from a user within an organization. paramorganizationIdstring The organization ID paramuserIdstring The user ID to remove the role from paramroleNamestring The name of the role to remove returnsobject Empty response on success MessageShape(typeof EmptySchema) ### getUserByExternalId [Section titled “getUserByExternalId”](#getuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#getUserByExternalId)asyncgetUserByExternalId Retrieves a user by their external ID. paramexternalIdstring The external identifier for the user returnsGetUserResponse The user. ### updateUserByExternalId [Section titled “updateUserByExternalId”](#updateuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#updateUserByExternalId)asyncupdateUserByExternalId Updates a user identified by their external ID. paramexternalIdstring The external identifier for the user paramoptionsobject Fields to update. Omit(UpdateUserRequestType, 'externalId') returnsUpdateUserResponse The updated user. ### deleteUserByExternalId [Section titled “deleteUserByExternalId”](#deleteuserbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#deleteUserByExternalId)asyncdeleteUserByExternalId Permanently deletes a user identified by their external ID. paramexternalIdstring The external identifier for the user returnsobject Empty response on success MessageShape(typeof EmptySchema) ### createMembershipByExternalId [Section titled “createMembershipByExternalId”](#createmembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#createMembershipByExternalId)asynccreateMembershipByExternalId Adds a user identified by external ID as a member of an organization. paramorganizationIdstring The organization ID to add the user to paramexternalIdstring The external identifier for the user paramoptionsobject Optional fields: `roles`, `metadata`, `sendInvitationEmail`. roles, metadata, sendInvitationEmail returnsCreateMembershipResponse The created resource. ### deleteMembershipByExternalId [Section titled “deleteMembershipByExternalId”](#deletemembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#deleteMembershipByExternalId)asyncdeleteMembershipByExternalId Removes the membership of a user identified by external ID from an organization. paramorganizationIdstring The organization ID to remove the user from paramexternalIdstring The external identifier for the user returnsobject Empty response on success MessageShape(typeof EmptySchema) ### updateMembershipByExternalId [Section titled “updateMembershipByExternalId”](#updatemembershipbyexternalid) clientUsershttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/user.ts [#](#updateMembershipByExternalId)asyncupdateMembershipByExternalId Updates the membership of a user identified by external ID within an organization. paramorganizationIdstring The organization ID where the membership exists paramexternalIdstring The external identifier for the user paramoptionsobject Optional fields: `roles`, `metadata`. roles, metadata returnsUpdateMembershipResponse The updated resource. --- # DOCUMENT BOUNDARY --- # Passkeys > Passkey and WebAuthn credentials Use the `webauthn` client to register and authenticate passkeys (WebAuthn credentials). Build registration and authentication ceremonies with the options and verification methods below. ### listCredentials [Section titled “listCredentials”](#listcredentials) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/webauthn.ts [#](#listCredentials)asynclistCredentials List all WebAuthn credentials for a user paramuserIdstring The user ID to list credentials for returnsListCredentialsResponse Paginated credentials. ```typescript const response = await scalekit.webauthn.listCredentials('usr_123456'); console.log('Credentials:', response.credentials); ``` ### updateCredential [Section titled “updateCredential”](#updatecredential) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/webauthn.ts [#](#updateCredential)asyncupdateCredential Update a WebAuthn credential’s display name paramcredentialIdstring The credential ID to update paramdisplayNamestring The new display name for the credential returnsUpdateCredentialResponse The updated resource. ```typescript const response = await scalekit.webauthn.updateCredential( 'cred_123', 'My YubiKey' ); ``` ### deleteCredential [Section titled “deleteCredential”](#deletecredential) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-node/blob/main/src/webauthn.ts [#](#deleteCredential)asyncdeleteCredential Delete a WebAuthn credential paramcredentialIdstring The credential ID to delete returnsDeleteCredentialResponse Empty on success. ```typescript const response = await scalekit.webauthn.deleteCredential('cred_123'); console.log('Deleted:', response.success); ``` --- # DOCUMENT BOUNDARY --- # Authorization > Authorization helpers for protected resources Use auth helpers for authorization code exchange, token validation, and related login token flows on the Scalekit client. These methods sit next to session management—use them when implementing hosted login callbacks and token checks. ### update_login_user_details [Section titled “update_login_user_details”](#update_login_user_details) clientAuthhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/auth.py [#](#update_login_user_details)asyncupdate_login_user_details Runs `update_login_user_details` and returns the result. paramconnection_idstr Connection ID. paramlogin_request_idstr Login request id. paramuserOptional\[Mapping\[str, Any]] User profile fields. returnsTuple\[UpdateLoginUserDetailsResponse, grpc.Call] Tuple of the response (carrying `auth_request_id`) and the gRPC call metadata. Use the auth request ID to look up the user’s authentication journey in the auth logs. ```python response = scalekit_client.auth.update_login_user_details( 'conn_abc123', 'login_xyz789', { 'email': 'john.doe@company.com', 'sub': 'unique_user_id_456', } ) # The auth request ID can be used to look up the authentication journey via auth logs. print(response[0].auth_request_id) ``` --- # DOCUMENT BOUNDARY --- # Connected accounts > Connected accounts for agent workflows Manage connected accounts for agent tool access (AgentKit-style connections from the Python SaaSKit surface). Prefer the AgentKit SDK docs when building agent product flows; use these methods when integrating from the Python SaaS client. ### list_connected_accounts [Section titled “list_connected_accounts”](#list_connected_accounts) clientConnectedAccountshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connected_accounts.py [#](#list_connected_accounts)asynclist_connected_accounts Runs `list_connected_accounts` and returns the result. paramorganization_idOptional\[str] Organization ID. paramuser_idOptional\[str] Scalekit user ID (`usr_...`). paramconnectorOptional\[str] Connector slug. paramidentifierOptional\[str] End-user identifier. paramproviderOptional\[str] Identity provider. parampage_sizeOptional\[int] Results per page. parampage_tokenOptional\[str] Pagination token. paramconnection_namesOptional\[List\[str]] Connector slug filter. returnsListConnectedAccountsResponse Paginated connected accounts. ```python response = scalekit_client.connected_accounts.list_connected_accounts( organization_id='org_123456', user_id='usr_123456', page_size=50 ) for account in response[0].connected_accounts: print(f'Account: {account.id}') ``` ### get_connected_account_by_identifier [Section titled “get_connected_account_by_identifier”](#get_connected_account_by_identifier) clientConnectedAccountshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connected_accounts.py [#](#get_connected_account_by_identifier)asyncget_connected_account_by_identifier Method to get connected account by identifier paramconnectorstr Connector identifier paramidentifierstr Identifier for the connector paramorganization_idOptional\[str] Optional. Organization ID paramuser_idOptional\[str] Optional. User ID paramconnected_account_idOptional\[str] Optional. ID of the connected account returnsGetConnectedAccountByIdentifierResponse Get Connected Account By Identifier Response ```python response = scalekit_client.connected_accounts.get_connected_account_by_identifier( 'slack', 'workspace_id', organization_id='org_123456', user_id='usr_123456' ) ``` ### create_connected_account [Section titled “create_connected_account”](#create_connected_account) clientConnectedAccountshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connected_accounts.py [#](#create_connected_account)asynccreate_connected_account Method to create a connected account paramconnectorstr Connector identifier paramidentifierstr Identifier for the connector paramconnected_accountCreateConnectedAccount Connected account details paramorganization_idOptional\[str] Optional. Organization ID paramuser_idOptional\[str] Optional. User ID returnsCreateConnectedAccountResponse Create Connected Account Response ```python from scalekit.v1.connected_accounts.connected_accounts_pb2 import CreateConnectedAccount account = CreateConnectedAccount() response = scalekit_client.connected_accounts.create_connected_account( 'slack', 'workspace_id', account, organization_id='org_123456', user_id='usr_123456' ) ``` ### update_connected_account [Section titled “update_connected_account”](#update_connected_account) clientConnectedAccountshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connected_accounts.py [#](#update_connected_account)asyncupdate_connected_account Method to update a connected account paramconnectorstr Connector identifier paramidentifierstr Identifier for the connector paramconnected_accountUpdateConnectedAccount Updated connected account details paramorganization_idOptional\[str] Optional. Organization ID paramuser_idOptional\[str] Optional. User ID paramconnected_account_idOptional\[str] Optional. ID of the connected account to update returnsUpdateConnectedAccountResponse Update Connected Account Response ```python from scalekit.v1.connected_accounts.connected_accounts_pb2 import UpdateConnectedAccount account = UpdateConnectedAccount() response = scalekit_client.connected_accounts.update_connected_account( 'slack', 'workspace_id', account, organization_id='org_123456', user_id='usr_123456' ) ``` ### delete_connected_account [Section titled “delete_connected_account”](#delete_connected_account) clientConnectedAccountshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connected_accounts.py [#](#delete_connected_account)asyncdelete_connected_account Method to delete a connected account paramconnectorstr Connector identifier paramidentifierstr Identifier for the connector paramorganization_idOptional\[str] Optional. Organization ID paramuser_idOptional\[str] Optional. User ID paramconnected_account_idOptional\[str] Optional. ID of the connected account to delete returnsDeleteConnectedAccountResponse Delete Connected Account Response ```python scalekit_client.connected_accounts.delete_connected_account( 'slack', 'workspace_id', organization_id='org_123456', user_id='usr_123456' ) ``` ### get_magic_link_for_connected_account [Section titled “get_magic_link_for_connected_account”](#get_magic_link_for_connected_account) clientConnectedAccountshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connected_accounts.py [#](#get_magic_link_for_connected_account)asyncget_magic_link_for_connected_account Runs `get_magic_link_for_connected_account` and returns the result. paramconnectorstr Connector. paramidentifierstr Identifier. paramorganization_idOptional\[str] Organization ID. paramuser_idOptional\[str] Scalekit user ID (`usr_...`). paramconnected_account_idOptional\[str] Connected account ID. paramstateOptional\[str] Opaque state string. paramuser_verify_urlOptional\[str] Post-auth verify URL. returnsGetMagicLinkForConnectedAccountResponse Authorization magic link. ```python response = scalekit_client.connected_accounts.get_magic_link_for_connected_account( 'slack', 'workspace_id', organization_id='org_123456', user_id='usr_123456' ) print(f'Magic Link: {response[0].magic_link}') ``` ### get_connected_account_details_by_identifier [Section titled “get_connected_account_details_by_identifier”](#get_connected_account_details_by_identifier) clientConnectedAccountshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connected_accounts.py [#](#get_connected_account_details_by_identifier)asyncget_connected_account_details_by_identifier Method to get connected account details (without auth credentials) by identifier paramconnectorstr Connector identifier paramidentifierstr Identifier for the connector paramorganization_idOptional\[str] Optional. Organization ID paramuser_idOptional\[str] Optional. User ID paramconnected_account_idOptional\[str] Optional. ID of the connected account returnsGetConnectedAccountByIdentifierResponse The response payload for this operation. ### verify_connected_account_user [Section titled “verify_connected_account_user”](#verify_connected_account_user) clientConnectedAccountshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connected_accounts.py [#](#verify_connected_account_user)asyncverify_connected_account_user Method to verify a connected account user after OAuth callback paramauth_request_idstr Auth request ID as base64url-encoded opaque token from the paramidentifierstr Current logged in user’s connected account identifier returnsVerifyConnectedAccountUserResponse Verify Connected Account User Response --- # DOCUMENT BOUNDARY --- # Connections > Configure enterprise SSO connections Use `scalekit_client.connection` to configure enterprise SSO (SAML/OIDC) for an organization. Enable SSO after the organization exists: create a connection, complete IdP setup, and test sign-in for that org. ### list_connections [Section titled “list_connections”](#list_connections) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#list_connections)asynclist_connections Method to list connections paramorganization_idstr Client organization id to get connections paramincludeOptional\[str] Optional. Return active connections or all, E.g. - ‘all’ returnsListConnectionsResponse List Connections Response ```python response = scalekit_client.connection.list_connections( 'org_123456', include='all' ) for conn in response[0].connections: print(f"Connection: {conn.id}") ``` ### list_connections_by_domain [Section titled “list_connections_by_domain”](#list_connections_by_domain) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#list_connections_by_domain)asynclist_connections_by_domain Method to get connection object by domain paramdomainstr Client domain id to get connections paramincludeOptional\[str] Optional. Return active connections or all, E.g. - ‘all’ returnsListConnectionsResponse List Connections Response ```python response = scalekit_client.connection.list_connections_by_domain( 'acme.com', include='all' ) ``` ### get_connection [Section titled “get_connection”](#get_connection) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#get_connection)asyncget_connection Method to get connection object paramorganization_idstr Client organization id to get connection paramconn_idstr Client connection id to get connection returnsGetConnectionResponse Connection Response ```python response = scalekit_client.connection.get_connection( 'org_123456', 'conn_123456' ) connection = response[0].connection ``` ### create_connection [Section titled “create_connection”](#create_connection) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#create_connection)asynccreate_connection Method to create a new connection paramorganization_idstr Organization id to create connection for paramconnectionCreateConnection CreateConnection object with expected values for conn. returnsCreateConnectionResponse Create Connection Response ```python from scalekit.v1.connections.connections_pb2 import CreateConnection connection = CreateConnection() connection.type = "SAML" response = scalekit_client.connection.create_connection( 'org_123456', connection ) ``` ### enable_connection [Section titled “enable_connection”](#enable_connection) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#enable_connection)asyncenable_connection Method to enable connection paramorganization_idstr Client organization id to enable connection paramconn_idstr Client connection id to enable connection returnsToggleConnectionResponse Enable Connection Response ```python scalekit_client.connection.enable_connection('org_123456', 'conn_123456') ``` ### disable_connection [Section titled “disable_connection”](#disable_connection) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#disable_connection)asyncdisable_connection Method to disable connection paramorganization_idstr Client organization id to disable connection paramconn_idstr Client connection id to disable connection returnsToggleConnectionResponse Disable Connection Response ```python scalekit_client.connection.disable_connection('org_123456', 'conn_123456') ``` ### delete_connection [Section titled “delete_connection”](#delete_connection) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#delete_connection)asyncdelete_connection Method to delete a connection based in given organization and connection id paramorganization_idstr Organization id to delete connection for paramconnection_idstr Connection id of connection to be deleted returnsNone Empty on success. ```python scalekit_client.connection.delete_connection('org_123456', 'conn_123456') ``` ### list_app_connections [Section titled “list_app_connections”](#list_app_connections) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#list_app_connections)asynclist_app_connections Method to list environment-level app connections parampage_sizeOptional\[int] Results per page. parampage_tokenOptional\[str] Optional. Token for pagination paramproviderOptional\[str] Optional. Filter by provider (e.g. ‘HUBSPOT’) paramqueryOptional\[str] Optional. Free-text search matching connections by connection name (key ID) or provider (3–100 characters). returnsListAppConnectionsResponse List App Connections Response ```python response = scalekit_client.connection.list_app_connections() for conn in response[0].connections: print(f"Connection: {conn.id}, Provider: {conn.provider_key}") # Filter by provider response = scalekit_client.connection.list_app_connections(provider='HUBSPOT', page_size=10) # Search by connection name or provider response = scalekit_client.connection.list_app_connections(query='hubspot') ``` ### get_environment_connection [Section titled “get_environment_connection”](#get_environment_connection) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#get_environment_connection)asyncget_environment_connection Method to get an environment-level connection by its id paramconnection_idstr Connection id to retrieve returnsGetConnectionResponse Get Connection Response ```python response = scalekit_client.connection.get_environment_connection('conn_123456') conn = response[0].connection print(f"Provider: {conn.provider_key}, Type: {conn.type}") ``` ### create_environment_connection [Section titled “create_environment_connection”](#create_environment_connection) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#create_environment_connection)asynccreate_environment_connection Method to create a new environment-level connection paramconnectionCreateConnection CreateConnection object with expected values for conn. paramflagsOptional\[Flags] Optional. Optional Flags (is_login, is_app) returnsCreateConnectionResponse Create Connection Response ```python from scalekit.v1.connections.connections_pb2 import ( CreateConnection, ConnectionType, Flags ) # Create an OAuth app connection (e.g., HubSpot) response = scalekit_client.connection.create_environment_connection( connection=CreateConnection( provider_key='HUBSPOT', type=ConnectionType.OAUTH ), flags=Flags(is_app=True) ) conn = response[0].connection print(f"Created: {conn.id}, Key: {conn.key_id}") # Create a Google Domain-Wide Delegation connection response = scalekit_client.connection.create_environment_connection( connection=CreateConnection( provider_key='GOOGLEDWD', type=ConnectionType.GOOGLE_DWD, key_id='my-gdwd-connection' ), flags=Flags(is_app=True) ) ``` ### update_environment_connection [Section titled “update_environment_connection”](#update_environment_connection) clientConnectionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/connection.py [#](#update_environment_connection)asyncupdate_environment_connection Method to update an environment-level connection paramconnection_idstr Connection id to update paramconnectionUpdateConnection UpdateConnection object with fields to update returnsUpdateConnectionResponse Update Connection Response ```python from scalekit.v1.connections.connections_pb2 import ( UpdateConnection, ConnectionType, OAuthConnectionConfig, GoogleDWDConfig ) # Update OAuth connection credentials response = scalekit_client.connection.update_environment_connection( connection_id='conn_123456', connection=UpdateConnection( provider_key='HUBSPOT', key_id='hubspot-key', type=ConnectionType.OAUTH, oauth_config=OAuthConnectionConfig( client_id={'value': 'my-client-id'}, client_secret={'value': 'my-client-secret'} ) ) ) # Update Google DWD connection response = scalekit_client.connection.update_environment_connection( connection_id='conn_123456', connection=UpdateConnection( provider_key='GOOGLEDWD', key_id='my-gdwd-connection', type=ConnectionType.GOOGLE_DWD, google_dwd_config=GoogleDWDConfig( service_account_json={'value': '{"type":"service_account",...}'}, scopes=['https://www.googleapis.com/auth/admin.directory.user.readonly'], token_uri={'value': 'https://oauth2.googleapis.com/token'} ) ) ) conn = response[0].connection ``` --- # DOCUMENT BOUNDARY --- # Custom OAuth > Custom OAuth provider configuration Configure custom OAuth providers when you bring your own OAuth app outside the built-in connector catalog. Use this for providers Scalekit does not ship as a first-party connector. Supply client credentials and endpoints, then connect accounts through the usual OAuth flow. ### create_custom_provider [Section titled “create_custom_provider”](#create_custom_provider) clientCustomProvidershttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/providers.py [#](#create_custom_provider)asynccreate_custom_provider Runs `create_custom_provider` and returns the result. paramdisplay_namestr Display name. paramproxy_urlstr Proxy url. paramproxy_enabledbool Proxy enabled. paramdescriptionstr Description. paramauth_patternsOptional\[List\[AuthPattern]] Auth patterns. paramicon_srcOptional\[str] Icon src. parammetadataOptional\[Dict\[str, str]] Custom key-value metadata. returnsNone The created resource. ### update_custom_provider [Section titled “update_custom_provider”](#update_custom_provider) clientCustomProvidershttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/providers.py [#](#update_custom_provider)asyncupdate_custom_provider Runs `update_custom_provider` and returns the result. paramidentifierstr Identifier. paramdisplay_namestr Display name. paramproxy_urlstr Proxy url. paramdescriptionOptional\[str] Description. paramauth_patternsOptional\[List\[AuthPattern]] Auth patterns. paramicon_srcOptional\[str] Icon src. parammetadataOptional\[Dict\[str, str]] Custom key-value metadata. returnsNone The updated resource. ### list_providers [Section titled “list_providers”](#list_providers) clientCustomProvidershttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/providers.py [#](#list_providers)asynclist_providers Runs `list_providers` and returns the result. parampage_sizeOptional\[int] Results per page. parampage_tokenOptional\[str] Pagination token. paramprovider_typeOptional\[int] Provider type. paramidentifierOptional\[str] End-user identifier. returnsNone Paginated results. ### delete_custom_provider [Section titled “delete_custom_provider”](#delete_custom_provider) clientCustomProvidershttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/providers.py [#](#delete_custom_provider)asyncdelete_custom_provider Delete a custom provider by identifier. paramidentifierstr Identifier of the custom provider to delete. Required. returnsNone Tuple of (DeleteProviderResponse proto, grpc.Call). --- # DOCUMENT BOUNDARY --- # Directory > Sync users and groups via SCIM directories Use `scalekit_client.directory` for SCIM directory sync—listing providers, users, and groups provisioned from an IdP. Use this after SSO is in place when the customer provisions users via SCIM. ### list_directories [Section titled “list_directories”](#list_directories) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#list_directories)asynclist_directories Method to list directories for given organization id paramorganization_idAny org id to fetch directory list returnsListDirectoriesResponse list of directories ```python response = scalekit_client.directory.list_directories('org_123456') for directory in response[0].directories: print(f"Directory: {directory.id}") ``` ### get_directory [Section titled “get_directory”](#get_directory) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#get_directory)asyncget_directory Method to get directory based on given organization and directory id paramorganization_idstr Organization id paramdirectory_idstr directory id returnsGetDirectoryResponse Get Directory Response ```python response = scalekit_client.directory.get_directory( 'org_123456', 'directory_123456' ) directory = response[0].directory ``` ### create_directory [Section titled “create_directory”](#create_directory) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#create_directory)asynccreate_directory Method to create directory based on given organization id paramorganization_idstr Organization id to create directory for paramdirectoryCreateDirectory CreateDirectory object with expected values for dir creation returnsCreateDirectoryResponse Create Directory Response ```python from scalekit.v1.directories.directories_pb2 import CreateDirectory directory = CreateDirectory() directory.provider = "azure" response = scalekit_client.directory.create_directory( 'org_123456', directory ) ``` ### list_directory_users [Section titled “list_directory_users”](#list_directory_users) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#list_directory_users)asynclist_directory_users Runs `list_directory_users` and returns the result. paramorganization_idstr Organization ID. paramdirectory_idstr Directory ID. parampage_sizeOptional\[int] Results per page. parampage_tokenOptional\[str] Pagination token. paraminclude_detailOptional\[bool] Include detail. paramupdated_afterOptional\[str] Updated after. returnstuple\[ListDirUsersResponse, Any] tuple\[ListDirUsersResponse, Any] ```python response = scalekit_client.directory.list_directory_users( 'org_123456', 'directory_123456', page_size=50 ) for user in response[0].users: print(f"User: {user.email}") ``` ### list_directory_groups [Section titled “list_directory_groups”](#list_directory_groups) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#list_directory_groups)asynclist_directory_groups Runs `list_directory_groups` and returns the result. paramorganization_idstr Organization ID. paramdirectory_idstr Directory ID. parampage_sizeOptional\[int] Results per page. parampage_tokenOptional\[str] Pagination token. paraminclude_detailOptional\[bool] Include detail. paramupdated_afterOptional\[str] Updated after. returnstuple\[ListDirGroupsResponse, Any] tuple\[ListDirGroupsResponse, Any] ```python response = scalekit_client.directory.list_directory_groups( 'org_123456', 'directory_123456', page_size=50 ) for group in response[0].groups: print(f"Group: {group.display_name}") ``` ### enable_directory [Section titled “enable_directory”](#enable_directory) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#enable_directory)asyncenable_directory Method to enable directory based on given organization and directory id paramorganization_idstr Organization id paramdirectory_idstr directory id returnsToggleDirectoryResponse Toggle Directory Response ```python scalekit_client.directory.enable_directory('org_123456', 'directory_123456') ``` ### disable_directory [Section titled “disable_directory”](#disable_directory) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#disable_directory)asyncdisable_directory Method to disable directory based on given organization and directory id paramorganization_idstr Organization id paramdirectory_idstr directory id returnsToggleDirectoryResponse Toggle Directory Response ```python scalekit_client.directory.disable_directory('org_123456', 'directory_123456') ``` ### delete_directory [Section titled “delete_directory”](#delete_directory) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#delete_directory)asyncdelete_directory Method to delete directory based on given organization and directory id paramorganization_idstr Organization id to delete directory for paramdirectory_idstr Directory id for directory to be deleted returnsNone Empty on success. ```python scalekit_client.directory.delete_directory('org_123456', 'directory_123456') ``` ### get_primary_directory_by_organization_id [Section titled “get_primary_directory_by_organization_id”](#get_primary_directory_by_organization_id) clientDirectoryhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/directory.py [#](#get_primary_directory_by_organization_id)asyncget_primary_directory_by_organization_id Method to get primary directory based on given organization id paramorganization_idstr Organization id returnsNone Primary directory --- # DOCUMENT BOUNDARY --- # Domains > Manage organization email domains Use `scalekit_client.domain` to manage verified email domains for an organization (SSO routing and domain-based policies). Add a domain, complete verification, then use it with SSO and organization policies. ### create_domain [Section titled “create_domain”](#create_domain) clientDomainhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/domain.py [#](#create_domain)asynccreate_domain Method to create domain paramorganization_idstr Organization id to create domain for paramdomain_namestr Domain name for new creation paramdomain_typeOptional\[Union\[str, 'DomainType']] Domain type filter. returnsCreateDomainResponse Domain Response ```python response = scalekit_client.domain.create_domain( 'org_123456', 'acme.com', domain_type='ORGANIZATION_DOMAIN' ) ``` ### list_domains [Section titled “list_domains”](#list_domains) clientDomainhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/domain.py [#](#list_domains)asynclist_domains Method to list existing domains paramorganization_idstr Organization id to list domains for paramdomain_typeOptional\[Union\[str, 'DomainType']] Domain type filter. returnsListDomainResponse List Domain Response ```python response = scalekit_client.domain.list_domains( 'org_123456', domain_type='ORGANIZATION_DOMAIN' ) for domain in response[0].domains: print(f"Domain: {domain.domain}") ``` ### get_domain [Section titled “get_domain”](#get_domain) clientDomainhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/domain.py [#](#get_domain)asyncget_domain Method to list existing domains paramorganization_idstr Organization id to list domains for paramdomain_idstr Domain name for new creation returnsGetDomainResponse Get Domain Response ```python response = scalekit_client.domain.get_domain('org_123456', 'domain_123456') domain = response[0].domain ``` ### delete_domain [Section titled “delete_domain”](#delete_domain) clientDomainhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/domain.py [#](#delete_domain)asyncdelete_domain Method to delete domain paramorganization_idstr Organization id to delete domain for paramdomain_idstr Domain id to delete returnsNone Empty on success. ```python scalekit_client.domain.delete_domain('org_123456', 'domain_123456') ``` --- # DOCUMENT BOUNDARY --- # M2M clients > Machine-to-machine clients and tokens Use the M2M client on `scalekit_client` to create and manage machine-to-machine OAuth clients and secrets for service-to-service auth. Create an M2M client, store the secret securely, then obtain tokens with client credentials. ### list_organization_clients [Section titled “list_organization_clients”](#list_organization_clients) clientM2MClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/m2m_client.py [#](#list_organization_clients)asynclist_organization_clients Method to list organization clients paramorganization_idstr Organization id to list clients for parampage_sizeOptional\[int] Optional. Page size for pagination (between 10 and 100) parampage_tokenOptional\[str] Optional. Page token for pagination returnsListOrganizationClientsResponse List Organization Clients Response ```python response = scalekit_client.m2m_client.list_organization_clients( 'org_123456', page_size=50 ) for client in response[0].clients: print(f'Client: {client.id}') ``` ### get_organization_client [Section titled “get_organization_client”](#get_organization_client) clientM2MClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/m2m_client.py [#](#get_organization_client)asyncget_organization_client Method to get organization client paramorganization_idstr organization id paramclient_idstr client id returnsGetOrganizationClientResponse The response payload for this operation. ```python response = scalekit_client.m2m_client.get_organization_client( 'org_123456', 'client_123456' ) ``` ### create_organization_client [Section titled “create_organization_client”](#create_organization_client) clientM2MClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/m2m_client.py [#](#create_organization_client)asynccreate_organization_client Method to create organization client paramorganization_idstr Organization Id to create client for paramm2m_clientOrganizationClient OrganizationClient obj with desired client properties. returnsCreateOrganizationClientResponse Create Organization Client Response ```python from scalekit.v1.clients.clients_pb2 import OrganizationClient client = OrganizationClient() client.name = "My M2M Client" response = scalekit_client.m2m_client.create_organization_client( 'org_123456', client ) ``` ### update_organization_client [Section titled “update_organization_client”](#update_organization_client) clientM2MClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/m2m_client.py [#](#update_organization_client)asyncupdate_organization_client Method to update organization client paramorganization_idstr organization id paramclient_idstr client id paramm2m_clientOrganizationClient Organization Client returnsUpdateOrganizationClientResponse Update Organization Client Response ```python from scalekit.v1.clients.clients_pb2 import OrganizationClient client = OrganizationClient() client.name = "Updated M2M Client" response = scalekit_client.m2m_client.update_organization_client( 'org_123456', 'client_123456', client ) ``` ### delete_organization_client [Section titled “delete_organization_client”](#delete_organization_client) clientM2MClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/m2m_client.py [#](#delete_organization_client)asyncdelete_organization_client Method to delete organization client paramorganization_idstr organization id paramclient_idstr client id returnsNone Empty on success. ```python scalekit_client.m2m_client.delete_organization_client( 'org_123456', 'client_123456' ) ``` ### add_organization_client_secret [Section titled “add_organization_client_secret”](#add_organization_client_secret) clientM2MClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/m2m_client.py [#](#add_organization_client_secret)asyncadd_organization_client_secret Method to add organization client secret paramorganization_idstr organization id paramclient_idstr client id returnsCreateOrganizationClientSecretResponse Create Organization Client Secret Response ```python response = scalekit_client.m2m_client.add_organization_client_secret( 'org_123456', 'client_123456' ) ``` ### remove_organization_client_secret [Section titled “remove_organization_client_secret”](#remove_organization_client_secret) clientM2MClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/m2m_client.py [#](#remove_organization_client_secret)asyncremove_organization_client_secret Method to remove organization client secret paramorganization_idstr organization id paramclient_idstr client id paramsecret_idstr secret id returnsNone Empty on success. ```python scalekit_client.m2m_client.remove_organization_client_secret( 'org_123456', 'client_123456', 'secret_123456' ) ``` --- # DOCUMENT BOUNDARY --- # Organizations > Manage your customer organizations Use `scalekit_client.organization` to create and manage customer tenants (organizations) in your Scalekit application. Start here when onboarding a new customer: create an organization, set external IDs and branding, then add users and SSO for that org. ### list_organizations [Section titled “list_organizations”](#list_organizations) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#list_organizations)asynclist_organizations Method to list organizations parampage_sizeOptional\[int] Results per page. parampage_tokenOptional\[str] Optional. page token for org list fetch returnsListOrganizationsResponse list of organizations ```python response = scalekit_client.organization.list_organizations( page_size=50, page_token='next_page_token' ) for org in response[0].organizations: print(f"Organization: {org.display_name}") ``` ### create_organization [Section titled “create_organization”](#create_organization) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#create_organization)asynccreate_organization Method to create organization based on given data paramorganizationCreateOrganization Create Organization obj with details for org creation returnsCreateOrganizationResponse Create Organization Response ```python from scalekit.v1.organizations.organizations_pb2 import CreateOrganization org = CreateOrganization() org.display_name = "Acme Corp" org.external_id = "acme_123" response = scalekit_client.organization.create_organization(org) print(f"Created organization: {response[0].organization.id}") ``` ### get_organization [Section titled “get_organization”](#get_organization) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#get_organization)asyncget_organization Method to get organization based on given org id paramorganization_idstr Organization id returnsGetOrganizationResponse Get Organization Response ```python response = scalekit_client.organization.get_organization('org_123456') organization = response[0].organization print(f"Organization: {organization.display_name}") ``` ### get_organization_by_external_id [Section titled “get_organization_by_external_id”](#get_organization_by_external_id) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#get_organization_by_external_id)asyncget_organization_by_external_id Method to get organization based on given org id paramexternal_idstr External id to fetch org details returnsNone Get Organization Response ```python response = scalekit_client.organization.get_organization_by_external_id('acme_123') organization = response[0].organization ``` ### update_organization [Section titled “update_organization”](#update_organization) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#update_organization)asyncupdate_organization Method to update organization based on given data paramorganization_idstr Organization id to update paramorganizationUpdateOrganization params for update organization operation returnsUpdateOrganizationResponse Update Organization Response ```python from scalekit.v1.organizations.organizations_pb2 import UpdateOrganization org = UpdateOrganization() org.display_name = "Acme Corporation" response = scalekit_client.organization.update_organization('org_123456', org) ``` ### delete_organization [Section titled “delete_organization”](#delete_organization) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#delete_organization)asyncdelete_organization Method to delete organization based on given org id paramorganization_idstr Organization id returnsNone Empty on success. ```python scalekit_client.organization.delete_organization('org_123456') ``` ### generate_portal_link [Section titled “generate_portal_link”](#generate_portal_link) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#generate_portal_link)asyncgenerate_portal_link Method to generate customer portal link paramorganization_idstr Organization id to fetch portal link for paramfeatures\[Feature] Optional. Feature list to generate portal link for returnsGeneratePortalLinkResponse Authorization link. ```python portal_link = scalekit_client.organization.generate_portal_link('org_123456') print(f"Portal Link: {portal_link}") ``` ### update_organization_by_external_id [Section titled “update_organization_by_external_id”](#update_organization_by_external_id) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#update_organization_by_external_id)asyncupdate_organization_by_external_id Method to update organization based on external id paramexternal_idstr External id to update org paramorganizationUpdateOrganization params for update organization operation returnsUpdateOrganizationResponse Update Organization Response ### update_organization_settings [Section titled “update_organization_settings”](#update_organization_settings) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#update_organization_settings)asyncupdate_organization_settings Method to update organization settings paramorganization_idstr Organization id for org update paramsettingsList\[Dict\[str, bool]] Organization settings returnsNone The updated resource. ### upsert_user_management_settings [Section titled “upsert_user_management_settings”](#upsert_user_management_settings) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#upsert_user_management_settings)asyncupsert_user_management_settings Upsert organization user management settings like maximum allowed users. paramorganization_idstr Organization id for update parammax_allowed_usersOptional\[int] Optional. Maximum allowed users (None to clear) returnsNone None. ### get_organization_session_policy [Section titled “get_organization_session_policy”](#get_organization_session_policy) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#get_organization_session_policy)asyncget_organization_session_policy Get the session policy for an organization. paramorganization_idstr Organization id returnsGetOrganizationSessionPolicyResponse The response payload for this operation. ### update_organization_session_policy [Section titled “update_organization_session_policy”](#update_organization_session_policy) clientOrganizationhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/organization.py [#](#update_organization_session_policy)asyncupdate_organization_session_policy Runs `update_organization_session_policy` and returns the result. paramorganization_idstr Organization ID. parampolicy_sourceSessionPolicyType Policy source. paramabsolute_session_timeoutOptional\[int] Absolute session timeout. paramabsolute_session_timeout_unitOptional\[TimeUnit] Timeout unit. paramidle_session_timeout_enabledOptional\[bool] Idle session timeout enabled. paramidle_session_timeoutOptional\[int] Idle session timeout. paramidle_session_timeout_unitOptional\[TimeUnit] Timeout unit. returnsUpdateOrganizationSessionPolicyResponse The updated resource. --- # DOCUMENT BOUNDARY --- # Magic link & OTP > Send and verify passwordless login codes Use `scalekit_client.passwordless` to send and verify email magic links or one-time codes for sign-in. Call send, then verify with the code or link token from the user’s email to complete login. ### send_passwordless_email [Section titled “send_passwordless_email”](#send_passwordless_email) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/passwordless.py [#](#send_passwordless_email)asyncsend_passwordless_email Runs `send_passwordless_email` and returns the result. paramemailstr Email address. paramtemplateOptional\[Union\[str, 'TemplateType']] Template. parammagiclink_auth_uriOptional\[str] Magiclink auth uri. paramstateOptional\[str] Opaque state string. paramexpires_inOptional\[int] Expires in. paramtemplate_variablesOptional\[Dict\[str, str]] Template variables. returnsSendPasswordlessResponse Send passwordless. ```python response = scalekit_client.passwordless.send_passwordless_email( 'user@example.com', template='SIGNIN', state='random-state', expires_in=3600 ) print(f'Auth Request ID: {response[0].auth_request_id}') ``` ### verify_passwordless_email [Section titled “verify_passwordless_email”](#verify_passwordless_email) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/passwordless.py [#](#verify_passwordless_email)asyncverify_passwordless_email Method to verify passwordless authentication paramcodeOptional\[str] Optional. The OTP code received via email paramlink_tokenOptional\[str] Optional. The token from the magic link URL paramauth_request_idOptional\[str] Optional. The authentication request ID from send request returnsVerifyPasswordLessResponse Verify Passwordless Response ```python response = scalekit_client.passwordless.verify_passwordless_email( code='123456', auth_request_id='auth_request_id' ) print(f'Email: {response[0].email}') ``` ### resend_passwordless_email [Section titled “resend_passwordless_email”](#resend_passwordless_email) clientPasswordlesshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/passwordless.py [#](#resend_passwordless_email)asyncresend_passwordless_email Method to resend passwordless authentication email paramauth_request_idstr The authentication request ID from original send request returnsSendPasswordlessResponse Send Passwordless Response ```python response = scalekit_client.passwordless.resend_passwordless_email('auth_request_id') ``` --- # DOCUMENT BOUNDARY --- # Permissions > Manage fine-grained permissions Use `scalekit_client.permissions` to define fine-grained permissions that roles can grant. Model permissions for resources in your app, then attach them to roles. ### list_permissions [Section titled “list_permissions”](#list_permissions) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#list_permissions)asynclist_permissions Method to list all permissions parampage_tokenOptional\[str] Optional. Token for pagination parampage_sizeOptional\[int] Optional. Number of permissions per page returnsListPermissionsResponse List Permissions Response ```python response = scalekit_client.permissions.list_permissions( page_size=50 ) for permission in response[0].permissions: print(f"Permission: {permission.name}") ``` ### get_permission [Section titled “get_permission”](#get_permission) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#get_permission)asyncget_permission Method to get permission by name parampermission_namestr Permission name to get permission details returnsGetPermissionResponse Get Permission Response ```python response = scalekit_client.permissions.get_permission('write:articles') permission = response[0].permission ``` ### create_permission [Section titled “create_permission”](#create_permission) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#create_permission)asynccreate_permission Method to create a new permission parampermissionCreatePermission Permission create payload. returnsCreatePermissionResponse Create Permission Response ```python from scalekit.v1.roles.roles_pb2 import CreatePermission permission = CreatePermission() permission.name = "write:articles" permission.description = "Permission to write articles" response = scalekit_client.permissions.create_permission(permission) ``` ### update_permission [Section titled “update_permission”](#update_permission) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#update_permission)asyncupdate_permission Method to update an existing permission by name parampermission_namestr Permission name to update parampermissionCreatePermission Permission create payload. returnsUpdatePermissionResponse Update Permission Response ```python from scalekit.v1.roles.roles_pb2 import CreatePermission permission = CreatePermission() permission.description = "Updated description" response = scalekit_client.permissions.update_permission( 'write:articles', permission ) ``` ### delete_permission [Section titled “delete_permission”](#delete_permission) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#delete_permission)asyncdelete_permission Method to delete permission by name parampermission_namestr Permission name to be deleted returnsNone Empty on success. ```python scalekit_client.permissions.delete_permission('write:articles') ``` ### list_role_permissions [Section titled “list_role_permissions”](#list_role_permissions) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#list_role_permissions)asynclist_role_permissions Method to list all permissions associated with a role paramrole_namestr Role name to get permissions for returnsListRolePermissionsResponse List Role Permissions Response ```python response = scalekit_client.permissions.list_role_permissions('editor') for permission in response[0].permissions: print(f"Permission: {permission.name}") ``` ### add_permissions_to_role [Section titled “add_permissions_to_role”](#add_permissions_to_role) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#add_permissions_to_role)asyncadd_permissions_to_role Method to add permissions to a role paramrole_namestr Role name to add permissions to parampermission_nameslist\[str] List of permission names to add returnsNone The created resource. ```python scalekit_client.permissions.add_permissions_to_role( 'editor', ['write:articles', 'edit:articles'] ) ``` ### remove_permission_from_role [Section titled “remove_permission_from_role”](#remove_permission_from_role) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#remove_permission_from_role)asyncremove_permission_from_role Method to remove a permission from a role paramrole_namestr Role name to remove permission from parampermission_namestr Permission name to remove returnsNone Empty on success. ```python scalekit_client.permissions.remove_permission_from_role( 'editor', 'write:articles' ) ``` ### list_effective_role_permissions [Section titled “list_effective_role_permissions”](#list_effective_role_permissions) clientPermissionhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/permissions.py [#](#list_effective_role_permissions)asynclist_effective_role_permissions Method to list all effective permissions for a role (including inherited permissions) paramrole_namestr Role name to get effective permissions for returnsListEffectiveRolePermissionsResponse List Effective Role Permissions Response ```python response = scalekit_client.permissions.list_effective_role_permissions('senior_editor') print(f'Total effective permissions: {len(response[0].permissions)}') ``` --- # DOCUMENT BOUNDARY --- # Resource > Manage resource clients and the consents your end users grant against them Use `scalekit_client.resources` to manage resource clients and to read and revoke the consents your end users grant against one. A consent records that one end user allowed a specific resource client to act on their behalf. The same audit and revoke actions are available in the dashboard under [Managing MCP clients](/authenticate/mcp/managing-mcp-clients/). ### get_resource [Section titled “get_resource”](#get_resource) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#get_resource)asyncget_resource Retrieves a single resource by id. paramresource_idstr The resource to fetch (format: `res_...`). returnsGetResourceResponse Resource object. ```python response = scalekit_client.resources.get_resource('res_xxx') print(response[0].resource) ``` ### list_resources [Section titled “list_resources”](#list_resources) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#list_resources)asynclist_resources Lists resources of a given type in the environment, with pagination. paramresource_typeResourceType The resource type to filter by. Supported value: `ResourceType.MCP_SERVER`. parampage_sizeOptional\[int] Page size for pagination (max 30). parampage_tokenOptional\[str] Page token for pagination. returnsListResourcesResponse Paginated resources. ```python from scalekit.v1.clients.clients_pb2 import ResourceType response = scalekit_client.resources.list_resources( resource_type=ResourceType.MCP_SERVER, page_size=20, ) for resource in response[0].resources: print(resource.id, resource.scopes) ``` ### create_resource_client [Section titled “create_resource_client”](#create_resource_client) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#create_resource_client)asynccreate_resource_client Creates a resource client. Returns the created `client` and a `plain_secret` - the plaintext client secret, only available at creation time. paramresource_idstr The resource to create the client for (format: `res_...`). paramclientResourceClient The client properties. `scopes` should be the same or a subset of the scopes available for the resource. `custom_claims` is a flat key/value structure only. `expiry` (access token lifetime in seconds) defaults to the resource’s configured expiry. `redirect_uris` are the allowed redirect URIs for a pre-registered client. There is no `audience` field - audience is always server-determined. name, description, scopes, custom_claims, expiry, redirect_uris returnsCreateResourceClientResponse The created client and its plaintext secret. ```python from scalekit.v1.clients.clients_pb2 import ResourceClient as ResourceClientProto res_resource = scalekit_client.resources.get_resource('res_xxx') allowed_scopes = [s.name for s in res_resource[0].resource.scopes if s.enabled] response = scalekit_client.resources.create_resource_client( 'res_xxx', ResourceClientProto(name='My Resource Client', scopes=allowed_scopes), ) print(response[0].client.client_id) # Store response[0].plain_secret in your secret manager now - it is never # returned again. It grants full access as this client, so if it leaks, # replace it right away: create a new secret and delete the compromised one # (delete first if you're already at your secret limit; if it's your only # secret, raise the limit before rotating). ``` ### get_resource_client [Section titled “get_resource_client”](#get_resource_client) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#get_resource_client)asyncget_resource_client Fetches a single resource client, along with the end-users who have granted it consent. paramresource_idstr The resource the client must belong to (format: `res_...`). paramclient_idstr The client ID (format: `m2m_...`). returnsGetResourceClientResponse The resource client. ```python response = scalekit_client.resources.get_resource_client('res_xxx', 'm2m_xxx') print(response[0].client.name) ``` ### list_resource_clients [Section titled “list_resource_clients”](#list_resource_clients) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#list_resource_clients)asynclist_resource_clients Lists resource clients. paramresource_idstr The resource whose clients to list (format: `res_...`). returnsListResourceClientsResponse The resource’s clients, plus `total_dcr_clients` and `total_static_clients` counts. ```python response = scalekit_client.resources.list_resource_clients('res_xxx') print(response[0].total_dcr_clients, response[0].total_static_clients) for c in response[0].clients: print(c.client_id, c.name) ``` ### update_resource_client [Section titled “update_resource_client”](#update_resource_client) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#update_resource_client)asyncupdate_resource_client Updates a resource client. paramresource_idstr The resource the client must belong to (format: `res_...`). paramclient_idstr The client ID to update (format: `m2m_...`). paramnameOptional\[str] Updated name, if changing it. A no-op server-side when passed as an empty string, not a clear. paramdescriptionOptional\[str] Updated description, if changing it. Same empty-string behavior as `name`. paramscopesOptional\[List\[str]] Updated scopes, if changing them. Pass `[]` (not `None`) to clear. paramcustom_claimsOptional\[List\[CustomClaim]] Updated custom claims, if changing them. Pass `[]` to clear. paramexpiryOptional\[int] Updated access token lifetime in seconds, if changing it. paramredirect_urisOptional\[List\[str]] Updated redirect URIs, if changing them. Pass `[]` to clear. returnsUpdateResourceClientResponse The updated client. Only the parameters you pass (non-`None`) are changed. There’s no `audience` parameter - audience is always server-determined. ```python res_resource = scalekit_client.resources.get_resource('res_xxx') allowed_scopes = [s.name for s in res_resource[0].resource.scopes if s.enabled] response = scalekit_client.resources.update_resource_client( 'res_xxx', 'm2m_xxx', name='Updated Name', scopes=allowed_scopes, ) print(response[0].client.name, list(response[0].client.scopes)) ``` ### delete_resource_client [Section titled “delete_resource_client”](#delete_resource_client) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#delete_resource_client)asyncdelete_resource_client Deletes resource clients. Raises if the client is missing or scoped to a different resource. paramresource_idstr The resource the client must belong to (format: `res_...`). paramclient_idstr The client ID to delete (format: `m2m_...`). returnsDeleteResourceClientResponse Empty response on success. ```python scalekit_client.resources.delete_resource_client('res_xxx', 'm2m_xxx') ``` ### create_resource_client_secret [Section titled “create_resource_client_secret”](#create_resource_client_secret) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#create_resource_client_secret)asynccreate_resource_client_secret Creates a new secret for a resource client. Only 2 client secrets are recommended to exist at a given point in time - use `delete_resource_client_secret` to remove an existing one first if you need more. The plaintext client secret is only ever returned here, at creation time. paramresource_idstr The resource the client must belong to (format: `res_...`). paramclient_idstr The client ID to create a secret for (format: `m2m_...`). returnsCreateClientSecretResponse The new secret, including its plaintext value. ```python response = scalekit_client.resources.create_resource_client_secret('res_xxx', 'm2m_xxx') # Store response[0].plain_secret in your secret manager now - it is never # returned again. It grants full access as this client, so if it leaks, # replace it right away: create a new secret and delete the compromised one # (delete first if you're already at your secret limit; if it's your only # secret, raise the limit before rotating). ``` ### delete_resource_client_secret [Section titled “delete_resource_client_secret”](#delete_resource_client_secret) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#delete_resource_client_secret)asyncdelete_resource_client_secret Permanently deletes a secret from a resource client. A client must always keep at least 1 secret - calling this on a client’s last remaining secret raises an error. paramresource_idstr The resource the client must belong to (format: `res_...`). paramclient_idstr The client ID the secret belongs to (format: `m2m_...`). paramsecret_idstr The secret ID to delete (format: `sks_...`). returnsTuple\[Empty, grpc.Call] Empty response on success. ```python scalekit_client.resources.delete_resource_client_secret('res_xxx', 'm2m_xxx', 'sks_xxx') ``` ### list_user_consents [Section titled “list_user_consents”](#list_user_consents) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#list_user_consents)asynclist_user_consents Lists the end-user consents granted against a resource, with pagination. Use this to audit who authorized a client, and to find the `consent_id` you need before revoking. Filter by user in one of two ways. Pass `user_ids` to match external user IDs exactly and case-sensitively. Pass `search` for a case-insensitive substring match. When you give both, `user_ids` wins and `search` is ignored. paramresource_idstr The resource whose consents to list (format: `res_...`). paramsearchOptional\[str] Case-insensitive substring match on external user IDs. parampage_sizeOptional\[int] Page size for pagination (max 30). parampage_tokenOptional\[str] Page token for pagination. paramuser_idsOptional\[List\[str]] Exact match on external user IDs (max 25). Takes precedence over `search`. returnsListResourceUserConsentsResponse Consents with `id`, `external_user_id`, `client_id`, `client_name`, `scopes`, and `granted_at`, plus `total_size` and the `next_page_token` / `prev_page_token` cursors. ```python response = scalekit_client.resources.list_user_consents( 'res_xxx', page_size=20, user_ids=['user_456'], # optional; takes precedence over search ) print(response[0].total_size, response[0].next_page_token) for consent in response[0].consents: print(consent.id, consent.external_user_id, consent.client_id, consent.scopes) ``` ### revoke_user_consent [Section titled “revoke_user_consent”](#revoke_user_consent) clientResourceshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/resource.py [#](#revoke_user_consent)asyncrevoke_user_consent Revokes a single end-user consent held by a resource client. The client is prompted for consent again on its next authorization attempt, and every active refresh token issued to that client for the same user is revoked. Access tokens that Scalekit already issued stay valid until they expire. See [How revocation affects active access tokens](/authenticate/mcp/managing-mcp-clients/#how-revocation-affects-active-access-tokens) for ways to shorten that window. paramclient_idstr The resource client that holds the consent (format: `m2m_...`), not the resource ID. paramconsent_idstr The consent to revoke (format: `usrcnst_...`), taken from `list_user_consents`. returnsRevokeUserConsentResponse Empty response on success. The call raises on failure. ```python scalekit_client.resources.revoke_user_consent('m2m_xxx', 'usrcnst_789') ``` --- # DOCUMENT BOUNDARY --- # Roles > Define and assign organization roles Use `scalekit_client.roles` to define organization roles and list role assignments. Create roles and permissions for authorization after you have organizations and users. ### list_roles [Section titled “list_roles”](#list_roles) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#list_roles)asynclist_roles Method to list all roles returnsListRolesResponse List Roles Response ```python response = scalekit_client.roles.list_roles() for role in response[0].roles: print(f"Role: {role.name}") ``` ### get_role [Section titled “get_role”](#get_role) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#get_role)asyncget_role Method to get role by name paramrole_namestr Role name to get role details returnsGetRoleResponse Get Role Response ```python response = scalekit_client.roles.get_role('admin') role = response[0].role ``` ### create_role [Section titled “create_role”](#create_role) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#create_role)asynccreate_role Method to create a new role paramroleCreateRole CreateRole object with expected values for role creation returnsCreateRoleResponse Create Role Response ```python from scalekit.v1.roles.roles_pb2 import CreateRole role = CreateRole() role.name = "editor" role.display_name = "Editor" response = scalekit_client.roles.create_role(role) ``` ### update_role [Section titled “update_role”](#update_role) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#update_role)asyncupdate_role Method to update an existing role by name paramrole_namestr Role name to update paramroleUpdateRole UpdateRole object with expected values for role update returnsUpdateRoleResponse Update Role Response ```python from scalekit.v1.roles.roles_pb2 import UpdateRole role = UpdateRole() role.display_name = "Senior Editor" response = scalekit_client.roles.update_role('editor', role) ``` ### delete_role [Section titled “delete_role”](#delete_role) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#delete_role)asyncdelete_role Method to delete role by name paramrole_namestr Role name to be deleted paramreassign_role_nameOptional\[str] Replacement role for users. returnsNone Empty on success. ```python scalekit_client.roles.delete_role('editor', reassign_role_name='viewer') ``` ### get_role_users_count [Section titled “get_role_users_count”](#get_role_users_count) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#get_role_users_count)asyncget_role_users_count Method to get the count of users associated with a role paramrole_namestr Role name to get user count for returnsGetRoleUsersCountResponse Get Role Users Count Response ```python response = scalekit_client.roles.get_role_users_count('admin') print(f"User count: {response[0].count}") ``` ### list_organization_roles [Section titled “list_organization_roles”](#list_organization_roles) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#list_organization_roles)asynclist_organization_roles Method to list all organization roles paramorg_idstr Organization ID returnsListOrganizationRolesResponse List Organization Roles Response ```python response = scalekit_client.roles.list_organization_roles('org_123456') for role in response[0].roles: print(f"Role: {role.name}") ``` ### create_organization_role [Section titled “create_organization_role”](#create_organization_role) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#create_organization_role)asynccreate_organization_role Method to create a new organization role paramorg_idstr Organization ID paramroleCreateOrganizationRole Role create payload. returnsCreateOrganizationRoleResponse Create Organization Role Response ```python from scalekit.v1.roles.roles_pb2 import CreateOrganizationRole role = CreateOrganizationRole() role.name = "org_admin" role.display_name = "Organization Admin" response = scalekit_client.roles.create_organization_role('org_123456', role) ``` ### get_organization_role [Section titled “get_organization_role”](#get_organization_role) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#get_organization_role)asyncget_organization_role Method to get organization role by name paramorg_idstr Organization ID paramrole_namestr Role name to get role details returnsGetOrganizationRoleResponse Get Organization Role Response ```python response = scalekit_client.roles.get_organization_role('org_123456', 'org_admin') role = response[0].role ``` ### update_organization_role [Section titled “update_organization_role”](#update_organization_role) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#update_organization_role)asyncupdate_organization_role Method to update an existing organization role by name paramorg_idstr Organization ID paramrole_namestr Role name to update paramroleUpdateRole Role update payload. returnsUpdateOrganizationRoleResponse Update Organization Role Response ```python from scalekit.v1.roles.roles_pb2 import UpdateRole role = UpdateRole() role.display_name = "Organization Administrator" response = scalekit_client.roles.update_organization_role( 'org_123456', 'org_admin', role ) ``` ### delete_organization_role [Section titled “delete_organization_role”](#delete_organization_role) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#delete_organization_role)asyncdelete_organization_role Method to delete organization role by name paramorg_idstr Organization ID paramrole_namestr Role name to be deleted paramreassign_role_nameOptional\[str] Replacement role for users. returnsNone Empty on success. ```python scalekit_client.roles.delete_organization_role( 'org_123456', 'org_admin', reassign_role_name='member' ) ``` ### delete_role_base [Section titled “delete_role_base”](#delete_role_base) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#delete_role_base)asyncdelete_role_base Method to delete the base inheritance relationship for an environment-level role paramrole_namestr Role name to remove base relationship for returnsNone Empty on success. ### get_organization_role_users_count [Section titled “get_organization_role_users_count”](#get_organization_role_users_count) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#get_organization_role_users_count)asyncget_organization_role_users_count Method to get the count of users associated with an organization role paramorg_idstr Organization ID paramrole_namestr Role name to get user count for returnsGetOrganizationRoleUsersCountResponse Get Organization Role Users Count Response ### update_default_organization_roles [Section titled “update_default_organization_roles”](#update_default_organization_roles) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#update_default_organization_roles)asyncupdate_default_organization_roles Method to update default organization roles paramorg_idstr Organization ID paramdefault_rolesUpdateDefaultOrganizationRolesRequest UpdateDefaultOrganizationRoles object with default role. returnsUpdateDefaultOrganizationRolesResponse Update Default Organization Roles Response ### delete_organization_role_base [Section titled “delete_organization_role_base”](#delete_organization_role_base) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#delete_organization_role_base)asyncdelete_organization_role_base Method to delete organization role base relationship by name paramorg_idstr Organization ID paramrole_namestr Role name to remove base relationship for returnsNone Empty on success. ### update_default_roles [Section titled “update_default_roles”](#update_default_roles) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#update_default_roles)asyncupdate_default_roles Method to update default environment-level roles paramdefault_creator_roleOptional\[str] Optional. Name of the role to set as default creator role paramdefault_member_roleOptional\[str] Optional. Name of the role to set as default member role returnsUpdateDefaultRolesResponse Update Default Roles Response ### list_dependent_roles [Section titled “list_dependent_roles”](#list_dependent_roles) clientRolehttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/role.py [#](#list_dependent_roles)asynclist_dependent_roles Method to list roles that directly extend (depend on) the specified base role paramrole_namestr Name of the base role returnsListDependentRolesResponse List Dependent Roles Response --- # DOCUMENT BOUNDARY --- # Create client > Create the SDK client and run core auth helpers Create a `ScalekitClient` with your environment URL, client ID, and client secret. All other clients hang off this instance. Store credentials in environment variables. Never hard-code secrets. After you create the client, use [Sessions](/saaskit/sdks/python/sessions/) to work with sessions or open a domain client such as [Organizations](/saaskit/sdks/python/organization/) or [Users](/saaskit/sdks/python/user/). ### get_authorization_url [Section titled “get_authorization_url”](#get_authorization_url) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#get_authorization_url)asyncget_authorization_url Method to get authorization URL paramredirect_uristr Redirect URI for SAML SSO paramoptionsAuthorizationUrlOptions | None Auth URL options object returnsNone Authorization URL ```python import secrets from scalekit import AuthorizationUrlOptions # Security: generate a per-login OAuth state and store it in the session. # Comparing the returned state on callback prevents login CSRF. options = AuthorizationUrlOptions() options.state = secrets.token_urlsafe(32) options.organization_id = 'org_123456' # request.session['oauth_state'] = options.state auth_url = scalekit_client.get_authorization_url( 'https://yourapp.com/auth/callback', options ) # Redirect user to auth_url ``` ### authenticate_with_code [Section titled “authenticate_with_code”](#authenticate_with_code) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#authenticate_with_code)asyncauthenticate_with_code Method to authenticate with code options paramcodeAny authorization_code paramredirect_uriAny Redirect URI paramoptionsCodeAuthenticationOptions CodeAuthenticationOptions Object returnsNone Tokens and claims. ```python from scalekit import ScalekitClient @app.get('/auth/callback') async def auth_callback(request): code = request.query_params.get('code') state = request.query_params.get('state') # Security: reject the callback if state does not match the session value. if state != request.session.get('oauth_state'): return {'error': 'invalid state'}, 400 result = scalekit_client.authenticate_with_code( code, 'https://yourapp.com/auth/callback' ) request.session['access_token'] = result['access_token'] request.session['user'] = result['user'] return RedirectResponse('/dashboard') ``` ### get_idp_initiated_login_claims [Section titled “get_idp_initiated_login_claims”](#get_idp_initiated_login_claims) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#get_idp_initiated_login_claims)asyncget_idp_initiated_login_claims Method to get IDP initiated login claims paramidp_initiated_login_tokenstr IDP initiated login token paramoptionsOptional\[TokenValidationOptions] Optional request settings. paramaudienceAny Token audience. returnsIdpInitiatedLoginClaims `IdpInitiatedLoginClaims` ```python @app.get('/auth/callback') async def auth_callback(request): idp_initiated_login = request.query_params.get('idp_initiated_login') if idp_initiated_login: claims = scalekit_client.get_idp_initiated_login_claims(idp_initiated_login) options = AuthorizationUrlOptions() options.connection_id = claims['connection_id'] options.organization_id = claims['organization_id'] options.login_hint = claims.get('login_hint') if claims.get('relay_state'): options.state = claims['relay_state'] auth_url = scalekit_client.get_authorization_url( 'https://yourapp.com/auth/callback', options ) return RedirectResponse(auth_url) ``` ### validate_access_token [Section titled “validate_access_token”](#validate_access_token) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#validate_access_token)asyncvalidate_access_token Method to validate access token paramtokenstr access token paramoptionsOptional\[TokenValidationOptions] Optional validation settings. `issuer` accepts a string or a list; the token is valid if its `iss` claim equals any entry. paramaudienceAny Token audience. returnsbool bool ```python is_valid = scalekit_client.validate_access_token(token) if is_valid: # Token is valid, proceed with request pass # Accept more than one issuer, for example the environment URL and the server's own issuer from scalekit.common.scalekit import TokenValidationOptions options = TokenValidationOptions( issuer=[env_url, f"{env_url}/resources/{resource_id}"], audience=[expected_audience], ) is_valid = scalekit_client.validate_access_token(token, options=options) ``` ### get_logout_url [Section titled “get_logout_url”](#get_logout_url) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#get_logout_url)asyncget_logout_url Method to get logout URL paramoptionsLogoutUrlOptions Logout URL options object returnsstr str: The logout URL ```python options = LogoutUrlOptions() options.post_logout_redirect_uri = 'https://example.com' options.state = 'some-state' logout_url = scalekit_client.get_logout_url(options) ``` ### verify_webhook_payload [Section titled “verify_webhook_payload”](#verify_webhook_payload) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#verify_webhook_payload)asyncverify_webhook_payload Method to verify webhook payload paramsecretstr Secret for webhook verification paramheadersDict\[str, str] Webhook request headers parampayloadstr | bytes Webhook payload in str or bytes returnsbool bool ```python import os @app.post('/webhooks') async def webhook_handler(request): payload = await request.body() headers = dict(request.headers) # Security: load the webhook secret from configuration — never hard-code it. # Skipping signature verification accepts forged webhook payloads as legitimate. secret = os.environ['SCALEKIT_WEBHOOK_SECRET'] is_valid = scalekit_client.verify_webhook_payload(secret, headers, payload) if not is_valid: return {'error': 'invalid signature'}, 401 # Process verified webhook return {'ok': True} ``` ### refresh_access_token [Section titled “refresh_access_token”](#refresh_access_token) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#refresh_access_token)asyncrefresh_access_token Method to refresh access token using refresh token paramrefresh_tokenstr Refresh token to get new access token returnsNone dict with access token & refresh token ```python result = scalekit_client.refresh_access_token(refresh_token) new_access_token = result['access_token'] new_refresh_token = result['refresh_token'] ``` ### generate_client_token [Section titled “generate_client_token”](#generate_client_token) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#generate_client_token)asyncgenerate_client_token Method to generate access token paramclient_idstr Client Id for access token paramclient_secretstr Client Secret for access token paramscopesOptional\[list\[str]] OAuth scopes. returnsstr access token ### get_client_access_token [Section titled “get_client_access_token”](#get_client_access_token) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#get_client_access_token)asyncget_client_access_token Method to generate an access token using the stored client credentials. returnsstr access token string ### validate_access_token_and_get_claims [Section titled “validate_access_token_and_get_claims”](#validate_access_token_and_get_claims) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#validate_access_token_and_get_claims)asyncvalidate_access_token_and_get_claims Method to validate access token and get claims paramtokenstr access token paramoptionsOptional\[TokenValidationOptions] Optional validation settings. `issuer` accepts a string or a list. paramaudienceAny Token audience. returnsDict\[str, Any] claims ### validate_token [Section titled “validate_token”](#validate_token) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#validate_token)asyncvalidate_token Method to validate token paramtokenstr token paramoptionsOptional\[TokenValidationOptions] Optional validation settings. `issuer` accepts a string or a list. paramaudienceOptional\[str] Optional. audience for validation returnsDict\[str, Any] payload ### verify_scopes [Section titled “verify_scopes”](#verify_scopes) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#verify_scopes)asyncverify_scopes Verify that the token contains the required scopes paramtokenstr The token to verify paramrequired_scopeslist\[str] The scopes that must be present in the token returnsbool bool: Returns True if all required scopes are present ### verify_interceptor_payload [Section titled “verify_interceptor_payload”](#verify_interceptor_payload) classScalekitClienthttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/client.py [#](#verify_interceptor_payload)asyncverify_interceptor_payload Method to verify interceptor payload using common verification logic paramsecretstr Secret for interceptor verification paramheadersDict\[str, str] Headers. parampayloadstr | bytes Interceptor payload in str or bytes returnsbool bool --- # DOCUMENT BOUNDARY --- # Sessions > Validate tokens and manage user sessions Use the `scalekit_client.sessions` client to inspect session metadata and revoke sessions when users sign out or you detect risk. Typical flows: load a session by ID after authentication, list a user’s sessions for a security UI, and revoke one session or all of a user’s sessions on logout. ### get_session [Section titled “get_session”](#get_session) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/sessions.py [#](#get_session)asyncget_session Method to get session details by session ID paramsession_idstr Session ID to get session details returnsSessionDetails Session Details ```python response = scalekit_client.sessions.get_session('session_123456') session = response[0] ``` ### get_user_sessions [Section titled “get_user_sessions”](#get_user_sessions) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/sessions.py [#](#get_user_sessions)asyncget_user_sessions Method to get all session details for a user with pagination and filtering support paramuser_idstr User ID to get all session details for parampage_sizeint Optional. Number of sessions to return per page parampage_tokenstr Optional. Token for pagination to get next/previous page paramfilterUserSessionFilter Optional. Filter to apply to sessions (status, time range) returnsUserSessionDetails User Session Details with pagination info ```python response = scalekit_client.sessions.get_user_sessions( 'usr_123456', page_size=50 ) for session in response[0].sessions: print(f'Session: {session.id}') ``` ### revoke_session [Section titled “revoke_session”](#revoke_session) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/sessions.py [#](#revoke_session)asyncrevoke_session Method to revoke a session for a user paramsession_idstr Session ID to revoke returnsRevokeSessionResponse Revoke Session Response ```python response = scalekit_client.sessions.revoke_session('session_123456') ``` ### revoke_all_user_sessions [Section titled “revoke_all_user_sessions”](#revoke_all_user_sessions) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/sessions.py [#](#revoke_all_user_sessions)asyncrevoke_all_user_sessions Method to revoke all sessions for a user paramuser_idstr User ID to revoke all sessions for returnsRevokeAllUserSessionsResponse Revoke All User Sessions Response ```python response = scalekit_client.sessions.revoke_all_user_sessions('usr_123456') ``` ### create_session_filter [Section titled “create_session_filter”](#create_session_filter) clientSessionshttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/sessions.py [#](#create_session_filter)asynccreate_session_filter Helper method to create a UserSessionFilter for filtering sessions paramstatusOptional\[List\[str]] Optional. List of session statuses to filter by paramstart_timeOptional\[Timestamp] Optional. Filter sessions from this timestamp paramend_timeOptional\[Timestamp] Optional. Filter sessions until this timestamp returnsUserSessionFilter UserSessionFilter object --- # DOCUMENT BOUNDARY --- # Users > Provision and manage users and memberships Use `scalekit_client.users` to provision people, manage organization memberships, and assign roles. Common flows: invite or create a user, attach them to an organization, update profile fields, and look up users by Scalekit ID or your external ID. ### list_users [Section titled “list_users”](#list_users) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#list_users)list_users Method to list all users in environment parampage_sizeOptional\[int] Optional. Page size for pagination parampage_tokenOptional\[str] Optional. Page token for pagination returnsListUsersResponse List Users Response ```python response = scalekit_client.users.list_users( page_size=50, page_token='next_page_token' ) for user in response[0].users: print(f"User: {user.email}") ``` ### list_organization_users [Section titled “list_organization_users”](#list_organization_users) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#list_organization_users)list_organization_users Method to list users for given organization id paramorganization_idstr Organization id to list users for parampage_sizeOptional\[int] Optional. Page size for pagination parampage_tokenOptional\[str] Optional. Page token for pagination returnsListOrganizationUsersResponse List Organization Users Response ```python response = scalekit_client.users.list_organization_users( 'org_123456', page_size=50 ) for user in response[0].users: print(f"User: {user.email}") ``` ### get_user [Section titled “get_user”](#get_user) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#get_user)get_user Method to get user by ID paramuser_idstr User id to get user details returnsGetUserResponse Get User Response ```python response = scalekit_client.users.get_user('usr_123456') user = response[0].user print(f"User: {user.email}") ``` ### get_user_by_external_id [Section titled “get_user_by_external_id”](#get_user_by_external_id) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#get_user_by_external_id)get_user_by_external_id Method to get user by external ID paramexternal_idstr External id to get user details returnsGetUserResponse Get User Response ```python response = scalekit_client.users.get_user_by_external_id('external_123') user = response[0].user ``` ### create_user_and_membership [Section titled “create_user_and_membership”](#create_user_and_membership) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#create_user_and_membership)create_user_and_membership Method to create a new user and add them to an organization paramorganization_idstr Organization id to create user for paramuserCreateUser CreateUser object with expected values for user creation paramsend_invitation_emailbool Optional. Whether to send invitation email to the user returnsCreateUserAndMembershipResponse Create User And Membership Response ```python from scalekit.v1.users.users_pb2 import CreateUser user = CreateUser() user.email = "john.doe@example.com" user.given_name = "John" user.family_name = "Doe" response = scalekit_client.users.create_user_and_membership( 'org_123456', user, send_invitation_email=True ) ``` ### update_user [Section titled “update_user”](#update_user) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#update_user)update_user Method to update an existing user by ID paramuser_idstr User id to update paramuserUpdateUser UpdateUser object with expected values for user update returnsUpdateUserResponse Update User Response ```python from scalekit.v1.users.users_pb2 import UpdateUser user = UpdateUser() user.given_name = "John" response = scalekit_client.users.update_user('usr_123456', user) ``` ### delete_user [Section titled “delete_user”](#delete_user) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#delete_user)delete_user Method to delete user by ID paramuser_idstr User id to be deleted returnsNone Empty on success. ```python scalekit_client.users.delete_user('usr_123456') ``` ### create_membership [Section titled “create_membership”](#create_membership) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#create_membership)create_membership Method to create a membership for a user by user ID paramorganization_idstr Organization id paramuser_idstr User id parammembershipCreateMembership CreateMembership object paramsend_invitation_emailbool Optional. Whether to send invitation email to the user returnsCreateMembershipResponse Create Membership Response ```python from scalekit.v1.users.users_pb2 import CreateMembership membership = CreateMembership() response = scalekit_client.users.create_membership( 'org_123456', 'usr_123456', membership, send_invitation_email=True ) ``` ### update_membership [Section titled “update_membership”](#update_membership) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#update_membership)update_membership Method to update a membership for a user by user ID paramorganization_idstr Organization id paramuser_idstr User id parammembershipUpdateMembership UpdateMembership object returnsUpdateMembershipResponse Update Membership Response ```python from scalekit.v1.users.users_pb2 import UpdateMembership membership = UpdateMembership() response = scalekit_client.users.update_membership( 'org_123456', 'usr_123456', membership ) ``` ### delete_membership [Section titled “delete_membership”](#delete_membership) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#delete_membership)delete_membership Method to delete a membership for a user by user ID paramorganization_idstr Organization id paramuser_idstr User id returnsNone Empty on success. ```python scalekit_client.users.delete_membership('org_123456', 'usr_123456') ``` ### resend_invite [Section titled “resend_invite”](#resend_invite) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#resend_invite)resend_invite Method to resend an invitation email to a user who has a pending invitation paramorganization_idstr Organization id containing the pending invitation paramuser_idstr User id who has a pending invitation returnsResendInviteResponse Resend Invite Response ```python response = scalekit_client.users.resend_invite('org_123456', 'usr_123456') ``` ### update_user_by_external_id [Section titled “update_user_by_external_id”](#update_user_by_external_id) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#update_user_by_external_id)update_user_by_external_id Method to update an existing user by external ID paramexternal_idstr External id to update paramuserUpdateUser UpdateUser object with expected values for user update returnsUpdateUserResponse Update User Response ### delete_user_by_external_id [Section titled “delete_user_by_external_id”](#delete_user_by_external_id) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#delete_user_by_external_id)delete_user_by_external_id Method to delete user by external ID paramexternal_idstr External id to be deleted returnsNone Empty on success. ### create_membership_by_external_id [Section titled “create_membership_by_external_id”](#create_membership_by_external_id) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#create_membership_by_external_id)create_membership_by_external_id Method to create a membership for a user by external ID paramorganization_idstr Organization id paramexternal_idstr External id parammembershipCreateMembership CreateMembership object paramsend_invitation_emailbool Optional. Whether to send invitation email to the user returnsCreateMembershipResponse Create Membership Response ### update_membership_by_external_id [Section titled “update_membership_by_external_id”](#update_membership_by_external_id) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#update_membership_by_external_id)update_membership_by_external_id Method to update a membership for a user by external ID paramorganization_idstr Organization id paramexternal_idstr External id parammembershipUpdateMembership UpdateMembership object returnsUpdateMembershipResponse Update Membership Response ### delete_membership_by_external_id [Section titled “delete_membership_by_external_id”](#delete_membership_by_external_id) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#delete_membership_by_external_id)delete_membership_by_external_id Method to delete a membership for a user by external ID paramorganization_idstr Organization id paramexternal_idstr External id returnsNone Empty on success. ### list_user_roles [Section titled “list_user_roles”](#list_user_roles) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#list_user_roles)list_user_roles Method to list all roles assigned to a user in an organization paramorganization_idstr Organization id containing the user paramuser_idstr User id to list roles for returnsListUserRolesResponse List User Roles Response ### list_user_permissions [Section titled “list_user_permissions”](#list_user_permissions) clientUserhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/users.py [#](#list_user_permissions)list_user_permissions Method to list all permissions granted to a user in an organization paramorganization_idstr Organization id containing the user paramuser_idstr User id to list permissions for returnsListUserPermissionsResponse List User Permissions Response --- # DOCUMENT BOUNDARY --- # Passkeys > Passkey and WebAuthn credentials Use `scalekit_client.webauthn` to register and authenticate passkeys (WebAuthn credentials). Build registration and authentication ceremonies with the options and verification methods below. ### list_credentials [Section titled “list_credentials”](#list_credentials) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/webauthn.py [#](#list_credentials)list_credentials Method to list WebAuthn credentials for a user paramuser_idstr User ID to list credentials for returnswebauthn_pb2.ListCredentialsResponse Tuple of (ListCredentialsResponse, grpc.Call metadata) ```python response = scalekit_client.webauthn.list_credentials('usr_123456') print(f'Credentials: {response[0].credentials}') ``` ### update_credential [Section titled “update_credential”](#update_credential) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/webauthn.py [#](#update_credential)update_credential Method to update a WebAuthn credential’s display name paramcredential_idstr Credential ID to update paramdisplay_namestr New display name for the credential returnswebauthn_pb2.UpdateCredentialResponse Tuple of (UpdateCredentialResponse, grpc.Call metadata) ```python response = scalekit_client.webauthn.update_credential( 'cred_123', 'My YubiKey' ) ``` ### delete_credential [Section titled “delete_credential”](#delete_credential) clientWebAuthnhttps\://github.com/scalekit-inc/scalekit-sdk-python/blob/main/scalekit/webauthn.py [#](#delete_credential)delete_credential Method to delete a WebAuthn credential paramcredential_idstr Credential ID to delete returnswebauthn_pb2.DeleteCredentialResponse Tuple of (DeleteCredentialResponse, grpc.Call metadata) ```python response = scalekit_client.webauthn.delete_credential('cred_123') print(f'Deleted: {response[0].success}') ``` --- # DOCUMENT BOUNDARY --- # System requirements > Kubernetes, database, and network requirements for a self-hosted Scalekit deployment. You will confirm that your Kubernetes cluster, databases, and network meet the requirements for a self-hosted deployment. This helps you avoid installation failures and costly production issues later. ## Prepare Kubernetes [Section titled “Prepare Kubernetes”](#prepare-kubernetes) | Requirement | Value | | ------------------ | ------------------------------ | | Kubernetes version | 1.27 or later | | Helm version | 3.12 or later | | `kubectl` | Configured with cluster access | Any managed Kubernetes service works (GKE, EKS, AKS, or a self-managed cluster). Scalekit supports two ingress options. Use one, not both: * **Kubernetes Gateway API** (recommended for cloud clusters): requires a GatewayClass installed on your cluster * **Nginx Ingress** (for local or Minikube deployments): requires the nginx ingress controller For Gateway API, common GatewayClasses: | Provider | GatewayClass | | ------------- | ------------------------------------------- | | GKE | `gke-l7-global-external-managed` (built-in) | | Istio | `istio` | | Envoy Gateway | `eg` | On GKE, the Gateway controller is enabled by default. For other clusters, install the Gateway API CRDs and a compatible controller before deploying Scalekit. ## Prepare PostgreSQL [Section titled “Prepare PostgreSQL”](#prepare-postgresql) Scalekit requires **three separate databases** on PostgreSQL 15 or later. These can all live on the same PostgreSQL instance. | Database | Used by | | ---------- | --------------------------------- | | `scalekit` | Core auth service | | `webhooks` | Webhook delivery | | `openfga` | Authorization engine (if enabled) | Create a dedicated user with full privileges on each database. Scalekit runs migrations automatically on install and upgrade. Evaluation without external PostgreSQL The Helm chart includes an optional PostgreSQL subchart. Set `postgresql.enabled: true` in your `values.yaml` to spin up a bundled database for evaluation. Do not use this in production. CockroachDB CockroachDB is also supported. Set `DB_ADAPTER=postgresql` in the `db-migrations` secret (the setup script handles this automatically). ## Prepare Redis [Section titled “Prepare Redis”](#prepare-redis) | Requirement | Value | | ----------- | ------------ | | Version | 6.2 or later | Scalekit uses Redis for session storage, token caching, and background job queues. Redis does not need persistence enabled, but enabling RDB snapshots is recommended for production. Evaluation without external Redis The Helm chart includes an optional Redis subchart. Set `redis.enabled: true` in your `values.yaml` to use it for evaluation. Do not use this in production. ## Set up email (SMTP) [Section titled “Set up email (SMTP)”](#set-up-email-smtp) Scalekit sends transactional email for password resets, magic links, and user invitations. You need an SMTP provider configured before first run. Supported providers with first-class integration: **Postmark**, **SendGrid**. Any SMTP server works with the `OTHER` provider type. ## Size your nodes [Section titled “Size your nodes”](#size-your-nodes) | Resource | Production (per node) | | -------- | --------------------- | | CPU | 4 vCPUs | | RAM | 8 GB | The Scalekit Deployment defaults to 2 replicas with autoscaling up to 20. Each replica includes all sidecar containers. Tune `replicaCount` and HPA settings in your `values.yaml`. ## Configure network and DNS [Section titled “Configure network and DNS”](#configure-network-and-dns) ### Set up DNS [Section titled “Set up DNS”](#set-up-dns) A wildcard DNS record pointing to your Gateway’s external IP is required. You set this up after the deployment is running (see [Install Scalekit](/self-hosted/installation/#update-dns)). ### Set up TLS [Section titled “Set up TLS”](#set-up-tls) HTTPS is required. Manage certificates through your cloud provider’s certificate manager (for example, GCP Certificate Manager) and reference it via a Gateway annotation. cert-manager is also supported. Add the appropriate annotation to `gateway.annotations` in your `values.yaml`. ## Access the container registry [Section titled “Access the container registry”](#access-the-container-registry) Scalekit container images are hosted on a private container registry. You receive the registry server URL and credentials through the Scalekit distribution portal as part of onboarding. The registry token is stored as a Kubernetes image pull secret (`artifact-registry-secret`) during setup. ## Next step [Section titled “Next step”](#next-step) Once your infrastructure is ready, the Configure your deployment page will help you prepare the values.yaml that drives your deployment. --- # DOCUMENT BOUNDARY --- # Ways to implement SSO logins > Implement single sign-on on your login page using three UX strategies: identifier-driven, SSO button, or organization-specific pages. Single sign-on (SSO) login requires careful UX design to balance enterprise authentication requirements with user experience. Your login page must accommodate both SSO users (who authenticate through their organization’s identity provider) and non-SSO users (who use passwords or social authentication). This guide presents three proven UX strategies for adding SSO to your login page. Each strategy offers different trade-offs between user experience, implementation complexity, and administrative control. Choose the approach that best fits your users’ needs and your application’s architecture. The right strategy depends on your user base: identifier-driven flows work best when admins control authentication methods, explicit SSO buttons give users choice, and organization-specific login pages simplify enterprise deployments. ![Login page with password and social auth methods](/.netlify/images?url=_astro%2Fsimple_login_page.CjjjVgoK.png\&w=1024\&h=1222\&dpl=6ae471fc143c9a4c27550463) ## Strategy 1: Identifier-driven single sign-on [Section titled “Strategy 1: Identifier-driven single sign-on”](#strategy-1-identifier-driven-single-sign-on) Collect the user’s email address first. Use the email domain or organization identifier to determine whether to route to SSO or password-based authentication. ![Identifier-driven login](/.netlify/images?url=_astro%2Fidentifier_first_login.BlfaJ4QS.png\&w=2222\&h=1044\&dpl=6ae471fc143c9a4c27550463) Users don’t choose the authentication method. This reduces cognitive load and works well when admins mandate SSO after users have already logged in with passwords. Popular products like [Google](https://accounts.google.com), [Microsoft](https://login.microsoftonline.com), and [AWS](https://console.aws.amazon.com/console/) use this strategy. ## Strategy 2: Login with single sign-on button [Section titled “Strategy 2: Login with single sign-on button”](#strategy-2-login-with-single-sign-on-button) Add a “Login with SSO” button to your login page. This presents all authentication options and lets users choose their preferred method. ![Explicit option for login with SSO](/.netlify/images?url=_astro%2Fsso_button_login.onnUOag1.png\&w=1082\&h=1242\&dpl=6ae471fc143c9a4c27550463) If a user attempts password login but their admin mandates SSO, force SSO-based authentication instead of showing an error. Popular products like [Cal.com](https://app.cal.com/auth/login) and [Notion](https://www.notion.so/login) use this strategy. Tip If a user chooses an authentication method like social login, verify their identity and the appropriate authentication method. If the user must authenticate through SSO, prompt them to re-authenticate through SSO. ## Strategy 3: organization-specific login page [Section titled “Strategy 3: organization-specific login page”](#strategy-3-organization-specific-login-page) Serve different login pages for each organization instead of a single login page. For example, `https://customer1.b2b-app.com/login` and `https://customer2.b2b-app.com/login`. Show only the authentication methods applicable to that organization based on the URL. Popular products like [Zendesk](https://www.zendesk.com/in/login/) and [Slack](https://scalekit.slack.com/) use this strategy. The drawback is that users must remember their organization URL to access the login page. *** ## Next steps [Section titled “Next steps”](#next-steps) After implementing your chosen SSO login strategy: * [Pre-check SSO by domain](/guides/user-auth/check-sso-domain/) - Validate email domains have active SSO before redirecting * [Complete login with code exchange](/authenticate/fsa/complete-login/) - Exchange authorization codes for user data and tokens * [Manage user sessions](/authenticate/fsa/manage-session/) - Store and validate session tokens securely --- # DOCUMENT BOUNDARY --- # Authorization URL to initiate SSO > Learn how to construct and implement authorization URLs in Scalekit to initiate secure Single Sign-on (SSO) flows with your identity provider. The authorization endpoint is where your application redirects users to begin the authentication process. Scalekit powers this endpoint and handles redirecting users to the appropriate identity provider. Example authorization URL ```sh https://SCALEKIT_ENVIRONMENT_URL/oauth/authorize? response_type=code& client_id=skc_1234& scope=openid%20profile& redirect_uri=https%3A%2F%2Fyoursaas.com%2Fcallback& organization_id=org_1243412& state=aHR0cHM6Ly95b3Vyc2Fhcy5jb20vZGVlcGxpbms%3D ``` ## Parameters [Section titled “Parameters”](#parameters) | Parameter | Requirement | Description | | ----------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------- | | `client_id` | Required | Your unique client identifier from the API credentials page | | `nonce` | Optional | Random value for replay protection | | `organization_id` | Required\* | Identifier for the organization initiating SSO | | `connection_id` | Required\* | Identifier for the specific SSO connection | | `domain` | Required\* | Domain portion of email addresses configured for an organization | | `provider` | Required\* | Social login provider name. Supported providers: `google`, `microsoft`, `github`, `gitlab`, `linkedin`, `salesforce`, `facebook` | | `response_type` | Required | Must be set to `code` | | `redirect_uri` | Required | URL where Scalekit sends the response. Must match an authorized redirect URI | | `scope` | Required | Must be set to `openid email profile` | | `state` | Optional | Opaque string for request-response correlation | | `login_hint` | Optional | User’s email address for prefilling the login form | \* You must provide one of `organization_id`, `connection_id`, `domain`, or `provider`. If you identify SSO connection using `domain` or `login_hint`, the domain must be registered to the organization. Register domains in **Dashboard > Organizations > General**, or let customers add them via the admin portal. See [Onboard enterprise customers](/sso/guides/onboard-enterprise-customers/). Tip * Your `redirect_uri` must exactly match one of the authorized redirect URIs configured in your API settings * Always include the `state` parameter to protect against cross-site request forgery attacks ## SDK usage [Section titled “SDK usage”](#sdk-usage) Use Scalekit SDKs to generate authorization URLs programmatically. This approach handles parameter encoding and validation automatically. * Node.js ```diff 1 import { ScalekitClient } from '@scalekit-sdk/node'; 2 3 const scalekit = new ScalekitClient( 4 'https://your-subdomain.scalekit.dev', 5 '', 6 '' 7 ); 8 9 const options = { 10 loginHint: 'user@example.com', 11 organizationId: 'org_123235245', 12 }; 13 14 +const authorizationURL = scalekit.getAuthorizationUrl(redirectUri, options); 15 // Example generated URL: 16 // https://your-subdomain.scalekit.dev/oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile&redirect_uri=https%3A%2F%2Fyoursaas.com%2Fcallback&organization_id=org_123235245&login_hint=user%40example.com&state=abc123 ``` * Python ```diff 1 from scalekit import ScalekitClient, AuthorizationUrlOptions 2 3 scalekit = ScalekitClient( 4 'https://your-subdomain.scalekit.dev', 5 '', 6 '' 7 ) 8 9 options = AuthorizationUrlOptions() 10 options.organization_id = "org_12345" 11 options.login_hint = "user@example.com" 12 13 authorization_url = scalekit.get_authorization_url( 14 + redirect_uri, 15 + options 16 +) 17 +# Example generated URL: 18 # https://your-subdomain.scalekit.dev/oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile&redirect_uri=https%3A%2F%2Fyoursaas.com%2Fcallback&organization_id=org_12345&login_hint=user%40example.com&state=abc123 ``` * Go ```diff 1 import ( 2 "github.com/scalekit-inc/scalekit-sdk-go" 3 ) 4 5 func main() { 6 scalekitClient := scalekit.NewScalekitClient( 7 "https://your-subdomain.scalekit.dev", 8 "", 9 "" 10 ) 11 12 options := scalekitClient.AuthorizationUrlOptions{ 13 OrganizationId: "org_12345", 14 LoginHint: "user@example.com", 15 } 16 17 +authorizationURL := scalekitClient.GetAuthorizationUrl( 18 +redirectUrl, 19 +options, 20 + ) 21 // Example generated URL: 22 // https://your-subdomain.scalekit.dev/oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile&redirect_uri=https%3A%2F%2Fyoursaas.com%2Fcallback&organization_id=org_12345&login_hint=user%40example.com&state=abc123 23 } ``` * Java ```diff 1 package com.scalekit; 2 3 import com.scalekit.ScalekitClient; 4 import com.scalekit.internal.http.AuthorizationUrlOptions; 5 6 public class Main { 7 public static void main(String[] args) { 8 ScalekitClient scalekitClient = new ScalekitClient( 9 "https://your-subdomain.scalekit.dev", 10 "", 11 "" 12 ); 13 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 14 // Option 1: Authorization URL with the organization ID 15 options.setOrganizationId("org_13388706786312310"); 16 // Option 2: Authorization URL with the connection ID 17 options.setConnectionId("con_13388706786312310"); 18 // Option 3: Authorization URL with login hint 19 options.setLoginHint("user@example.com"); 20 21 try { 22 +String url = scalekitClient.authentication().getAuthorizationUrl(redirectUrl, options).toString(); 23 // Example generated URL: 24 // https://your-subdomain.scalekit.dev/oauth/authorize?response_type=code&client_id=skc_1234&scope=openid%20profile&redirect_uri=https%3A%2F%2Fyoursaas.com%2Fcallback&organization_id=org_13388706786312310&connection_id=con_13388706786312310&login_hint=user%40example.com&state=abc123 25 } catch (Exception e) { 26 System.out.println(e.getMessage()); 27 } 28 } 29 } ``` ## Parameter precedence [Section titled “Parameter precedence”](#parameter-precedence) When you provide multiple connection parameters, Scalekit follows a specific precedence order to determine which identity provider to use: 1. `provider` (highest precedence): If present, Scalekit ignores all other connection parameters and directs users to the specified social login provider. For example, `provider=google` redirects users to Google’s login screen. See [Social Login](/authenticate/auth-methods/social-logins/) for more details. 2. `connection_id`: Takes highest precedence among enterprise SSO parameters. Scalekit uses this specific connection if you provide a valid connection ID. If the connection ID is invalid, the authorization request fails. 3. `organization_id`: Scalekit uses this parameter when no valid `connection_id` is provided. It selects the SSO connection configured for the specified organization. 4. `domain`: Scalekit uses this parameter when neither `connection_id` nor `organization_id` are provided. It selects the SSO connection configured for the specified domain. 5. `login_hint` (lowest precedence): Scalekit extracts the domain portion from the email address and uses the corresponding SSO connection mapped to that organization. The domain must be registered to the organization either manually from the Scalekit Dashboard or through the admin portal when [onboarding an enterprise customer](/sso/guides/onboard-enterprise-customers/). ## Common scenarios [Section titled “Common scenarios”](#common-scenarios) SSO falls back to OTP instead of redirecting to the IdP When routing via `domain` or `login_hint`, Scalekit performs an **exact match** against the domains registered to the organization. Subdomains and root domains are treated as distinct values — `wal-mart.com` and `homeoffice.wal-mart.com` are different. If your users have addresses like `user@homeoffice.wal-mart.com`, register `homeoffice.wal-mart.com` as the organization domain, not just `wal-mart.com`. A mismatch causes Scalekit to silently fall through to OTP login. Both organization_id and login_hint provided — which wins? `organization_id` takes precedence. When you pass both `organization_id=org_123` and `login_hint=user@company.com`, Scalekit routes to the SSO connection configured for that organization and ignores the domain extracted from the email. See [Parameter precedence](#parameter-precedence) above for the full priority order. --- # DOCUMENT BOUNDARY --- # Handle identity provider initiated SSO > Learn how to securely implement IdP-initiated Single Sign-On for your application This guide shows you how to securely implement Identity Provider (IdP)-initiated Single Sign-On for your application. When users log into your application directly from their identity provider’s portal, Scalekit converts the IdP-initiated request to a Service Provider (SP)-initiated flow for enhanced security. Modular SSO requirement With Full Stack Auth enabled, Scalekit handles all authentication flows automatically. IdP-initiated SSO needs to be handled manually when using Modular SSO. Enable/Disable Full Stack Auth in **Dashboard > Authentication > General** Review the authentication sequence ![IdP-initiated SSO: the user starts from the identity provider, Scalekit sends the response, and your app exchanges the code to log them in](/d2/docs/sso/guides/idp-init-sso-0.svg) The workflow converts the traditional IdP-initiated flow to a secure SP-initiated flow by: 1. The user logs into their identity provider portal and selects your application 2. The identity provider sends user details as assertions to Scalekit 3. Scalekit redirects to your initiate login endpoint with a JWT token 4. Your application validates the JWT and generates a new SP-initiated authorization URL To securely implement IdP-initiated SSO, follow these steps to convert incoming IdP-initiated requests to SP-initiated flows: 1. Set up an initiate login endpoint and register it in **Dashboard > Developers > Redirect URLs > Initiate Login URL** 2. Extract information from the JWT token containing organization, connection, and user details 3. Convert to SP-initiated flow using the extracted parameters to generate a new authorization URL 4. Handle errors with proper callback processing and error handling best practices ## Implementation examples [Section titled “Implementation examples”](#implementation-examples) Use the extracted parameters to initiate a new SSO request. This converts the IdP-initiated flow to a secure SP-initiated flow. Here are implementation examples: * Node.js Express.js ```javascript 5 collapsed lines 1 // Security: ALWAYS verify requests are from Scalekit before processing 2 // This prevents unauthorized parties from triggering your interceptor logic 3 4 // Use case: Handle IdP-initiated SSO requests from enterprise customer portals 5 // Examples: Okta dashboard, Azure AD portal, Google Workspace apps 6 7 const express = require('express'); 8 const app = express(); 9 10 app.get('/login', async (req, res) => { 11 try { 12 // Your Initiate Login Endpoint receives a JWT 13 const { error_description, idp_initiated_login } = req.query; 14 15 if (error_description) { 16 return res.redirect('/login?error=auth_failed'); 17 } 18 19 // Decode the JWT and extract claims 20 if (idp_initiated_login) { 21 const { 22 connection_id, 23 organization_id, 24 login_hint, 25 relay_state 26 } = await scalekit.getIdpInitiatedLoginClaims(idp_initiated_login); 27 28 // Use ONE of the following properties for authorization 29 const options = {}; 30 if (connection_id) options.connectionId = connection_id; 31 if (organization_id) options.organizationId = organization_id; 32 if (login_hint) options.loginHint = login_hint; 33 if (relay_state) options.state = relay_state; 34 35 // Generate Authorization URL for SP-initiated flow 36 const url = scalekit.getAuthorizationUrl( 37 process.env.REDIRECT_URI, 38 options 39 ); 40 41 return res.redirect(url); 42 } 43 44 // Handle regular login flow here 45 res.redirect('/login'); 46 } catch (error) { 47 console.error('IdP-initiated login error:', error); 48 res.redirect('/login?error=auth_failed'); 49 } 50 }); ``` * Python Flask ```python 5 collapsed lines 1 # Security: ALWAYS verify requests are from Scalekit before processing 2 # This prevents unauthorized parties from triggering your interceptor logic 3 4 # Use case: Handle IdP-initiated SSO requests from enterprise customer portals 5 # Examples: Okta dashboard, Azure AD portal, Google Workspace apps 6 7 from flask import Flask, request, redirect, url_for 8 import os 9 10 app = Flask(__name__) 11 12 @app.route('/login') 13 def login(): 14 try: 15 # Your Initiate Login Endpoint receives a JWT 16 error_description = request.args.get('error_description') 17 idp_initiated_login = request.args.get('idp_initiated_login') 18 19 if error_description: 20 return redirect(url_for('login', error='auth_failed')) 21 22 # Decode the JWT and extract claims 23 if idp_initiated_login: 24 claims = await scalekit_client.get_idp_initiated_login_claims(idp_initiated_login) 25 26 # Extract claims with fallbacks 27 connection_id = claims.get('connection_id') 28 organization_id = claims.get('organization_id') 29 login_hint = claims.get('login_hint') 30 relay_state = claims.get('relay_state') 31 32 # Create authorization options 33 options = AuthorizationUrlOptions() 34 if connection_id: 35 options.connection_id = connection_id 36 if organization_id: 37 options.organization_id = organization_id 38 if login_hint: 39 options.login_hint = login_hint 40 if relay_state: 41 options.state = relay_state 42 43 # Generate Authorization URL for SP-initiated flow 44 authorization_url = scalekit_client.get_authorization_url( 45 redirect_uri=os.getenv('REDIRECT_URI'), 46 options=options 47 ) 48 49 return redirect(authorization_url) 50 51 # Handle regular login flow here 52 return redirect(url_for('login')) 53 except Exception as error: 54 print(f"IdP-initiated login error: {error}") 55 return redirect(url_for('login', error='auth_failed')) ``` * Go Gin ```go 5 collapsed lines 1 // Security: ALWAYS verify requests are from Scalekit before processing 2 // This prevents unauthorized parties from triggering your interceptor logic 3 4 // Use case: Handle IdP-initiated SSO requests from enterprise customer portals 5 // Examples: Okta dashboard, Azure AD portal, Google Workspace apps 6 7 package main 8 9 import ( 10 "net/http" 11 "github.com/gin-gonic/gin" 12 ) 13 14 func (a *App) handleLogin(c *gin.Context) { 15 // Your Initiate Login Endpoint receives a JWT 16 errDescription := c.Query("error_description") 17 idpInitiatedLogin := c.Query("idp_initiated_login") 18 19 if errDescription != "" { 20 c.Redirect(http.StatusFound, "/login?error=auth_failed") 21 return 22 } 23 24 // Decode the JWT and extract claims 25 if idpInitiatedLogin != "" { 26 claims, err := scalekitClient.GetIdpInitiatedLoginClaims(c.Request.Context(), idpInitiatedLogin) 27 if err != nil { 28 http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 29 return 30 } 31 32 // Create authorization options with ONE of the following properties 33 options := scalekit.AuthorizationUrlOptions{} 34 if claims.ConnectionID != "" { 35 options.ConnectionId = claims.ConnectionID 36 } 37 if claims.OrganizationID != "" { 38 options.OrganizationId = claims.OrganizationID 39 } 40 if claims.LoginHint != "" { 41 options.LoginHint = claims.LoginHint 42 } 43 if claims.RelayState != "" { 44 options.State = claims.RelayState 45 } 46 47 // Generate Authorization URL for SP-initiated flow 48 authUrl, err := scalekitClient.GetAuthorizationUrl(redirectUrl, options) 49 if err != nil { 50 http.Error(c.Writer, err.Error(), http.StatusInternalServerError) 51 return 52 } 53 54 c.Redirect(http.StatusFound, authUrl.String()) 55 return 56 } 57 58 // Handle regular login flow here 59 c.Redirect(http.StatusFound, "/login") 60 } ``` * Java Spring Boot ```java 5 collapsed lines 1 // Security: ALWAYS verify requests are from Scalekit before processing 2 // This prevents unauthorized parties from triggering your interceptor logic 3 4 // Use case: Handle IdP-initiated SSO requests from enterprise customer portals 5 // Examples: Okta dashboard, Azure AD portal, Google Workspace apps 6 7 import org.springframework.web.bind.annotation.*; 8 import org.springframework.web.servlet.view.RedirectView; 9 import javax.servlet.http.HttpServletResponse; 10 11 @RestController 12 public class AuthController { 13 14 @GetMapping("/login") 15 public RedirectView handleLogin( 16 @RequestParam(required = false, name = "error_description") String errorDescription, 17 @RequestParam(required = false, name = "idp_initiated_login") String idpInitiatedLoginToken, 18 HttpServletResponse response) throws IOException { 19 20 if (errorDescription != null) { 21 return new RedirectView("/login?error=auth_failed"); 22 } 23 24 // Decode the JWT and extract claims 25 if (idpInitiatedLoginToken != null) { 26 IdpInitiatedLoginClaims claims = scalekitClient.authentication() 27 .getIdpInitiatedLoginClaims(idpInitiatedLoginToken); 28 29 if (claims == null) { 30 response.sendError(HttpStatus.BAD_REQUEST.value(), 31 "Invalid idp_initiated_login token"); 32 return null; 33 } 34 35 // Create authorization options with ONE of the following 36 AuthorizationUrlOptions options = new AuthorizationUrlOptions(); 37 if (claims.getConnectionID() != null) { 38 options.setConnectionId(claims.getConnectionID()); 39 } 40 if (claims.getOrganizationID() != null) { 41 options.setOrganizationId(claims.getOrganizationID()); 42 } 43 if (claims.getLoginHint() != null) { 44 options.setLoginHint(claims.getLoginHint()); 45 } 46 if (claims.getRelayState() != null) { 47 options.setState(claims.getRelayState()); 48 } 49 50 // Generate Authorization URL for SP-initiated flow 51 String url = scalekitClient.authentication() 52 .getAuthorizationUrl(redirectUrl, options) 53 .toString(); 54 55 response.sendRedirect(url); 56 return null; 57 } 58 59 // Handle regular login flow here 60 return new RedirectView("/login"); 61 } 62 } ``` ## Implementation details [Section titled “Implementation details”](#implementation-details) ### Endpoint setup [Section titled “Endpoint setup”](#endpoint-setup) Implement this as a backend route, not a frontend page. Scalekit redirects the browser to the Initiate Login URL with a `GET` request; your server reads the `idp_initiated_login` parameter, validates it, and issues its own redirect to continue the flow — the route never renders a page. Your initiate login endpoint will receive requests with the following format: ```sh https://yourapp.com/login?idp_initiated_login= ``` ### JWT token structure [Section titled “JWT token structure”](#jwt-token-structure) The `idp_initiated_login` parameter contains a signed JWT with organization, connection, and user details. View JWT structure ```json { "organization_id": "org_225336910XXXX588", "connection_id": "conn_22533XXXXX575236", "login_hint": "name@example.com", "exp": 1723042087, "nbf": 1723041787, "iat": 1723041787, "iss": "https://b2b-app.com" } ``` ### Error callback format [Section titled “Error callback format”](#error-callback-format) If errors occur, the redirect URI will receive a callback with this format: ```sh https://{your-subdomain}.scalekit.dev/callback ?error="" &error_description="
" ``` After completing the SP-initiated flow, users are redirected back to your callback URL where you can complete the authentication process. Next, let’s look at how to test your IdP-initiated SSO implementation. ## Integrating with a downstream auth provider [Section titled “Integrating with a downstream auth provider”](#integrating-with-a-downstream-auth-provider) If your application uses a third-party service like [Firebase Authentication](/guides/integrations/auth-systems/firebase/) to manage user sessions, you must initiate its sign-in flow after completing **Step 3**. This process has two stages: first, the IdP redirects the user to your app via Scalekit, and second, your app triggers a new sign-in flow with Firebase using the Authorization URL you just generated. Review the downstream auth flow ![IdP-initiated SSO with a downstream auth provider: Scalekit redirects with a JWT that triggers sign-in in the downstream provider](/d2/docs/sso/guides/idp-init-sso-1.svg) The example below shows how to pass the Authorization URL to the Firebase Web SDK. * Firebase (Web SDK) Firebase Web SDK ```javascript import { getAuth, OAuthProvider, signInWithRedirect } from "firebase/auth"; // Security: Configure OIDC provider properly to prevent token injection const auth = getAuth(); // "scalekit" is the OIDC provider you configured in Firebase const scalekitProvider = new OAuthProvider("scalekit"); // Use the authorizationUrl generated in Step 3 scalekitProvider.setCustomParameters({ connection_id: "", // Enables Firebase to forward the connection ID to Scalekit }); // Initiate Firebase sign-in with Scalekit provider signInWithRedirect(auth, scalekitProvider); ``` Provider compatibility This pattern applies to other OIDC-compatible providers like Auth0 or AWS Cognito. Simply supply the Authorization URL from **Step 3** to start the provider’s standard sign-in flow. ## Security considerations [Section titled “Security considerations”](#security-considerations) While IdP-initiated SSO offers convenience, it comes with significant security risks. Scalekit’s approach converts the flow to SP-initiated to mitigate these vulnerabilities. ### Traditional IdP-initiated SSO security risks [Section titled “Traditional IdP-initiated SSO security risks”](#traditional-idp-initiated-sso-security-risks) **Stolen SAML assertions**: Attackers can steal SAML assertions and use them to gain unauthorized access. If an attacker manages to steal these assertions, they can: * Inject them into another service provider, gaining access to that user’s account * Inject them back into your application with altered assertions, potentially elevating their privileges With a stolen SAML assertion, an attacker can gain access to your application as the compromised user, bypassing the usual authentication process. ### How attackers steal SAML assertions [Section titled “How attackers steal SAML assertions”](#how-attackers-steal-saml-assertions) Attackers can steal SAML assertions through various methods: * **Man-in-the-middle (MITM) attacks**: Intercepting and replacing the SAML response during transmission * **Open redirect attacks**: Exploiting improper endpoint validation to redirect the SAML response to a malicious server * **Leaky logs and headers**: Sensitive information, including SAML assertions, can be leaked through logs or headers * **Browser-based attacks**: Exploiting browser vulnerabilities to steal SAML assertions ### The challenge for service providers [Section titled “The challenge for service providers”](#the-challenge-for-service-providers) The chief problem with stolen assertions is that everything appears legitimate to the service provider (your application). The message and assertion are valid, issued by the expected identity provider, and signed with the expected key. However, the service provider cannot verify whether the assertions are stolen or not. Performance note The conversion from IdP-initiated to SP-initiated flow adds minimal latency (typically under 100ms) while significantly improving security. If you encounter issues implementing IdP-initiated SSO: 1. **Verify configuration**: Ensure your redirect URI is properly configured in **Dashboard > Developers > Redirect URLs** 2. **Check JWT processing**: Verify you’re correctly processing the JWT token from the `idp_initiated_login` parameter 3. **Validate error handling**: Ensure your error handling properly captures and processes any error messages 4. **Test connections**: Confirm the organization and connection IDs in the JWT are valid and active 5. **Review logs**: Check both your application logs and Scalekit dashboard logs for debugging information Common issues The most frequent issue is mismatched redirect URLs between your code and the Scalekit dashboard configuration. Ensure URLs match exactly, including protocol (http/https) and trailing slashes. --- # DOCUMENT BOUNDARY --- # Production readiness checklist > A focused checklist for launching your Scalekit SSO integration, based on the core enterprise authentication launch checks. As you prepare to launch enterprise SSO to production, you should confirm that your configuration satisfies the core enterprise checks from the authentication launch checklist. This page extracts the SSO-specific items from the main authentication [production readiness checklist](/authenticate/launch-checklist/) and organizes them for your SSO rollout. Use this checklist alongside the main launch checklist to validate that your SSO flows, admin experience, and network access are ready for enterprise customers. **Verify production environment configuration** Confirm that your environment URL (`SCALEKIT_ENVIRONMENT_URL`), client ID (`SCALEKIT_CLIENT_ID`), and client secret (`SCALEKIT_CLIENT_SECRET`) are correctly configured for your production environment and match your production Scalekit dashboard settings. **Verify SSO integrations with identity providers** Test SSO integrations with your target identity providers (for example, Okta, Azure AD, Google Workspace) using your production environment URL and credentials. **Configure SSO attribute mapping and identifiers** Configure SSO user attribute mapping (email, name, groups) and ensure you use consistent user identifiers (for example, email or `userPrincipalName`) across all SSO connections. **Verify redirect URIs and state validation** Confirm that your redirect URIs are correctly configured in both Scalekit and your identity providers, and that you validate the `state` parameter in callbacks to prevent CSRF attacks. **Test SP-initiated and IdP-initiated SSO flows** Test both SP-initiated and IdP-initiated SSO flows end-to-end in a staging environment before enabling them for production tenants. See [test SSO flows](/sso/guides/test-sso) for detailed scenarios. **Finalize admin portal setup and branding** Configure the self-service admin portal, apply your branding (logo, accent colors), and verify that enterprise admins can manage SSO connections and users as expected. **Review admin portal URL and DNS** Customize the admin portal URL to match your domain (for example, `https://sso.b2b-app.com`), update your `.env` configuration after CNAME setup, and confirm that your customers can access the portal from their networks. **Verify customer network and firewall access** Ask your enterprise customers to whitelist your Scalekit environment domain and related endpoints so SSO redirects and admin portal access work behind their VPNs and firewalls. **Harden error handling and monitoring for SSO** Test SSO error scenarios (for example, misconfigured connections, invalid assertions, and deactivated users), and set up logging and alerts so you can quickly detect and remediate SSO issues. --- # DOCUMENT BOUNDARY --- # Onboard enterprise customers > Complete workflow for enabling enterprise SSO and self-serve configuration for your customers Enterprise SSO enables users to authenticate to your application using their organization’s identity provider (IdP) such as Okta, Microsoft Entra ID, or Google Workspace. This provides enterprise customers with a secure, centralized authentication experience while reducing password management overhead. ![How Scalekit connects your application to enterprise identity providers](/.netlify/images?url=_astro%2Fhow-scalekit-connects.CrZX8E30.png\&w=5776\&h=1924\&dpl=6ae471fc143c9a4c27550463) This guide walks you through the complete workflow for onboarding enterprise customers with SSO. You’ll learn how to create organizations, provide admin portal access, enable domain-based SSO, and verify the integration. Before onboarding enterprise customers, ensure you have completed the [Full Stack Auth quickstart](/authenticate/fsa/quickstart/) to set up basic authentication in your application. ## Table of contents * [Create organization](#create-organization) * [Provide admin portal access](#provide-admin-portal-access) * [Customer configures SSO](#customer-configures-sso) * [Verify domain ownership](#verify-domain-ownership) 1. ## Create organization Create an organization in Scalekit to represent your enterprise customer: * Log in to the [Scalekit dashboard](https://app.scalekit.com) * Navigate to **Dashboard > Organizations** * Click **Create Organization** * Enter the organization name and relevant details * Save the organization Each organization in Scalekit represents one of your enterprise customers and can have its own SSO configuration, directory sync settings, and domain associations. 2. ## Provide admin portal access Give your customer’s IT administrator access to the self-serve admin portal to configure their identity provider. Scalekit provides two integration methods: **Option 1: Share a no-code link** Quick setup Generate and share a link to the admin portal: * Select the organization from **Dashboard > Organizations** * Click **Generate link** in the organization overview * Share the link with your customer’s IT admin via email, Slack, or your preferred channel The link remains valid for 7 days and can be revoked anytime from the dashboard. **Link properties:** | Property | Details | | -------------- | ------------------------------------------------------------------------------- | | **Expiration** | Links expire after 7 days | | **Revocation** | Revoke links anytime from the dashboard | | **Sharing** | Share via email, Slack, or any preferred channel | | **Security** | Anyone with the link can view and update the organization’s connection settings | The generated link follows this format: Portal link example ```http https://your-app.scalekit.dev/magicLink/2cbe56de-eec4-41d2-abed-90a5b82286c4_p ``` Security consideration Treat portal links as sensitive credentials. Anyone with the link can view and modify the organization’s SSO and SCIM configuration. **Option 2: Embed the portal** Seamless experience Embed the admin portal directly in your application so customers can configure SSO without leaving your interface. The portal link must be generated programmatically on each page load for security. Each generated link is single-use and expires after 1 minute, though once loaded, the session remains active for up to 6 hours. * Node.js ```bash npm install @scalekit-sdk/node ``` * Python ```sh pip install scalekit-sdk-python ``` * Go ```sh go get -u github.com/scalekit-inc/scalekit-sdk-go ``` * Java ```groovy /* Gradle users - add the following to your dependencies in build file */ implementation "com.scalekit:scalekit-sdk-java:2.1.3" ``` ```xml com.scalekit scalekit-sdk-java 2.1.3 ``` ### Generate portal link Use the Scalekit SDK to generate a unique, embeddable admin portal link for an organization. Call this API endpoint each time you render the page containing the iframe: * Node.js Express.js ```javascript 6 collapsed lines import { Scalekit } from '@scalekit-sdk/node'; const scalekit = new Scalekit( process.env.SCALEKIT_ENVIRONMENT_URL, process.env.SCALEKIT_CLIENT_ID, process.env.SCALEKIT_CLIENT_SECRET, ); async function generatePortalLink(organizationId) { const link = await scalekit.organization.generatePortalLink(organizationId); return link.location; // Use as iframe src } ``` * Python Flask ```python 6 collapsed lines from scalekit import Scalekit import os scalekit_client = Scalekit( environment_url=os.environ.get("SCALEKIT_ENVIRONMENT_URL"), client_id=os.environ.get("SCALEKIT_CLIENT_ID"), client_secret=os.environ.get("SCALEKIT_CLIENT_SECRET") ) def generate_portal_link(organization_id): link = scalekit_client.organization.generate_portal_link(organization_id) return link.location # Use as iframe src ``` * Go Gin ```go 10 collapsed lines 1 import ( 2 "context" 3 "os" 4 5 "github.com/scalekit/sdk-go" 6 ) 7 8 scalekitClient := scalekit.New( 9 os.Getenv("SCALEKIT_ENVIRONMENT_URL"), 10 os.Getenv("SCALEKIT_CLIENT_ID"), 11 os.Getenv("SCALEKIT_CLIENT_SECRET"), 12 ) 13 14 func generatePortalLink(organizationID string) (string, error) { 15 ctx := context.Background() 16 link, err := scalekitClient.Organization().GeneratePortalLink(ctx, organizationID) 17 if err != nil { 18 return "", err 19 } 20 return link.Location, nil // Use as iframe src 21 } ``` * Java Spring Boot ```java 8 collapsed lines 1 import com.scalekit.client.Scalekit; 2 import com.scalekit.client.models.Link; 3 import com.scalekit.client.models.Feature; 4 import java.util.Arrays; 5 6 Scalekit scalekitClient = new Scalekit( 7 System.getenv("SCALEKIT_ENVIRONMENT_URL"), 8 System.getenv("SCALEKIT_CLIENT_ID"), 9 System.getenv("SCALEKIT_CLIENT_SECRET") 10 ); 11 12 public String generatePortalLink(String organizationId) { 13 Link portalLink = scalekitClient.organizations() 14 .generatePortalLink(organizationId, Arrays.asList(Feature.sso, Feature.dir_sync)); 15 return portalLink.getLocation(); // Use as iframe src 16 } ``` The API returns a JSON object with the portal link. Use the `location` property as the iframe `src`: API response ```json { "id": "8930509d-68cf-4e2c-8c6d-94d2b5e2db43", "location": "https://random-subdomain.scalekit.dev/magicLink/8930509d-68cf-4e2c-8c6d-94d2b5e2db43", "expireTime": "2024-10-03T13:35:50.563013Z" } ``` Embed portal in iframe ```html ``` Embed the portal in your application’s settings or admin section where customers manage authentication configuration. Listen for UI events from the embedded portal to respond to configuration changes, such as when SSO is enabled or the session expires. See the [Admin portal UI events reference](/reference/admin-portal/ui-events/) for details on handling these events. ### Configuration and session | Setting | Requirement | | --------------------- | ----------------------------------------------------------------------------- | | **Redirect URI** | Add your application domain at **Dashboard > Developers > API Configuration** | | **iframe attributes** | Include `allow="clipboard-write"` for copy-paste functionality | | **Dimensions** | Minimum recommended height: 600px | | **Link expiration** | Generated links expire after 1 minute if not loaded | | **Session duration** | Portal session remains active for up to 6 hours once loaded | | **Single-use** | Each generated link can only be used once to initialize a session | Generate fresh links Generate a new portal link on each page load rather than caching the URL. This ensures security and prevents expired link errors. 3. ## Customer configures SSO After receiving admin portal access, your customer’s IT administrator: * Opens the admin portal (via shared link or embedded iframe) * Selects their identity provider (Okta, Microsoft Entra ID, Google Workspace, etc.) * Follows the provider-specific setup guide * Enters the required configuration (metadata URL, certificates, etc.) * Tests the connection * Activates the SSO connection Once configured, the SSO connection appears as active in your organization’s settings: ![Active enterprise SSO connection](/.netlify/images?url=_astro%2Fenterpise-sso-1.BfV9F7Wk.png\&w=2074\&h=1116\&dpl=6ae471fc143c9a4c27550463) IdP configuration guides Share the appropriate [SSO integration guide](/guides/integrations/sso-integrations/) with your customer’s IT team to help them configure their identity provider correctly. 4. ## Verify domain ownership After SSO is configured, verify the organization’s email domains to enable automatic SSO routing. When domains are verified, users with matching email addresses are automatically redirected to their organization’s SSO login. This is also called as **Home Realm Discovery**. Home realm discovery Domain verification enables home realm discovery, where Scalekit automatically determines which identity provider to use based on the user’s email domain. ### Manual verification (no DNS required) Domains added via the Scalekit Dashboard or API are marked as **Admin-verified** — no DNS challenge is required. Use this when you have already confirmed domain ownership through another process (contract, business verification, etc.) and want to activate SSO routing immediately. **Via Scalekit Dashboard:** * Navigate to **Dashboard > Organizations** and select the organization * Go to **Overview > Organization Domains** * Click **Add domain** and enter the domain name (e.g., `megacorp.com`) ![Organization domain verification in dashboard](/.netlify/images?url=_astro%2Forg_domain.CnZ3T4x-.png\&w=2940\&h=1588\&dpl=6ae471fc143c9a4c27550463) **Via API:** * cURL ```sh curl 'https:///api/v1/organizations//domains' \ --request POST \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "domain": "megacorp.com", "domain_type": "ORGANIZATION_DOMAIN" }' ``` * Node.js ```ts const domain = await scalekit.organization.createDomain(organizationId, { domain: 'megacorp.com', domainType: 'ORGANIZATION_DOMAIN', }); ``` * Python ```python domain = scalekit_client.organization.create_domain( organization_id, domain="megacorp.com", domain_type="ORGANIZATION_DOMAIN", ) ``` * Go ```go domain, err := scalekitClient.Organization().CreateDomain( ctx, organizationID, scalekit.CreateDomainOptions{ Domain: "megacorp.com", DomainType: "ORGANIZATION_DOMAIN", }, ) ``` * Java ```java Domain domain = scalekitClient.organizations() .createDomain(organizationId, new CreateDomainOptions() .setDomain("megacorp.com") .setDomainType("ORGANIZATION_DOMAIN")); ``` ### DNS verification (self-serve for org admins) DNS verification lets your customer’s IT admin prove domain ownership by publishing a TXT record — without involving your team. Scalekit displays the TXT record to publish, then polls DNS automatically until verified. 1. Enable the `Domain Verification` feature for the organization (Dashboard org **Overview** → toggle **Domain Verification**, or via the [API](/apis/#tag/organizations/PATCH/api/v1/organizations/%7Bid%7D/settings)). 2. Generate an Admin Portal link and share it with the org admin. 3. The admin enters the domain in the portal, copies the displayed TXT record, and adds it to their DNS provider (e.g., Cloudflare, Route 53, GoDaddy). 4. Scalekit detects the TXT record automatically and marks the domain as **Verified**. ![Domain verification via admin portal](/.netlify/images?url=_astro%2F2026-05-26-19-01-37.C8CAmZIi.png\&w=2196\&h=1596\&dpl=6ae471fc143c9a4c27550463) Once verified, users with email addresses from that domain (e.g., `user@megacorp.com`) can authenticate using their organization’s SSO. ## Customize the admin portal Match the admin portal to your brand identity. Configure branding at **Dashboard > Settings > Branding**: | Option | Description | | ---------------- | --------------------------------------------------------- | | **Logo** | Upload your company logo (displayed in the portal header) | | **Accent color** | Set the primary color to match your brand palette | | **Favicon** | Provide a custom favicon for browser tabs | Branding scope Branding changes apply globally to all portal instances (both shareable links and embedded iframes) in your environment. For additional customization options including custom domains, see the [Custom domain guide](/guides/custom-domain/). ## 5. Test the integration [Section titled “5. Test the integration”](#5-test-the-integration) Before rolling out SSO to your customers, thoroughly test the integration: * **Use the IdP Simulator** during development to test without configuring real identity providers * **Test with real providers** like Okta or Microsoft Entra ID in your staging environment * **Validate all scenarios**: SP-initiated SSO, IdP-initiated SSO, and error handling For complete testing instructions, see the [Test SSO integration guide](/sso/guides/test-sso/). --- # DOCUMENT BOUNDARY --- # Security and compliance for SSO and SCIM > How Scalekit validates SAML responses, enforces PKCE, isolates organizations, handles certificate rotation and gives you its SOC 2 report. Use this page when your security team reviews Scalekit’s modular SSO and SCIM, or when you fill in a vendor security questionnaire. It covers how Scalekit validates sign-in responses from your customers’ identity providers (IdPs), how it protects the authorization code it returns to your app, how it keeps organizations apart, where it stores identity data, and how to get its compliance reports. ## SAML response validation [Section titled “SAML response validation”](#saml-response-validation) Scalekit validates every SAML response before it reads any user attributes from it. Your app receives a user’s profile only after every check below passes. | Check | What Scalekit requires | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Signature | The response or the assertion is signed with a certificate configured on the connection. An unsigned response with an unsigned assertion is rejected. | | Signature placement | The signature is a direct child of the element it signs, and its reference points to that element’s ID. Scalekit reads assertions only from direct children of the response. These rules block XML signature-wrapping attacks. | | Issuer | The response and assertion issuer match the IdP entity ID configured on the connection. | | Audience | Any audience restriction in the assertion names the connection’s SP entity ID. | | Destination and recipient | The response destination and the subject confirmation recipient match the connection’s assertion consumer service (ACS) URL. | | Time conditions | `NotBefore` and `NotOnOrAfter` hold, with up to 180 seconds of clock skew. `IssueInstant` is within 90 seconds. | | Status | The response status is `Success`. | Scalekit also accepts encrypted assertions and decrypts them before validation. ### Replay protection [Section titled “Replay protection”](#replay-protection) Scalekit rejects a SAML response it has already accepted: * **SP-initiated sign-in:** the response’s `InResponseTo` must match a SAML request that Scalekit issued for the same browser, and each request ID can be used only once. * **IdP-initiated sign-in:** Scalekit stores the ID of each assertion it accepts and rejects an assertion with the same ID. Scalekit doesn’t send an authorization code to your app from an IdP-initiated response. It sends the user to your initiate-login URL, and your app starts a new SP-initiated sign-in. See [IdP-initiated SSO](/sso/guides/idp-init-sso/). ## PKCE on the authorization endpoint [Section titled “PKCE on the authorization endpoint”](#pkce-on-the-authorization-endpoint) Proof Key for Code Exchange (PKCE) stops a stolen authorization code from being redeemed by anyone else. Scalekit supports PKCE with the `S256` method on `/oauth/authorize`, the same endpoint your app uses to start SSO with `organization_id`, `connection_id`, `domain` or `login_hint`. | Request | PKCE parameters | | ---------------------- | ------------------------------------------------------------------------------------------------------------------ | | `GET /oauth/authorize` | `code_challenge`: the base64url-encoded SHA-256 hash of a random `code_verifier`. `code_challenge_method`: `S256`. | | `POST /oauth/token` | `code_verifier`: the value you hashed for `code_challenge`. | When the authorization request includes a `code_challenge`, the token request fails with `invalid_request` if `code_verifier` is missing, and with `invalid_grant` if it doesn’t match. ### Require PKCE for an application [Section titled “Require PKCE for an application”](#require-pkce-for-an-application) PKCE is required for single-page and native applications. To require it for a server-side app, register it as a web application: 1. In **Developers** > **Applications**, select **Create Application** and choose **Web Application**. 2. In the application’s settings, select **Enforce PKCE**. Scalekit then rejects authorization requests from that application that don’t include a code challenge. ## Organizations that share an identity provider [Section titled “Organizations that share an identity provider”](#organizations-that-share-an-identity-provider) Each SAML connection has its own SP entity ID, ACS URL and SP metadata URL, and each contains the connection’s ID. Scalekit identifies the connection from the ACS URL a response is posted to, not from the IdP’s entity ID. Two organizations can each have a SAML connection to the same IdP entity ID. The second connection doesn’t replace the first. In their IdP, the customer creates one SAML app per organization and enters that connection’s SP values in each. ## Directory sync (SCIM) [Section titled “Directory sync (SCIM)”](#directory-sync-scim) * **One endpoint and token per directory.** Each directory has its own SCIM endpoint URL and bearer token. Scalekit shows the token once, when it’s generated. Regenerate it to rotate it. * **Signed webhooks.** Every webhook Scalekit sends to your app is signed with your endpoint’s signing secret. Verify the signature and reject requests more than 5 minutes old. See [Verify webhook signatures](/guides/webhooks-best-practices/#verify-webhook-signatures). * **Recovery after downtime.** See [Recover from missed events](/guides/webhooks-best-practices/#recover-from-missed-events). ## Export connection configuration [Section titled “Export connection configuration”](#export-connection-configuration) Read a connection’s full configuration through the API with [Get a connection](/apis/#tag/connections). For a SAML connection, the response includes the IdP entity ID, SSO and logout URLs, metadata URL, bindings, signing options and the IdP certificates. Each certificate is the full base64-encoded certificate body, with its issue and expiry dates. Add `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` lines to use it as a PEM file. For an OIDC connection, the response includes the issuer, endpoint URLs, scopes and client ID. The client secret is masked. ## Certificate expiry and rotation [Section titled “Certificate expiry and rotation”](#certificate-expiry-and-rotation) Identity providers sign SAML responses with a certificate that expires on a date the IdP sets. How a rotation reaches Scalekit depends on how your customer configured the connection: * **With an IdP metadata URL (recommended)**: Scalekit reads the IdP’s metadata on every sign-in, so when your customer’s IdP switches to a new signing certificate, Scalekit uses it right away. Nobody needs to update the connection. * **With a manually entered certificate**: Scalekit uses the certificate saved on the connection. Once it expires, sign-in through the connection fails with `invalid_saml_metadata` until your customer’s IT admin enters the new certificate in the [admin portal](/authenticate/sso/admin-portal/), or you update the connection in the dashboard. ## Data residency [Section titled “Data residency”](#data-residency) Choose the region when you create your workspace. US workspaces run in Google Cloud `us-west2`, and EU workspaces run in `europe-west3`. Each region is a separate deployment with its own databases, and data isn’t replicated between them. SSO connection configuration, directory data, auth logs and backups for a workspace stay in its region. See [Choose a region](/dev-kit/guides/dashboard/environments/#choose-a-region-us-or-eu). ## Compliance reports [Section titled “Compliance reports”](#compliance-reports) * **SOC 2 Type II.** Scalekit is audited annually against the SOC 2 Security criteria. * **Penetration testing.** A third party tests Scalekit annually. * **Getting the reports.** Request the SOC 2 Type II report and the latest penetration test report from the [Scalekit Trust Center](https://trustcenter.scalekit.com). One mutual NDA covers both. * **HIPAA.** To sign a Business Associate Agreement (BAA), [contact us](/support/contact-us/). ## Dashboard access [Section titled “Dashboard access”](#dashboard-access) Only people your Admins invite can open your workspace in the Scalekit dashboard, and each person’s role controls what they can view or change. See [Team members and roles](/dev-kit/guides/dashboard/manage-team-members/). SSO for the Scalekit dashboard To sign your team in to the dashboard through your own identity provider with SAML or OIDC, [contact us](/support/contact-us/). --- # DOCUMENT BOUNDARY --- # Introduction to Single Sign-on > Learn to basics of Single Sign-On (SSO), including how SAML and OIDC protocols work, and how Scalekit simplifies enterprise authentication. Single Sign-On (SSO) streamlines user access by enabling a single authentication event to grant access to multiple applications with the same credentials. For example, logging into one Google service, such as Gmail, automatically authenticates you to YouTube, Google Drive, and other Google platforms. There are two key benefits to the users and organizations with a secure single sign-on implementation: 1. User can seamlessly access multiple applications using only one set of credentials. 2. User credentials are managed in a centralized identity system. This enables Admins to easily configure and manage authentication policies for all their users from the centralized identity provider. Furthermore, this integrated SSO mechanism enhances user convenience, boosts productivity, and reduces the risks associated with password fatigue and reuse. These security & administration benefits are driving factors for enterprise organizations to only procure SaaS applications that offer SSO-based authentication. ## Understand how Single Sign-On works [Section titled “Understand how Single Sign-On works”](#understand-how-single-sign-on-works) Fundamentally, Single Sign-on works by exchanging user information in a pre-determined format between two trusted parties - your application and your customer’s identity provider (aka IdP). Most of these interactions happen in the browser context as some steps need user intervention. To ensure secure exchange of user information between your application and your customer’s identity provider, most IdPs support two protocols: Secure Assertion Markup Language (SAML) or OpenID Connect (OIDC). The objective of both these protocols is same: allow secure user information exchange between the Service Provider (your application) and Identity Provider (your customer’s identity system). These protocols differ in how these systems trust each other, communicate, and exchange user information. Let’s understand these protocols at a high level. ## Understanding SAML protocol [Section titled “Understanding SAML protocol”](#understanding-saml-protocol) SAML 2.0 (Secure Assertion Markup Language) has been in use since 2005 and is also most widely implemented protocol. SAML exchanges user information using XML files via HTTPS or SOAP. But, before the user information is exchanged between the two parties, they need to establish the trust between them. Trust is established by exchanging information about each other as part of SAML configuration parameters like Assertion Consumer Service URL (ACS URL), Entity ID, X.509 Certificates, etc. After the trust has been established, subsequent user information can be exchanged in two ways - 1. Your application requesting for a user’s information - this is Service Provider initiated login flow 2. Or the identity provider directly shares user details via a pre-configured ACS URL - this is Identity Provider initiated login flow Let’s understand these two SSO flows. ### Implement Service Provider initiated flow [Section titled “Implement Service Provider initiated flow”](#implement-service-provider-initiated-flow) ![SP initiated SSO workflow](/.netlify/images?url=_astro%2F1.DdT6sA5U.png\&w=3536\&h=2644\&dpl=6ae471fc143c9a4c27550463) For service provider initiated SSO flow, 1. User tries to access your application and your app identifies that the user’s credentials need to be verified by their identity provider. 2. Your application requests the identity provider for the user’s information. 3. The identity provider authenticates the user and returns user details as “assertions” to your application. 4. You validate assertions, retrieve the user information, and if everything checks, allow the user to successfully login to your application. As you can imagine, in this workflow, the user login behaviour starts from your application and that’s why this is termed as service provider initiated SSO (aka SP-initiated SSO) ### Implement Identity Provider initiated flow [Section titled “Implement Identity Provider initiated flow”](#implement-identity-provider-initiated-flow) ![IdP initiated SSO workflow](/.netlify/images?url=_astro%2F2-idp-init-sso.CAu--K_L.png\&w=3536\&h=2168\&dpl=6ae471fc143c9a4c27550463) In case of Identity Provider initiated SSO, 1. User logs into their identity provider portal and selects your application from within the IdP portal. 2. Identity Provider sends the user details as assertions to your application. 3. You validate assertions, retrieve the user information, and if everything checks, allow the user to successfully login to your application. Since the user login workflow starts from the Identity Provider portal (and not from your application), this flow is called Identity Provider initiated SSO (aka IdP-initiated SSO). #### Mitigate security risks [Section titled “Mitigate security risks”](#mitigate-security-risks) IdP initiated SSO is susceptible to common security attacks like Man In the Middle attack, Stolen Assertion attack or Assertion Replay attack etc. Read the [IdP initiated SSO](/sso/guides/idp-init-sso) guide to understand these risks and how to mitigate them. Advanced SAML options Some organizations require **encrypted SAML assertions**, where the IdP encrypts the assertion so only the service provider can decrypt it. Others require **signed authentication requests**—the service provider signs the `AuthnRequest` sent to the IdP. Scalekit supports both on SAML connections; contact for help aligning IdP settings (e.g., Okta or Microsoft Entra ID). ## Understanding OIDC protocol [Section titled “Understanding OIDC protocol”](#understanding-oidc-protocol) OpenID Connect (OIDC) is an authentication protocol based on top of OAuth 2.0 to simplify the user information exchange process between Relying Party (your application) and the OpenID Provider (your customer’s Identity Provider). The OIDC protocol exchanges user information via signed JSON Web Tokens (JWT) over HTTPS. Because of the simplified nature of handling JWTs, it is often used in modern web applications, native desktop clients and mobile applications. With the latest extensions to the OIDC protocol like Proof Key of Code Exchange (PKCE) and Demonstrating Proof of Possession (DPoP), the overall security of user exchange information is strengthened. In its current format, OIDC only supports SP initiated Login. In this flow: 1. User tries to access your application. You identify that this user’s credentials need to be verified by their Identity Provider. 2. Your application requests the user’s Identity Provider for the user’s information via an OAuth2 request. 3. Identity Provider authenticates the user and sends the user’s details with an authorization_code to a pre-registered redirect_url on your server. 4. You will exchange the code for the actual user details by providing your information with the Identity provider. 5. Identity Provider will then send the user information in the form of JWTs. You retrieve the user information from those assertions and if everything is valid, you will allow the user inside your application. #### Simplify SSO with Scalekit [Section titled “Simplify SSO with Scalekit”](#simplify-sso-with-scalekit) Scalekit serves as an intermediary, abstracting the complexities involved in handling SSO with SAML and OIDC protocols. By integrating with Scalekit in just a few lines of code, your application can connect with numerous IdPs efficiently, ensuring security and compliance. --- # DOCUMENT BOUNDARY --- # Map user attributes to IdP > Learn how to add and map custom user attributes, such as an employee number, from an Identity Provider (IdP) like Okta using Scalekit. Scalekit simplifies Single Sign-On (SSO) by managing user information between Identity Providers (IdPs) and B2B applications. The IdPs provide standard user properties, such as `email` and `firstname`, to your application, thus helping recognize the user. Consider a scenario where you want to get the employee number of the user logging into the application. This guide demonstrates how to add your own custom attribute (such as `employee_number`) and map its value from the Identity Provider. Broadly, we’ll go through two steps: 1. Create a new attribute in Scalekit 2. Set up the value that the Identity Provider should relay to this attribute ## Create a new attribute [Section titled “Create a new attribute”](#create-a-new-attribute) Let’s begin by signing into the Scalekit dashboard: 1. Navigate to **Dashboard > SSO > User Attributes** 2. Click **Add Attribute** 3. Add “Employee Number” as Display name ![add attribute](/.netlify/images?url=_astro%2F1-add-attribute-scalekit.ChxO8Ovm.png\&w=1146\&h=600\&dpl=6ae471fc143c9a4c27550463) You’ll now notice “Employee Number” in the list of user attributes. Scalekit is now ready to receive this attribute from your customers’ Identity Providers (IdPs). ![see attribute](/.netlify/images?url=_astro%2F2.42Rj4Bw-.png\&w=2786\&h=1746\&dpl=6ae471fc143c9a4c27550463) ## Set up IdP attributes Okta example [Section titled “Set up IdP attributes ”](#set-up-idp-attributes-) Now, we’ll set up an Identity Provider to send these details. For the purposes of this guide, we’ll use Okta as IdP to send the `employee_number` to Scalekit. However, similar functionality can be achieved using any other IdP. Note that in this specific Okta instance, the “Employee Number” is a default attribute that hasn’t been utilized yet. Before you proceed forward, it’s important to modify the profile’s `employee_number` attribute with any desired number for this example (for example, `1729`). For a detailed guide on how to achieve this, consult [Okta’s dedicated help article on updating profile attributes](https://help.okta.com/en-us/content/topics/users-groups-profiles/usgp-edit-user-attributes.htm#:~:text=Click%20the%20Profile%20tab). Alternatively, you can [add a new custom attribute in the Okta Profile Editor](https://help.okta.com/en-us/content/topics/users-groups-profiles/usgp-add-custom-user-attributes.htm#:~:text=In%20the%20Admin%20Console%20%2C%20go%20to%20Directory%20Profile%20Editor). ![map attribute](/.netlify/images?url=_astro%2F3-map-attribute-okta.CtVAf_eI.png\&w=2764\&h=1578\&dpl=6ae471fc143c9a4c27550463) ## Test SSO for new attributes [Section titled “Test SSO for new attributes”](#test-sso-for-new-attributes) In the Scalekit dashboard, navigate to **Dashboard > Organizations**. 1. Select the organization that you’d like to add custom attribute to 2. Navigate to the SSO Connection 3. Click **Test Connection** - you’ll find this if the IdP has already been established ![map attr scalekit](/.netlify/images?url=_astro%2F4-map-attribute-scalekit.BYU0mngo.png\&w=1978\&h=1520\&dpl=6ae471fc143c9a4c27550463) Upon testing the connection, if you notice the updated user profile (`employee_number` as `1729` in this example), this signifies a successful test. Subsequently, these details will be integrated into your B2B application through Scalekit. This ensures seamless recognition and handling of customer user attributes during the SSO authentication process. ## Reserved attribute names [Section titled “Reserved attribute names”](#reserved-attribute-names) Some attribute names are **reserved by Scalekit** and must not be used for custom attributes. Using a reserved name causes silent failures — the custom attribute value is silently dropped or overwritten during SSO. | Name | Purpose | | ----------------------------------- | --------------------------------------------------------- | | `roles` | Used by Scalekit for FSA role-based access control (RBAC) | | `permissions` | Used by Scalekit for FSA permissions | | `email` | Standard claim — always populated from IdP | | `email_verified` | Standard claim | | `name`, `given_name`, `family_name` | Standard profile claims | | `sub`, `oid`, `sid` | Internal Scalekit identifiers | If your IdP sends an attribute named `roles`, it **will not** appear as a custom attribute in the JWT. Instead, rename it to something unique (e.g., `user_role` or `idp_roles`) in both Scalekit and your IdP attribute mapping. ## Access custom attributes from the ID token [Section titled “Access custom attributes from the ID token”](#access-custom-attributes-from-the-id-token) After configuring a custom attribute in Scalekit, its value appears in the ID token as a JWT claim. Use the Scalekit SDK to validate the token and read the claim: * Node.js Read custom attributes from ID token ```typescript import type { IdTokenClaim } from '@scalekit-sdk/node'; // Validate the ID token and cast to include your custom attributes const claims = await scalekit.validateToken>(idToken); const employeeNumber = claims['employee_number']; const userRole = claims['user_role']; // use 'user_role', not 'roles' ``` * Python Read custom attributes from ID token ```python # Validate the ID token — returns a dict of all claims claims = scalekit_client.validate_token(id_token) employee_number = claims.get('employee_number') user_role = claims.get('user_role') # use 'user_role', not 'roles' ``` * Go Read custom attributes from ID token ```go // Validate the ID token — returns a map of all claims claims, err := scalekitClient.ValidateToken(ctx, idToken) if err != nil { log.Fatal(err) } employeeNumber := claims["employee_number"] userRole := claims["user_role"] // use "user_role", not "roles" ``` * Java Read custom attributes from ID token ```java import java.util.Map; // Validate the ID token — returns a map of all claims Map claims = scalekitClient.authentication().validateToken(idToken); Object employeeNumber = claims.get("employee_number"); Object userRole = claims.get("user_role"); // use "user_role", not "roles" ``` --- # DOCUMENT BOUNDARY --- # SSO simulator > Test Enterprise SSO based authentication using our SSO Simulator without configuring SAML or OIDC based SSO with a real IdP After implementing Single Sign-On using our [Quickstart guide](/authenticate/sso/add-modular-sso/), you need to validate your integration for all possible scenarios. This guide shows you how to test your SSO implementation using two approaches: 1. **SSO Simulator (quick testing):** Test all SSO scenarios without external services. Your development environment includes a pre-configured test organization with an SSO connection to our SSO Simulator. 2. **Real identity provider (production-ready testing):** Test with actual identity providers like Okta or Microsoft Entra ID to simulate real customer scenarios. To ensure a successful SSO implementation, test all three scenarios described in this guide before deploying to production: SP-initiated SSO, IdP-initiated SSO, and error handling. ## Testing with SSO Simulator Quick testing [Section titled “Testing with SSO Simulator ”](#testing-with-sso-simulator-) The SSO Simulator allows you to test all SSO scenarios without requiring external services. Your development environment includes a pre-configured test organization with an SSO connection to our SSO Simulator and test domains like `@example.com` or `@example.org`. To locate the test organization, navigate to **Dashboard > Organizations** and select **Test Organization**. ![Test Organization](/.netlify/images?url=_astro%2F2.CCYEcEtj.png\&w=2786\&h=1746\&dpl=6ae471fc143c9a4c27550463) ### Service provider (SP) initiated SSO Scenario 1 [Section titled “Service provider (SP) initiated SSO ”](#service-provider-sp-initiated-sso-) In this common scenario, users start the Single Sign-On process from your application’s login page. ![SP initiated SSO](/.netlify/images?url=_astro%2F1.Bn8Ae4ZM.png\&w=4936\&h=3744\&dpl=6ae471fc143c9a4c27550463) #### Generate authorization URL [Section titled “Generate authorization URL”](#generate-authorization-url) Generate an authorization URL with your test organization ID. This redirects users to Scalekit’s hosted login page, which will then redirect to the SSO Simulator. * Node.js Express.js ```javascript // Use your test organization ID from the dashboard const options = { organizationId: 'org_32656XXXXXX0438' // Replace with your test organization ID }; // Generate Authorization URL that redirects to SSO Simulator const authorizationURL = scalekit.getAuthorizationUrl(redirectUrl, options); // Redirect user to start SSO flow res.redirect(authorizationURL); ``` * Python Flask ```python # Use your test organization ID from the dashboard options = { "organizationId": 'org_32656XXXXXX0438' # Replace with your test organization ID } # Generate Authorization URL that redirects to SSO Simulator authorization_url = scalekit_client.get_authorization_url( redirect_url, options, ) # Redirect user to start SSO flow return redirect(authorization_url) ``` * Go Gin ```go // Use your test organization ID from the dashboard options := scalekit.AuthorizationUrlOptions{ OrganizationId: "org_32656XXXXXX0438", // Replace with your test organization ID } // Generate Authorization URL that redirects to SSO Simulator authorizationURL := scalekitClient.GetAuthorizationUrl( redirectUrl, options, ) // Redirect user to start SSO flow c.Redirect(http.StatusFound, authorizationURL) ``` * Java Spring Boot ```java // Use your test organization ID from the dashboard AuthorizationUrlOptions options = new AuthorizationUrlOptions(); options.setOrganizationId("org_32656XXXXXX0438"); // Replace with your test organization ID // Generate Authorization URL that redirects to SSO Simulator String authorizationURL = scalekitClient .authentication() .getAuthorizationUrl(redirectUrl, options) .toString(); // Redirect user to start SSO flow return "redirect:" + authorizationURL; ``` Find your organization ID Your test organization ID is displayed in the organization details page at **Dashboard > Organizations > Test Organization**. #### Test the SSO flow [Section titled “Test the SSO flow”](#test-the-sso-flow) After generating the authorization URL, users are redirected to the SSO Simulator: 1. Select **User login via SSO** from the dropdown menu 2. Enter test user details (email, name, etc.) to simulate authentication 3. Click **Submit** to complete the simulation ![SSO Simulator form](/.netlify/images?url=_astro%2F2.1.BEM1Vo-J.png\&w=2646\&h=1652\&dpl=6ae471fc143c9a4c27550463) After submitting the form, your application receives an `idToken` containing the user details you entered: ![ID token response](/.netlify/images?url=_astro%2F2.2.tePTMu6U.png\&w=2182\&h=1146\&dpl=6ae471fc143c9a4c27550463) Custom user attributes To test custom attributes from the SSO Simulator, first register them at **Dashboard > Development > Single Sign-On > Custom Attributes**. ### Identity provider (IdP) initiated SSO Scenario 2 [Section titled “Identity provider (IdP) initiated SSO ”](#identity-provider-idp-initiated-sso-) In this scenario, users start the sign-in process from their identity provider (typically through an applications catalog) rather than from your application’s login page. Your application must handle this flow by detecting IdP-initiated requests and converting them to SP-initiated SSO. If you haven’t implemented IdP-initiated SSO yet, follow our [IdP-initiated SSO implementation guide](/sso/guides/idp-init-sso) before testing this scenario. ![How IdP-initiated SSO works](/.netlify/images?url=_astro%2F4.DI1M7pT-.png\&w=4936\&h=4432\&dpl=6ae471fc143c9a4c27550463) #### Test IdP-initiated SSO flow [Section titled “Test IdP-initiated SSO flow”](#test-idp-initiated-sso-flow) 1. Generate the authorization URL using your test organization 2. When redirected to the SSO Simulator, select **IdP initiated SSO** from the dropdown menu 3. Enter test user details to simulate the login 4. Click **Submit** to complete the simulation ![IdP initiated SSO form](/.netlify/images?url=_astro%2F3.1.CmRUnvaS.png\&w=2530\&h=1656\&dpl=6ae471fc143c9a4c27550463) #### Verify callback handling [Section titled “Verify callback handling”](#verify-callback-handling) Your callback handler receives the IdP-initiated request and must process it correctly: ![IdP initiated callback](/.netlify/images?url=_astro%2F3.2.D4V_v_y-.png\&w=2024\&h=486\&dpl=6ae471fc143c9a4c27550463) Your application should: 1. Detect the IdP-initiated request based on the request parameters 2. Retrieve connection details (`connection_id` or `organization_id`) from Scalekit 3. Generate a new authorization URL to convert the IdP-initiated flow to SP-initiated SSO 4. Complete the authentication flow Testing notes * The SSO Simulator uses your default redirect URL as the callback URL. Ensure this is correctly configured at **Dashboard > Developers > Redirect URLs**. * In production, users would select your application from their identity provider’s app catalog to initiate this flow. ### Error handling Scenario 3 [Section titled “Error handling ”](#error-handling-) Your application should gracefully handle error scenarios to provide a good user experience. SSO failures can occur due to misconfiguration, incomplete user profiles, or integration issues. #### Test error scenarios [Section titled “Test error scenarios”](#test-error-scenarios) 1. Generate and redirect to the authorization URL 2. In the SSO Simulator, select **Error** from the dropdown menu 3. Verify your callback handler processes the error correctly 4. Ensure users see an appropriate error message ![Error scenario in SSO Simulator](/.netlify/images?url=_astro%2F5.DIgPtBxP.png\&w=2364\&h=1216\&dpl=6ae471fc143c9a4c27550463) Error handling best practices Review the complete list of [SSO integration error codes](/sso/reference/sso-integration-errors/) to implement comprehensive error handling in your application. ## Testing with real identity providers Production-ready [Section titled “Testing with real identity providers ”](#testing-with-real-identity-providers-) After validating your SSO implementation with the SSO Simulator, test with real identity providers like Okta or Microsoft Entra ID to simulate actual customer scenarios. This ensures your integration works correctly with production identity systems. ### Setup your test environment [Section titled “Setup your test environment”](#setup-your-test-environment) To simulate a real customer onboarding scenario, create a new organization with a real SSO connection: 1. Create an organization at **Dashboard > Organizations** with a name that reflects a test customer 2. Generate an **Admin Portal link** from the organization’s overview page 3. Open the Admin Portal link and follow the integration guide to set up an SSO connection: * [Okta SAML integration guide](/guides/integrations/sso-integrations/okta-saml/) * [Microsoft Entra ID integration guide](/guides/integrations/sso-integrations/azure-ad-saml/) * [Other SSO integrations](/guides/integrations/) Customize the admin portal You can [customize the Admin Portal](/authenticate/sso/admin-portal/#customize-the-admin-portal) with your application’s branding to provide a polished experience for your customers. Free Okta developer account If you don’t have access to an identity provider, sign up for a free [Okta developer account](https://developer.okta.com/signup/) to test SSO integration. ### Service provider (SP) initiated SSO Scenario 1 [Section titled “Service provider (SP) initiated SSO ”](#service-provider-sp-initiated-sso--1) Test the most common SSO scenario where users start the authentication flow from your application’s login page. ![SP initiated SSO workflow](/.netlify/images?url=_astro%2F1.Bn8Ae4ZM.png\&w=4936\&h=3744\&dpl=6ae471fc143c9a4c27550463) #### Validate the flow [Section titled “Validate the flow”](#validate-the-flow) 1. **Generate authorization URL**: Create an authorization URL with your test organization’s ID (see [Authorization URL documentation](/sso/guides/authorization-url/)) 2. **User authentication**: Verify that Scalekit redirects users to the correct identity provider 3. **Callback handling**: Confirm your application receives the authorization code at your redirect URI 4. **Token exchange**: Verify you can exchange the authorization code for user details and tokens 5. **Session creation**: Ensure your application creates a session and logs the user in successfully Your application should successfully retrieve user details including email, name, and any custom attributes configured in the SSO connection. ### Identity provider (IdP) initiated SSO Scenario 2 [Section titled “Identity provider (IdP) initiated SSO ”](#identity-provider-idp-initiated-sso--1) Test the scenario where users start authentication from their identity provider’s application catalog. ![IdP-initiated SSO workflow](/.netlify/images?url=_astro%2Fidp-initiated-sso.v3FnpBpw.png\&w=3536\&h=2168\&dpl=6ae471fc143c9a4c27550463) #### Validate the flow [Section titled “Validate the flow”](#validate-the-flow-1) 1. **Initial callback**: User is redirected to your default redirect URI with IdP-initiated request parameters 2. **Detection logic**: Your application detects this as an IdP-initiated request (based on the request parameters) 3. **SP-initiated conversion**: Your application initiates SP-initiated SSO by generating an authorization URL 4. **IdP redirect**: User is redirected to the identity provider based on the authorization URL 5. **Final callback**: After authentication, user is redirected back with an authorization code and state parameter 6. **Token exchange**: Exchange the code for user details and complete the login For implementation details, see our [IdP-initiated SSO implementation guide](/sso/guides/idp-init-sso/). Default redirect URL configuration Ensure your default redirect URL is correctly configured at **Dashboard > Developers > Redirect URLs**. This URL receives IdP-initiated requests. ### Error handling Scenario 3 [Section titled “Error handling ”](#error-handling--1) Test how your application handles SSO failures. Common error scenarios include: * Misconfigured SSO connections (wrong certificates, invalid metadata) * Incomplete user profiles (missing required attributes) * Expired or revoked SSO connections * Network or integration issues with the identity provider #### Validate error handling [Section titled “Validate error handling”](#validate-error-handling) 1. Review the [SSO integration error codes](/sso/reference/sso-integration-errors/) documentation 2. Test each applicable error scenario by intentionally misconfiguring your SSO connection 3. Verify your application displays appropriate, user-friendly error messages 4. Ensure errors are logged for debugging purposes 5. Confirm users can retry authentication or contact support Error logging Implement comprehensive error logging to help diagnose SSO issues quickly. Include the error code, timestamp, organization ID, and connection ID in your logs. ## Next steps [Section titled “Next steps”](#next-steps) After thoroughly testing your SSO implementation: 1. Review the [SSO launch checklist](/sso/guides/launch-checklist/) to ensure production readiness 2. Configure the [Admin Portal](/authenticate/sso/admin-portal/) for your customers to self-serve SSO setup 3. Implement [custom domain](/guides/custom-domain/) for a seamless branded experience 4. Set up [webhooks](/authenticate/implement-workflows/implement-webhooks/) to receive real-time authentication events --- # DOCUMENT BOUNDARY --- # Normalized user profile > Learn how Scalekit's normalized user profiles standardize identity data across providers, streamlining single sign-on (SSO) integration and user management. When a user logs in with SSO, each identity provider shares the user profile information in their own format. This adds complexity for the application developers to parse the user profile info and code related identity workflows. To make this seamless for developers, Scalekit normalizes the user profile info into a standard set of fields across all identity providers. This means that you’d always receive the user profile payload in a fixed set of fields, irrespective of the identity provider and protocol you interact with. This is one of our foundational aspects of the unified SSO solution. Sample normalized user profile ```json 1 { 2 "email": "john.doe@acmecorp.com", 3 "email_verified": true, 4 "family_name": "Doe", 5 "given_name": "John", 6 "locale": "en", 7 "name": "John Doe", 8 "picture": "https://lh3.googleusercontent.com/a/ACg8ocKNE4TZ...iEma17URCEf=s96-c", 9 "sub": "conn_17576372041941092;google-oauth2|104630259163176101050", 10 "identities": [ 11 { 12 "connection_id": "conn_17576372041941092", 13 "organization_id": "org_17002852291444836", 14 "connection_type": "OIDC", 15 "provider_name": "AUTH0", 16 "social": false, 17 "provider_raw_attributes": { 18 "aud": "ztTgHijLLguDXJQab0oiPyIcDLXXrJX6", 19 "email": "john.doe@acmecorp.com", 20 "email_verified": true, 21 "exp": 1714580633, 22 "family_name": "Doe", 23 "given_name": "John", 24 "iat": 1714544633, 25 "iss": "https://dev-rmmfmus2g7vverbf.us.auth0.com/", 26 "locale": "en", 27 "name": "John Doe", 28 "nickname": "john.doe", 29 "nonce": "Lof9SpxEzs9dhUlJzgrrbQ==", 30 "picture": "https://lh3.googleusercontent.com/a/ACg8ocKNE4T...17URCEf=s96-c", 31 "sid": "5yqRJIfjPh8c7lr1s2N-IbY6WR8VyaIZ", 32 "sub": "google-oauth2|104630259163176101050", 33 "updated_at": "2024-04-30T10:02:30.988Z" 34 } 35 } 36 ] 37 } ``` ## Full list of user profile attributes [Section titled “Full list of user profile attributes”](#full-list-of-user-profile-attributes) | Profile attribute | Data type | Description | | ----------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `sub` | string | The connection ID and the user’s ID at the identity provider, joined by `;`. See [How `sub` is built](#how-sub-is-built). | | `email` | string | The user’s email address. | | `email_verified` | boolean | True if the user’s e-mail address has been verified as claimed by the identity provider; otherwise false. | | `name` | string | Fully formatted user’s name | | `family_name` | string | The user’s surname or last name. | | `given_name` | string | The user’s given name or first name. | | `locale` | string | The user’s locale, represented by a BCP 47 language tag. Example: ‘en’ | | `picture` | string | The user’s profile picture in URL format | | `identities` | Array of [Identity objects](/sso/guides/user-profile-details/#identity-object-attributes) | Array of all identity information received from the identity providers in the raw format | ### Identity object attributes [Section titled “Identity object attributes”](#identity-object-attributes) | Identity attribute | Data type | Description | | ------------------------- | --------- | ----------------------------------------------------------------------------------------------------- | | `organization_id` | string | Unique ID of the organization to which this user belongs to | | `connection_id` | string | Unique ID of the connection for which this identity data is fetched from | | `connection_type` | string | type of the connection: SAML or OIDC | | `provider_name` | string | name of the connection provider. Example: Okta, Google, Auth0 | | `social` | boolean | Is the connection a social provider (like Google, Microsoft, GitHub etc) or an enterprise connection. | | `provider_raw_attributes` | object | key-value map of all the raw attributes received from the connection provider as-is | Note * The identities array may contain multiple objects if the user has authenticated through different methods. * The `provider_raw_attributes` object contains all original data from the identity provider, which may vary based on the provider and connection type. ## How `sub` is built [Section titled “How sub is built”](#how-sub-is-built) `sub` has the form `;`, for example `conn_17576372041941092;00u1a2b3c4d5e6f7g8h9`. The IdP user ID depends on the connection type: | Connection type | IdP user ID in `sub` | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | SAML | The `NameID` value in the assertion’s `Subject`. Scalekit rejects a SAML response without a `NameID` with `mandatory_attribute_missing`, so `sub` is always present. | | OIDC | The `sub` claim from the identity provider. | ## What `provider_raw_attributes` contains [Section titled “What provider_raw_attributes contains”](#what-provider_raw_attributes-contains) `provider_raw_attributes` holds the identity provider’s data before Scalekit maps it to the fields above: * **SAML**: every attribute in the assertion’s attribute statements, keyed by the attribute `Name` exactly as the IdP sent it, plus the `NameID` under the key `nameid`. An attribute with one value is a string; an attribute with several values, and `groups`, is an array. * **OIDC**: every claim in the identity provider’s ID token. For some providers, such as Okta, Scalekit reads the claims from the provider’s userinfo endpoint instead, because their ID tokens leave out attributes like groups. ## Choose a stable user identifier [Section titled “Choose a stable user identifier”](#choose-a-stable-user-identifier) Store the identifier that stays the same for a user across sign-ins: * **Key users on the organization and the IdP user ID**, not on the full `sub`. `sub` starts with the connection ID, so it changes if your customer deletes their SSO connection and creates a new one. * **Check which value the IdP sends as the user ID.** A SAML `NameID` in persistent format, or an OIDC `sub`, stays the same for a user. A `NameID` that holds an email address or user principal name changes when the user is renamed. ### Microsoft Entra ID [Section titled “Microsoft Entra ID”](#microsoft-entra-id) * **SAML**: Entra ID sends the user principal name as the `NameID` by default. Use the object ID instead: read it from the `http://schemas.microsoft.com/identity/claims/objectidentifier` attribute in `provider_raw_attributes`, or ask your customer to set the **Unique User Identifier (Name ID)** claim to `user.objectid` in their Entra ID app. * **OIDC**: Entra ID’s `sub` claim is unique to each app registration. To match the same user across apps, or with directory data, read the `oid` claim from `provider_raw_attributes`. It’s the user’s object ID in Entra ID. --- # DOCUMENT BOUNDARY --- # SSO connection events > Webhook events Scalekit sends when an organization's SSO connection is created, enabled, disabled or deleted, with sample payloads and fields. Scalekit sends a webhook when an organization’s SSO connection is created, enabled, disabled or deleted. Use these events to keep your app’s record of which organizations can sign in with SSO in step with Scalekit, for example to show or hide an SSO button or to require SSO for an organization’s users. To receive them, add an endpoint in **Developers** > **Webhooks** and select the events. See [Implement webhooks](/authenticate/implement-workflows/implement-webhooks/), and verify every delivery as described in [Verify webhook signatures](/guides/webhooks-best-practices/#verify-webhook-signatures). | Event | Sent when | | -------------------------------------------------------- | ----------------------------------------------------------------------------- | | [`organization.sso_created`](#organizationsso_created) | An SSO connection is created for an organization | | [`organization.sso_enabled`](#organizationsso_enabled) | An SSO connection is enabled, so the organization’s users can sign in with it | | [`organization.sso_disabled`](#organizationsso_disabled) | An SSO connection is disabled, so its users can no longer sign in with it | | [`organization.sso_deleted`](#organizationsso_deleted) | An SSO connection is deleted | Scalekit doesn’t send a webhook when a connection’s configuration changes, such as its IdP metadata, certificate or attribute mapping. To read a connection’s current configuration, call [Get connection details](/apis/#tag/connections). For how IdP certificate rotation works, see [Certificate expiry and rotation](/sso/guides/security-and-compliance/#certificate-expiry-and-rotation). ## Event envelope [Section titled “Event envelope”](#event-envelope) Every SSO connection event has the same top-level fields. The connection is in `data`. Keys can arrive in any order, so parse the body as JSON rather than matching it as text. | Field | Type | Description | | ----------------- | ------ | ---------------------------------------------------------------------------- | | `spec_version` | string | Webhook payload version. Currently `"1"` | | `id` | string | Unique event ID, prefixed with `evt_`. Use it to ignore duplicate deliveries | | `type` | string | Event type, such as `organization.sso_enabled` | | `occurred_at` | string | When the event occurred, in RFC 3339 format | | `environment_id` | string | Environment the event belongs to | | `organization_id` | string | Organization the connection belongs to | | `object` | string | Always `Connection` | | `data` | object | The connection. All four events carry the same fields | ### Connection fields in `data` [Section titled “Connection fields in data”](#connection-fields-in-data) | Field | Type | Description | | ----------------- | --------------- | ----------------------------------------------------------------------------------------------------------- | | `id` | string | Connection ID, prefixed with `conn_`. It’s the `connection_id` you pass in an authorization URL | | `organization_id` | string | Organization that owns the connection | | `connection_type` | string | `SAML` or `OIDC` | | `provider` | string | Identity provider, such as `OKTA` or `MICROSOFT_AD` | | `enabled` | boolean or null | Whether users can sign in with the connection. `null` when the value wasn’t recorded with the event | | `status` | string or null | Setup progress: `DRAFT`, `IN_PROGRESS` or `COMPLETED`. `null` when the value wasn’t recorded with the event | ## `organization.sso_created` [Section titled “organization.sso_created”](#organizationsso_created) Sent when an SSO connection is created for an organization, from the dashboard, the admin portal or the API. A new connection is usually disabled and in `DRAFT` until your customer’s IT admin finishes setup and enables it. organization.sso_created ```json 1 { 2 "spec_version": "1", 3 "id": "evt_94567862441607493", 4 "type": "organization.sso_created", 5 "occurred_at": "2025-10-14T09:27:18.488720586Z", 6 "environment_id": "env_74418471961625391", 7 "organization_id": "org_83544995172188677", 8 "object": "Connection", 9 "data": { 10 "id": "conn_94567862424830277", 11 "organization_id": "org_83544995172188677", 12 "connection_type": "OIDC", 13 "provider": "OKTA", 14 "enabled": false, 15 "status": "DRAFT" 16 } 17 } ``` ## `organization.sso_enabled` [Section titled “organization.sso_enabled”](#organizationsso_enabled) Sent when an SSO connection is enabled. From now on, the organization’s users can sign in through it. organization.sso_enabled ```json 1 { 2 "spec_version": "1", 3 "id": "evt_94568078213382471", 4 "type": "organization.sso_enabled", 5 "occurred_at": "2025-10-14T09:29:27.098914861Z", 6 "environment_id": "env_74418471961625391", 7 "organization_id": "org_83544995172188677", 8 "object": "Connection", 9 "data": { 10 "id": "conn_94567862424830277", 11 "organization_id": "org_83544995172188677", 12 "connection_type": "OIDC", 13 "provider": "OKTA", 14 "enabled": true, 15 "status": "COMPLETED" 16 } 17 } ``` ## `organization.sso_disabled` [Section titled “organization.sso_disabled”](#organizationsso_disabled) Sent when an SSO connection is disabled. Sign-in requests that target this connection now fail with `connection_not_active` or `no_active_connections` (see the [error reference](/sso/reference/sso-integration-errors/)), so route the organization’s users to another sign-in method until it’s enabled again. organization.sso_disabled ```json 1 { 2 "spec_version": "1", 3 "id": "evt_94557976165089560", 4 "type": "organization.sso_disabled", 5 "occurred_at": "2025-10-14T07:49:05.809554456Z", 6 "environment_id": "env_74418471961625391", 7 "organization_id": "org_83544995172188677", 8 "object": "Connection", 9 "data": { 10 "id": "conn_83545002856153607", 11 "organization_id": "org_83544995172188677", 12 "connection_type": "OIDC", 13 "provider": "OKTA", 14 "enabled": false, 15 "status": "COMPLETED" 16 } 17 } ``` ## `organization.sso_deleted` [Section titled “organization.sso_deleted”](#organizationsso_deleted) Sent when an SSO connection is deleted. If your customer creates a new connection, it has a new connection ID, so the `sub` of their users changes. See [Choose a stable user identifier](/sso/guides/user-profile-details/#choose-a-stable-user-identifier). organization.sso_deleted ```json 1 { 2 "spec_version": "1", 3 "id": "evt_94557997639926040", 4 "type": "organization.sso_deleted", 5 "occurred_at": "2025-10-14T07:49:18.604546332Z", 6 "environment_id": "env_74418471961625391", 7 "organization_id": "org_83544995172188677", 8 "object": "Connection", 9 "data": { 10 "id": "conn_83545002856153607", 11 "organization_id": "org_83544995172188677", 12 "connection_type": "OIDC", 13 "provider": "OKTA", 14 "enabled": false, 15 "status": "COMPLETED" 16 } 17 } ``` --- # DOCUMENT BOUNDARY --- # Error handling during single sign-on > Learn how to identify and resolve common single sign-on errors in Scalekit, ensuring a seamless authentication experience for your users Reference of error codes and how to handle them When users attempt to log in via single sign-on (SSO) using Scalekit, any issues encountered will result in error details being sent to your application’s redirect URI via the `error` and `error_description` query parameters. Proper error handling ensures a better user experience. ## Where Scalekit sends errors [Section titled “Where Scalekit sends errors”](#where-scalekit-sends-errors) Scalekit sends an SSO error to the `redirect_uri` of the sign-in request, the same URL that receives the authorization code on success. The redirect carries `error`, `error_description` and the `state` value from your [authorization URL](/sso/guides/authorization-url/). If the `redirect_uri` is missing or isn’t one of your registered redirect URLs, Scalekit can’t redirect and shows the user an error page instead. Errors from [IdP-initiated sign-in](/sso/guides/idp-init-sso/) have no sign-in request to return to, so Scalekit sends them to your initiate-login URL with the same `error` and `error_description` parameters. ### Show an organization-specific error page [Section titled “Show an organization-specific error page”](#show-an-organization-specific-error-page) Errors don’t have a per-connection destination, but you choose the `redirect_uri` and `state` on every sign-in request, so your callback knows which organization the user was signing in to. Use that to show an error page that fits the organization and tells its users how to fix the problem. * **Look up the organization from `state`.** When you build the authorization URL, store the organization ID in the user’s server-side session next to the random `state` value. In your callback, verify `state` matches, read the organization ID from the session, and render that organization’s error page. * **Or use a redirect URL per organization.** Register a separate redirect URL for each organization, such as `https://app.example.com/sso/callback/acme`, and pass it as `redirect_uri` on that organization’s sign-in requests. Keep `state` unguessable and verify it on every callback, including error callbacks. Don’t put the organization ID in `state` in place of the random value: `state` protects the callback from cross-site request forgery. ## Integration related errors [Section titled “Integration related errors”](#integration-related-errors) If there is any issue between Scalekit and your application, the following errors may occur: Tip Ideally, you would want to catch these errors in the development environments. These errors are not meant to be exposed to your customers in the production environments. | Error | Error description | Possible resolution strategy | | ----------------------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | ``` invalid_redirect_uri ``` | Redirect URI is not part of the pre-approved list of redirect URIs | Add the valid URL in the Scalekit dashboard before using it | | ``` invalid_connection_selector ``` | Missing `organization_id` (or) `connection_id` (or) `domain` (or) `provider` in the authorization URL | Include at least one of these parameters in the request | | ``` no_active_connections ``` | There are no active SSO connections configured to process the single sign-on request | Ensure active SSO connections are set up | | ``` connection_not_active ``` | The configured connection is not active | Enable the SSO connection in the Scalekit dashboard | | ``` no_configured_connections ``` | No active SSO connections configured | Ensure active SSO connections are set up | | ``` invalid_organization_id ``` | Invalid organization ID | Verify and use a valid organization ID | | ``` invalid_connection_id ``` | Invalid connection ID | Verify and use a valid connection ID | | ``` domain_not_found ``` | No domain specified for the SSO connection(s) | Check domain configuration in Scalekit dashboard | | ``` invalid_user_domain ``` | User’s domain not allowed for this SSO connection | Ensure user domain is part of the allowed domains list | | ``` invalid_client ``` | The client application is not recognized or not configured correctly | Verify the `client_id` value in your authorization URL | | ``` application_not_active ``` | The application is inactive | Enable the application in the Scalekit dashboard | | ``` invalid_request ``` | The authorization request contains invalid or missing parameters | Review the authorization URL parameters | | ``` unauthorized ``` | The request is unauthorized | Verify that valid credentials are being used | | ``` user_not_active ``` | The user account is inactive | Activate the user account or contact the IT admin | | ``` server_error ``` | *actual error description from the server* | This must be a rare occurrence. Please reach out to us via your private slack channel or [via email](mailto:support@scalekit.com) | ## SSO configuration related errors [Section titled “SSO configuration related errors”](#sso-configuration-related-errors) If SSO configuration issues arise, you will encounter the following errors: Tip Ideally, these errors should have been caught and handled by your customer’s IT admin at the time of SSO configuration. If your customers encounter problems with the single sign-on (SSO) setup, they will have the opportunity to review and correct the configuration during the “Test connection” step. Once your customer configures the SSO settings properly, tests the configuration and enables it - you shouldn’t receive these errors unless something has been modified, tampered or changed with identity provider. | Error code | Error description | Possible resolution strategy | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ``` mandatory_attribute_missing ``` | Missing mandatory user attributes | Ensure all the mandatory user attributes are configured properly | | ``` invalid_id_token ``` | Invalid ID token | Check the identity provider’s functioning | | ``` failed_to_exchange_token ``` | Token exchange failure due to incorrect `client_secret` | Update the `client_secret` with the correct value | | ``` user_info_retrieve_failed ``` | User info retrieval failed, possibly due to an incorrect `client_secret` or other issues | Update the `client_secret` with the correct value. If unsuccessful, investigate further. Please reach out to us via your private slack channel or [via email](mailto:support@scalekit.com) | | ``` invalid_saml_metadata ``` | Scalekit couldn’t use the IdP’s SAML metadata or certificate. A common cause is an expired IdP signing certificate | Check the metadata URL is correct and reachable. If the connection uses a manually entered certificate, enter the IdP’s current signing certificate. See [Certificate expiry and rotation](/sso/guides/security-and-compliance/#certificate-expiry-and-rotation) | | ``` invalid_saml_response ``` | Invalid SAML response | Review and fix SAML configuration settings | | ``` invalid_saml_request ``` | The SAML request is invalid | Check the SAML configuration in both Scalekit and the identity provider | | ``` invalid_saml_form_params ``` | The SAML form parameters are invalid or malformed | Review the SAML response format from the identity provider | | ``` signature_validation_failed ``` | Failed signature validation | Review and update the ACS URL in the identity provider’s settings | | ``` invalid_acs_url ``` | Invalid ACS URL | Review and update the ACS URL in the identity provider’s settings | | ``` invalid_assertion_url ``` | The assertion URL in the SAML request is invalid | Verify and update the ACS URL in the identity provider’s settings | | ``` invalid_status ``` | Invalid status | Review and update the SAML configuration settings in the identity provider | | ``` malformed_saml_response ``` | Marshalling error | Ensure SAML response is properly formatted | | ``` assertion_expired ``` | Expired SAML assertion | We received an expired SAML assertion. This could be because of clock skew between the identity provider’s server and our servers. Please reach out to us via your private slack channel or [via email](mailto:support@scalekit.com) | | ``` response_expired ``` | Expired SAML response | We received an expired SAML response. This could be because of clock skew between the identity provider’s server and our servers. Please reach out to us via your private slack channel or [via email](mailto:support@scalekit.com) | | ``` authentication_not_completed ``` | The authentication flow was not completed | Ensure the user completes the login process in the identity provider | | ``` user_login_required ``` | User login is required to continue | Redirect the user to the login page to complete authentication | --- # DOCUMENT BOUNDARY --- # SSO Integrations > Learn how to integrate with Scalekit's SSO feature. Scalekit provides seamless integration with all major identity providers (IdPs) to enable Single Sign-On for your application. Below you’ll find detailed guides for setting up SSO with popular providers like Okta, Microsoft Entra ID (formerly Azure AD), Google Workspace, JumpCloud, and more. Each guide walks you through the step-by-step process of configuring your IdP and connecting it to Scalekit, allowing you to quickly implement enterprise-grade authentication for your users. ### Okta - SAML Configure SSO with Okta using SAML protocol [Know more →](/guides/integrations/sso-integrations/okta-saml) ### Microsoft Entra ID - SAML Set up SSO with Microsoft Entra ID (Azure AD) using SAML [Know more →](/guides/integrations/sso-integrations/azure-ad-saml) ![JumpCloud - SAML logo](/assets/logos/jumpcloud.png) ### JumpCloud - SAML Implement SSO with JumpCloud using SAML [Know more →](/guides/integrations/sso-integrations/jumpcloud-saml) ![OneLogin - SAML logo](/assets/logos/onelogin.svg) ### OneLogin - SAML Configure SSO with OneLogin using SAML [Know more →](/guides/integrations/sso-integrations/onelogin-saml) ### Google Workspace - SAML Set up SSO with Google Workspace using SAML [Know more →](/guides/integrations/sso-integrations/google-saml) ![Ping Identity - SAML logo](/assets/logos/pingidentity.png) ### Ping Identity - SAML Configure SSO with Ping Identity using SAML [Know more →](/guides/integrations/sso-integrations/pingidentity-saml) ### Microsoft AD FS - SAML Set up SSO with Microsoft Active Directory Federation Services using SAML [Know more →](/guides/integrations/sso-integrations/microsoft-ad-fs) ![Shibboleth - SAML logo](/assets/logos/shibboleth.png) ### Shibboleth - SAML Set up SSO with Shibboleth using SAML [Know more →](/guides/integrations/sso-integrations/shibboleth-saml) ### Generic SAML Configure SSO with any SAML-compliant identity provider [Know more →](/guides/integrations/sso-integrations/generic-saml) ### Okta - OIDC Configure SSO with Okta using OpenID Connect [Know more →](/guides/integrations/sso-integrations/okta-oidc) ### Microsoft Entra ID - OIDC Set up SSO with Microsoft Entra ID using OpenID Connect [Know more →](/guides/integrations/sso-integrations/microsoft-entraid-oidc) ### Google Workspace - OIDC Set up SSO with Google Workspace using OpenID Connect [Know more →](/guides/integrations/sso-integrations/google-oidc) ![JumpCloud - OIDC logo](/assets/logos/jumpcloud.png) ### JumpCloud - OIDC Set up SSO with JumpCloud using OpenID Connect [Know more →](/guides/integrations/sso-integrations/jumpcloud-oidc) ![OneLogin - OIDC logo](/assets/logos/onelogin.svg) ### OneLogin - OIDC Set up SSO with OneLogin using OpenID Connect [Know more →](/guides/integrations/sso-integrations/onelogin-oidc) ![Ping Identity - OIDC logo](/assets/logos/pingidentity.png) ### Ping Identity - OIDC Set up SSO with Ping Identity using OpenID Connect [Know more →](/guides/integrations/sso-integrations/pingidentity-oidc) ### Generic OIDC Configure SSO with any OpenID Connect provider [Know more →](/guides/integrations/sso-integrations/generic-oidc) --- # DOCUMENT BOUNDARY --- # Microsoft Entra ID - SAML > Learn how to set up SAML-based Single Sign-On (SSO) using Microsoft Entra ID (Azure AD), with step-by-step instructions for enterprise application configuration. > Step-by-step guide to configure Single Sign-On with Microsoft Entra ID as the identity provider This guide walks you through configuring Microsoft Entra ID as your SAML identity provider for the application you are onboarding, enabling secure Single Sign-On for your users. You’ll learn how to set up an enterprise application, configure SAML settings, map user attributes, and assign users to the application. By following these steps, your users will be able to seamlessly authenticate using their Microsoft Entra ID credentials. ## Download metadata XML [Section titled “Download metadata XML”](#download-metadata-xml) 1. Sign into the SSO Configuration Portal, select **Microsoft Entra ID**, then **SAML**, and click on **Configure** Under **Service Provider Details**, click on **Download Metadata XML** ![Download Metadata XML](/.netlify/images?url=_astro%2F0.B2-Hlr-9.png\&w=2252\&h=1064\&dpl=6ae471fc143c9a4c27550463) ## Create enterprise application [Section titled “Create enterprise application”](#create-enterprise-application) 1. Login to **Microsoft Entra ID** in the [Microsoft Azure Portal](https://portal.azure.com/). Select the option for **Entra ID application** and locate the **Enterprise Applications** tab ![Locate Enterprise applications](/.netlify/images?url=_astro%2F1.BBTQIrRi.png\&w=1609\&h=1028\&dpl=6ae471fc143c9a4c27550463) 2. In the **Enterprise Applications** tab **New Application** in the top navigation bar ![Click on New application](/.netlify/images?url=_astro%2F2.CBVd35G6.png\&w=1582\&h=722\&dpl=6ae471fc143c9a4c27550463) 3. Click on **Create your own Application** and give your application a name Select the **_Integrate any other application you don’t find in the gallery (Non-gallery)_** option. Click on **Create** ![Create a new application on Entra ID](/.netlify/images?url=_astro%2F3.BElztJcS.gif\&w=1044\&h=582\&dpl=6ae471fc143c9a4c27550463) ## Configure SAML settings [Section titled “Configure SAML settings”](#configure-saml-settings) 1. Locate the **Single Sign-On** option under **Manage**, and choose **SAML** ![Locate SAML under Single sign-on](/.netlify/images?url=_astro%2F4.CpbXqvtA.png\&w=2058\&h=1302\&dpl=6ae471fc143c9a4c27550463) 2. Click on **Upload metadata file**. Upload the **Metadata XML file** downloaded in step 1 ![Click on Upload metadata file](/.netlify/images?url=_astro%2F4-5.BE2CjXIl.png\&w=1634\&h=904\&dpl=6ae471fc143c9a4c27550463) 3. Click on **Save** ![Save button](/.netlify/images?url=_astro%2F5.Omck9gZS.png\&w=1912\&h=1342\&dpl=6ae471fc143c9a4c27550463) ## Map user attributes [Section titled “Map user attributes”](#map-user-attributes) 1. Under **Attributes & Claims**, click on **Edit** ![Click on Edit](/.netlify/images?url=_astro%2F6.4JGavlLm.png\&w=2082\&h=1004\&dpl=6ae471fc143c9a4c27550463) 2. Check the **Attribute Mapping** section in the **SSO Configuration Portal**, and carefully map the same attributes on your **Entra ID** app ![SSO Configuration Portal](/.netlify/images?url=_astro%2F7.CYp7CRMD.png\&w=1840\&h=670\&dpl=6ae471fc143c9a4c27550463) ![Microsoft Entra ID](/.netlify/images?url=_astro%2F8.Cc6-NQ99.png\&w=1612\&h=932\&dpl=6ae471fc143c9a4c27550463) 3. To map new claims, click **Add a new claim** and select the claim to map. If you created a user attribute in the Admin dashboard (for example, `department`), enter that attribute name in the **Name** field. optional ![Add claims](/.netlify/images?url=_astro%2Fadd-claims.Dn14kmnJ.png\&w=2048\&h=591\&dpl=6ae471fc143c9a4c27550463) ## Assign users and groups [Section titled “Assign users and groups”](#assign-users-and-groups) 1. Go to the **Users and groups** tab, and click on **Add user/group** Here, please select all the required users or user groups that need login access to this application via Single Sign-On ![Assigning users and groups to your application](/.netlify/images?url=_astro%2F9.C4V0F3Py.gif\&w=1044\&h=582\&dpl=6ae471fc143c9a4c27550463) ## Configure metadata URL [Section titled “Configure metadata URL”](#configure-metadata-url) 1. Under **SAML Certification**, copy the link under **App Federation Metadata URL on Entra ID** ![Copy App Federation Metadata URL](/.netlify/images?url=_astro%2F10.DgcNRUHb.png\&w=2080\&h=964\&dpl=6ae471fc143c9a4c27550463) 2. Under **Identify Provider Configuration**, select **Configure using Metadata URL**, and paste it under **App Federation Metadata URL** on the **SSO Configuration Portal** ![Paste App Federation Metadata URL](/.netlify/images?url=_astro%2F11.UrmOdUzM.png\&w=2208\&h=710\&dpl=6ae471fc143c9a4c27550463) ## Test the connection [Section titled “Test the connection”](#test-the-connection) Click on **Test Connection**. If everything is done correctly, you will see a **Success** response as shown below. ![Test your SAML application for SSO](/.netlify/images?url=_astro%2F3.7zjJqSeQ.png\&w=2198\&h=978\&dpl=6ae471fc143c9a4c27550463) Note If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. ## Enable the connection [Section titled “Enable the connection”](#enable-the-connection) Click on **Enable Connection**. This will let all your selected users login to the new application via your Microsoft Entra ID SSO. ![Enable SSO on Entra ID](/.netlify/images?url=_astro%2F4.CY6-zQP7.png\&w=2194\&h=250\&dpl=6ae471fc143c9a4c27550463) With this, we are done configuring your Microsoft Entra ID application for an SSO login setup. --- # DOCUMENT BOUNDARY --- # Generic OIDC > Learn how to configure a generic OIDC identity provider for secure single sign-on (SSO) with your application. This guide walks you through configuring a generic OIDC identity provider for your application, enabling secure single sign-on for your users. You’ll learn how to set up OIDC integration, configure client credentials, and test the connection. 1. ### Configure OIDC [Section titled “Configure OIDC”](#configure-oidc) Sign into the SSO Configuration Portal, select **Custom Provider**, then **OIDC,** and click on **Configure.** ![Select Custom Provider→OIDC and then Configure](/.netlify/images?url=_astro%2F0.mFP5EFKM.png\&w=2194\&h=1238\&dpl=6ae471fc143c9a4c27550463) Copy the **Redirect URl** from the **SSO Configuration Portal**. ![Copy Redirect URI](/.netlify/images?url=_astro%2F1.BcqKGAyd.png\&w=2206\&h=460\&dpl=6ae471fc143c9a4c27550463) On your Identity Provider portal, select OIDC as the integration method, and Web Applications as application type. Paste this Redirect URI in the sign in redirect URI space on your identity provider portal. 2. ### Configure Attribute mapping [Section titled “Configure Attribute mapping”](#configure-attribute-mapping) On your identity provider portal, if attribute mapping is required, map the given attributes exactly as shown below. Tip Usually, you don’t have to configure any attributes and by default - most identity providers support standard OIDC claims to send user information as part of ID Token or User Info endpoint. ![Map exact attributes shown](/.netlify/images?url=_astro%2F2.D5WZUDQX.png\&w=2182\&h=724\&dpl=6ae471fc143c9a4c27550463) 3. ### Assign users/groups [Section titled “Assign users/groups”](#assign-usersgroups) Choose who can access the app by assigning users to your app on your identity provider portal. 4. ### Configure Identity Provider [Section titled “Configure Identity Provider”](#configure-identity-provider) Find the client ID from your identity provider portal. Paste this in the space for Client ID on your SSO Configuration Portal. ![Enter copied Client ID in the SSO Configuration Portal](/.netlify/images?url=_astro%2F3.C8fpzXVF.png\&w=2162\&h=832\&dpl=6ae471fc143c9a4c27550463) Similarly, generate and copy the Client Secret from your SSO Configuration Portal and paste it under Client Secret under IdP Configuration. ![Enter copied Client Secret in the SSO Configuration Portal](/.netlify/images?url=_astro%2F4.B1ARa6op.png\&w=2168\&h=826\&dpl=6ae471fc143c9a4c27550463) Find and copy the Issuer URL from your custom provider’s portal. Paste the above URL in the **SSO configuration Portal** under **Issuer URL**. Click on Update. ![Enter copied Issuer URL, and click Update](/.netlify/images?url=_astro%2F5.Bcd5nX-j.png\&w=2176\&h=826\&dpl=6ae471fc143c9a4c27550463) We support configuring Issuer URL field with Discovery Endpoint also. Discovery Endpoints usually end with ./well-known/openid-configuration 5. ### Finalize application [Section titled “Finalize application”](#finalize-application) Your IdP configuration section on the SSO Configuration Portal should look something like this once you’re done configuring it. ![Completed view of IdP configuration on the SSO Configuration Portal](/.netlify/images?url=_astro%2F6.qXp4akn6.png\&w=2226\&h=1170\&dpl=6ae471fc143c9a4c27550463) 6. ### Test connection [Section titled “Test connection”](#test-connection) Click on **Test Connection.** If everything is done correctly, you will see a **Success** response as shown below. If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. ![Test SSO Configuration](/.netlify/images?url=_astro%2F7.CCbftkf-.png\&w=2190\&h=982\&dpl=6ae471fc143c9a4c27550463) 7. ### Enable connection [Section titled “Enable connection”](#enable-connection) Click on **Enable Connection.** This will let all your selected users login to the new application via OIDC. ![Enable OIDC Connection](/.netlify/images?url=_astro%2F4.CY6-zQP7.png\&w=2194\&h=250\&dpl=6ae471fc143c9a4c27550463) With this, we are done configuring your application for an OIDC login setup. --- # DOCUMENT BOUNDARY --- # Generic SAML > Learn how to configure a generic SAML identity provider for secure single sign-on (SSO) with your application. This guide walks you through configuring a generic SAML identity provider for your application, enabling secure single sign-on for your users. You’ll learn how to set up a SAML application, configure service provider and identity provider settings, and test the connection. 1. ### Create a SAML application [Section titled “Create a SAML application”](#create-a-saml-application) Login to your Identity Provider portal as an admin and create a new Application with SAML as the single sign-on method. 2. ### Configure the Service Provider [Section titled “Configure the Service Provider”](#configure-the-service-provider) Depending on your Identity Provider, they may allow you to configure **Service Provider section** in your SAML application via either of the three following methods: * via SAML Metadata URL * via SAML Metadata file * via copying ACS URL and Entity ID manually #### via SAML Metadata URL [Section titled “via SAML Metadata URL”](#via-saml-metadata-url) Copy the **Metadata URL** content in your Identity Provider portal #### via SAML Metadata File [Section titled “via SAML Metadata File”](#via-saml-metadata-file) Under **Service Provider Details,** click on **Download Metadata XML** and upload in your Identity Portal ![Download Metadata XML](/.netlify/images?url=_astro%2F0.BfUk9wMU.png\&w=1350\&h=512\&dpl=6ae471fc143c9a4c27550463) #### via Manual Configuration [Section titled “via Manual Configuration”](#via-manual-configuration) Copy the **ACS URL (Assertion Consumer Service)** and **Service Provider Entity ID** from the Service Provider Details section and paste them in the appropriate sections in your Identity Provider Portal. 3. ### Configure Attribute mapping & assign users/groups [Section titled “Configure Attribute mapping & assign users/groups”](#configure-attribute-mapping--assign-usersgroups) #### Attribute mapping [Section titled “Attribute mapping”](#attribute-mapping) SAML Attributes need to be configured in your Identity Provider portal so that the user profile details are shared with us at the time of user login as part of SAML Response payload. User profile details that are needed for seamless user login are: * Email Address of the user * First Name of the user * Last Name of the user To configure these attributes, locate **Attribute Settings** section in the SAML Configuration page in your Identity Provider’s application, and carefully map the attributes with the Attribute names exactly as shown in the below image. ![Attribute Mapping section in SSO Configuration Portal](/.netlify/images?url=_astro%2F1.Dsi9Olvk.png\&w=2208\&h=742\&dpl=6ae471fc143c9a4c27550463) #### Assign user/group [Section titled “Assign user/group”](#assign-usergroup) To finish the Service Provider section of the SAML configuration, you need to “Assign” the users who need to access to this application. Find the User/Group assignment section in your Identity Provider application and select and assign all the required users or user groups that need access to this application via Single Sign-on. 4. ### Configure Identity Provider [Section titled “Configure Identity Provider”](#configure-identity-provider) After you have completed the Service Provider configuration, you now need to configure the Identity Provider details in our SSO Configuration page. Depending on your Identity Provider, you can choose either of the below methods: * Automated Configuration (configuration via Metadata URL) * Manual Configuration (configuration via individual fields) #### Automated Configuration (recommended) [Section titled “Automated Configuration (recommended)”](#automated-configuration-recommended) If you supply the Identity Provider Metadata URL, our system will automatically fetch the necessary configuration details required like Login URL, Identity Provider Entity ID, X.509 Certificate to complete the SAML SSO configuration. Also, we will periodically scan this url to keep the configuration up-to-date incase any of this information changes in your Identity Provider reducing the manual effort needed from your side. Locate and copy the Identity Provider Metadata URL from your Identity Provider’s application. Under **Identify Provider Configuration,** select **Configure using Metadata URL,** and paste it under **Metadata URL** on the **SSO Configuration Portal.** ![Paste Issuer URL on SSO Configuration Portal](/.netlify/images?url=_astro%2F2.BUU5fgqD.png\&w=2182\&h=704\&dpl=6ae471fc143c9a4c27550463) #### Manual Configuration [Section titled “Manual Configuration”](#manual-configuration) 1. Choose “Configure Manually” option in the “Identity Provider Configuration” section 2. Carefully copy the below configuration details from your Identity Provider section and paste them in the appropriate fields: * Issuer (also referred to as Identity Provider Entity ID) * Sign-on URL (also referred to as SSO URL or Single Sign-on URL) * Signing Certificate (also referred to as X.509 certificate) * You can also upload the certificate file instead of copying the contents manually. 5. ### Test Single Sign-on [Section titled “Test Single Sign-on”](#test-single-sign-on) To verify whether the SAML SSO configuration is completed correctly, click on **Test Connection** on the SSO Configuration Portal. If everything is done correctly, you will see a **Success** response as shown below. ![Test your SAML application for SSO configuration](/.netlify/images?url=_astro%2F3.7zjJqSeQ.png\&w=2198\&h=978\&dpl=6ae471fc143c9a4c27550463) If there’s a misconfiguration, our test will identify the errors and will offer you a way to correct the configuration right on the screen. 6. ### Enable Single Sign-on [Section titled “Enable Single Sign-on”](#enable-single-sign-on) After you successfully verified that the connection is configured correctly, you can enable the connection to let your users login to this application via Single Sign-on. Click on **Enable Connection.** ![Enable Single Sign-on](/.netlify/images?url=_astro%2F4.CY6-zQP7.png\&w=2194\&h=250\&dpl=6ae471fc143c9a4c27550463) With this, we are done configuring your application for an SSO login setup. --- # DOCUMENT BOUNDARY --- # Google Workspace - OIDC > Learn how to set up OpenID Connect (OIDC) Single Sign-On (SSO) using Google Workspace, with step-by-step instructions for app registration and client configuration. This guide walks you through configuring Google Workspace as your OIDC identity provider. You’ll create a Google OAuth app, configure an OAuth client, provide the required OIDC values in the SSO Configuration Portal, test the connection, and then enable Single Sign-On. 1. ## Create an OAuth App [Section titled “Create an OAuth App”](#create-an-oauth-app) Sign in to **Google Cloud Console** and open the project you want to use for this integration. Search for **Google Auth Platform** and open it from the results list. ![Search for Google Auth Platform in Google Cloud Console](/.netlify/images?url=_astro%2Fgoogle-auth-platform-search.B4lWW2xw.png\&w=2540\&h=1136\&dpl=6ae471fc143c9a4c27550463) Click **Get started** to begin the OAuth app setup. ![Google Auth Platform overview with Get started button](/.netlify/images?url=_astro%2Fgoogle-auth-platform-get-started.CEKJDkl0.png\&w=2538\&h=1296\&dpl=6ae471fc143c9a4c27550463) Enter the **App Information** and select the appropriate **User support email**. ![Google OAuth app configuration flow](/.netlify/images?url=_astro%2Fgoogle-oauth-app-information.7b4adoSB.png\&w=2078\&h=1186\&dpl=6ae471fc143c9a4c27550463) Select the **Audience** as **Internal** and click **Next**. ![Google OAuth consent screen with Internal audience selected](/.netlify/images?url=_astro%2Fgoogle-oauth-app-audience-internal.BjVmOj20.png\&w=3018\&h=1624\&dpl=6ae471fc143c9a4c27550463) Add the relevant email address in the **Contact Information** and click **Next**. ![Google OAuth consent screen contact information step](/.netlify/images?url=_astro%2Fgoogle-oauth-app-contact-information.DtjPGT8Z.png\&w=3024\&h=1626\&dpl=6ae471fc143c9a4c27550463) Agree to Google’s policy and click **Continue** and then **Create**. ![Google OAuth consent screen policy agreement and Create button](/.netlify/images?url=_astro%2Fgoogle-oauth-app-create-confirmation.D26d-1qq.png\&w=3024\&h=1626\&dpl=6ae471fc143c9a4c27550463) 2. ## Create OAuth Client [Section titled “Create OAuth Client”](#create-oauth-client) From the left-side menu, navigate to **Clients** and click **Create client**. ![Google Auth Platform Clients page with Create client button](/.netlify/images?url=_astro%2Fgoogle-clients-create-client.fQ7W8cPr.png\&w=1440\&h=628\&dpl=6ae471fc143c9a4c27550463) In Application type dropdown, select **Web Application** and add **Name** for the client. ![Create OAuth client form with Web application selected and client name entered](/.netlify/images?url=_astro%2Fgoogle-oauth-client-type-and-name.BaQ6Qd8-.png\&w=3018\&h=1624\&dpl=6ae471fc143c9a4c27550463) Copy the **Redirect URI** from **SSO Configuration Portal**. ![SSO Configuration Portal showing the Google OIDC Redirect URI](/.netlify/images?url=_astro%2Fgoogle-sso-portal-redirect-uri.Vf81H4Vt.png\&w=1974\&h=704\&dpl=6ae471fc143c9a4c27550463) On **Google console**, under the **Authorized redirect URIs**, click **Add URI**. Add the above copied URI to this field and click **Create**. ![Google OAuth client form with Authorized redirect URIs section](/.netlify/images?url=_astro%2Fgoogle-oauth-client-authorized-redirect-uri.D75nO_ha.png\&w=2486\&h=1564\&dpl=6ae471fc143c9a4c27550463) 3. ## Provide Client Credentials [Section titled “Provide Client Credentials”](#provide-client-credentials) After the client is created, copy the **Client ID** and **Client Secret** from Google Cloud. ![Google Cloud OAuth client details showing Client ID and Client Secret](/.netlify/images?url=_astro%2Fgoogle-client-id-and-secret.DuKsiNVb.png\&w=3024\&h=1484\&dpl=6ae471fc143c9a4c27550463) Add the above values under **Identity Provider Configuration** in the **SSO Configuration Portal**. For **Issuer URL**, use `https://accounts.google.com`. Once all values are entered, click **Update**. ![SSO Configuration Portal fields for Google Client ID and Client Secret](/.netlify/images?url=_astro%2Fgoogle-sso-portal-client-credentials.6tZHOP97.png\&w=2024\&h=810\&dpl=6ae471fc143c9a4c27550463) ![SSO Configuration Portal showing the Google Issuer URL after update](/.netlify/images?url=_astro%2Fgoogle-sso-portal-issuer-url.XtnoS61W.png\&w=1932\&h=902\&dpl=6ae471fc143c9a4c27550463) 4. ## Test Connection [Section titled “Test Connection”](#test-connection) In the **SSO Configuration Portal**, click **Test Connection**. If everything is configured correctly, you will see a **Success** response. Note If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. 5. ## Enable Single Sign-On [Section titled “Enable Single Sign-On”](#enable-single-sign-on) Once the test succeeds, click **Enable Connection** to allow users in your organization to sign in with Google Workspace OIDC. ![SSO Configuration Portal with Enable Connection button for Google Workspace OIDC](/.netlify/images?url=_astro%2Fgoogle-enable-connection.CC7rMBop.png\&w=1924\&h=242\&dpl=6ae471fc143c9a4c27550463) This completes the Google Workspace OIDC SSO setup for your application. --- # DOCUMENT BOUNDARY --- # Google Workspace - SAML > Learn how to configure Google Workspace as a SAML identity provider for secure single sign-on (SSO) with your application. This guide walks you through configuring Google Workspace as your SAML identity provider for the application you are onboarding, enabling secure single sign-on for your users. You’ll learn how to set up an enterprise application and configure SAML settings to the host application. By following these steps, your users will be able to seamlessly authenticate using their Google Workspace credentials. 1. ## Create a custom SAML app in Google Workspace [Section titled “Create a custom SAML app in Google Workspace”](#create-a-custom-saml-app-in-google-workspace) Google allows you to add custom SAML applications using the SAML protocol. This is the first step in establishing a secure SSO connection. **Prerequisites:** You need a super administrator account in Google Workspace to complete these steps. 1. Go to Google **Admin console** (`admin.google.com`) 2. Select **Apps** → **Web and mobile apps** 3. Click **Add app** → **Add custom SAML app** 4. Provide an app name (e.g., “YourApp”) and upload an app icon if needed 5. Click **Continue** ![Custom SAML app](/.netlify/images?url=_astro%2F0-google-saml.DQJWVST1.png\&w=1166\&h=648\&dpl=6ae471fc143c9a4c27550463) *Creating a new custom SAML application in Google Workspace* **Get Google identity provider details:** On the **Google identity provider details** page, you’ll need to collect setup information. You can either: * Download the **IDP metadata** file, or * Copy the **SSO URL** and **Entity ID** and download the **Certificate** Your SSO config portal connects with Google IdP using three essential pieces of information: * **SSO URL** * **Entity ID** * **Certificate** Copy these values from the Google console and paste them into your config portal. ![Google IdP Details](/.netlify/images?url=_astro%2F0.1-google-saml.BJCnAGkh.png\&w=2048\&h=1134\&dpl=6ae471fc143c9a4c27550463) *Essential SAML configuration details from Google Workspace* **Note:** Keep this page open as you’ll need to return to it after configuring the service provider details. 2. ## Configure the service provider in Google Admin console [Section titled “Configure the service provider in Google Admin console”](#configure-the-service-provider-in-google-admin-console) In your SSO configuration portal: 1. Navigate to Single sign-on (SSO) → Google Workspace → SAML 2.0 2. Select the organization you want to configure 3. Copy these critical details from the SSO settings: * **ACS URL** (Assertion consumer service URL) * **SP Entity ID** (Service provider entity ID) * **SP Metadata URL** ![SSO Config Portal](/.netlify/images?url=_astro%2F1-google-saml.pDeCLwtz.png\&w=1954\&h=1196\&dpl=6ae471fc143c9a4c27550463) *Service provider configuration details in SSO portal* In Google Admin console: 1. Paste the copied details into their respective fields 2. Select **“Email”** as the **NameID format** (this serves as the primary user identifier during authentication) 3. Click **Continue** ![Google Workspace](/.netlify/images?url=_astro%2F1.1-google-saml.M_XJhpXJ.png\&w=3456\&h=1920\&dpl=6ae471fc143c9a4c27550463) *Configuring service provider details in Google Workspace* 3. ## Configure attribute mapping [Section titled “Configure attribute mapping”](#configure-attribute-mapping) User profile attributes in Google IdP need to be mapped to your application’s user attributes for seamless authentication. The essential attributes are: * Email address * First name * Last name To configure these attributes: 1. Locate the **Attribute mapping** section in your identity provider’s application 2. Map the Google attributes to your application attributes as shown below ![User profile attributes](/.netlify/images?url=_astro%2F2.1-google-saml.BvlwixSf.png\&w=2670\&h=1180\&dpl=6ae471fc143c9a4c27550463) *Mapping user attributes between Google Workspace and your application* 4. ## Assign users and groups [Section titled “Assign users and groups”](#assign-users-and-groups) Control access to your application by assigning specific groups: In the created app landing page click **view details** in the user access section. ![Navigate to View Details page](/.netlify/images?url=_astro%2F2.3-google-saml.DXkpwu-V.png\&w=1440\&h=742\&dpl=6ae471fc143c9a4c27550463) Here, you can either enable **ON for everyone** or assign a specific group to the application. To assign a group, search for the group name in the **Search for a group** field and select the correct group. ![Group Assignment](/.netlify/images?url=_astro%2F2.4-google-saml.DZue7W8H.png\&w=1509\&h=774\&dpl=6ae471fc143c9a4c27550463) *Assigning user groups for SSO access* 5. ## Configure identity provider in SSO portal [Section titled “Configure identity provider in SSO portal”](#configure-identity-provider-in-sso-portal) **Copy Google identity provider details:** From your Google Workspace, copy the IdP details shown during custom app creation: ![Google IdP details](/.netlify/images?url=_astro%2F3.1-google-saml.D6Lcu1eM.png\&w=3456\&h=1914\&dpl=6ae471fc143c9a4c27550463) *Identity provider details from Google Workspace* **Update the SSO configuration:** In your SSO configuration portal, navigate to the Identity provider configuration section. Paste the Google IdP details into the appropriate fields: Entity ID, SSO URL, and x509 certificates. ![Update IdP details in SSO config portal](/.netlify/images?url=_astro%2F3.2-google-saml.Dfh_X6X-.png\&w=2446\&h=1184\&dpl=6ae471fc143c9a4c27550463) *Updating identity provider configuration in SSO portal* Click **Update** to save the configuration. 6. ## Test the connection [Section titled “Test the connection”](#test-the-connection) Verify your SAML SSO configuration: 1. Click **Test connection** in the SSO configuration portal 2. If successful, you’ll see a confirmation message: ![Test Single Sign On](/.netlify/images?url=_astro%2F3.7zjJqSeQ.png\&w=2198\&h=978\&dpl=6ae471fc143c9a4c27550463) *Successful SSO connection test* If there are any configuration issues, the test will identify them and provide guidance for correction. 7. ## Enable SSO connection [Section titled “Enable SSO connection”](#enable-sso-connection) Once you’ve verified the configuration: 1. Click **Enable connection** to activate SSO for your users ![Enable SSO Connection](/.netlify/images?url=_astro%2F4.CY6-zQP7.png\&w=2194\&h=250\&dpl=6ae471fc143c9a4c27550463) *Enabling the SSO connection* 8. ## Test SSO functionality [Section titled “Test SSO functionality”](#test-sso-functionality) After enabling the connection, test both types of SSO flows to ensure everything works correctly: **Identity provider (IdP) initiated SSO:** 1. In Google Admin console, go to **Apps** → **Web and mobile apps** 2. Select your custom SAML app 3. Click **Test SAML login** at the top left 4. Your app should open in a separate tab with successful authentication **Service provider (SP) initiated SSO:** 1. Open the SSO URL for your SAML app 2. You should be automatically redirected to the Google sign-in page 3. Enter your Google Workspace credentials 4. After successful authentication, you’ll be redirected back to your application **Troubleshooting:** If either test fails, check the SAML app error messages and verify your IdP and SP settings match exactly. Congratulations! You have successfully configured Google SAML for your application. Your users can now securely authenticate using their Google Workspace credentials through single sign-on. Google Workspace SSO resources For more detailed information about setting up custom SAML apps in Google Workspace, refer to the [official Google Workspace documentation](https://support.google.com/a/answer/6087519). --- # DOCUMENT BOUNDARY --- # JumpCloud - OIDC > Learn how to set up OpenID Connect (OIDC) Single Sign-On (SSO) using JumpCloud, with step-by-step instructions for OIDC application setup. This guide walks you through configuring JumpCloud as your OIDC identity provider. You’ll create a custom OIDC application, add the redirect URI, provide the required OIDC values in the SSO Configuration Portal, assign access, test the connection, and then enable Single Sign-On. 1. ## Create an OIDC Application [Section titled “Create an OIDC Application”](#create-an-oidc-application) Sign in to your **JumpCloud Admin Portal**. Go to **Access -> SSO Applications** and click **Add New Application**. ![JumpCloud SSO Applications page with Add New Application](/.netlify/images?url=_astro%2Fjumpcloud-sso-applications-add-new-application.D064TbAO.png\&w=1866\&h=1424\&dpl=6ae471fc143c9a4c27550463) In the application catalog, search for **OIDC** and select **Custom OIDC App**. ![Search for Custom OIDC App in JumpCloud](/.netlify/images?url=_astro%2Fjumpcloud-search-custom-oidc-app.CVZ0DJ3u.png\&w=2898\&h=1554\&dpl=6ae471fc143c9a4c27550463) Continue through the setup, confirm the OIDC app selection by clicking **Next**. ![Select Custom OIDC App in JumpCloud](/.netlify/images?url=_astro%2Fjumpcloud-select-custom-oidc-app.D1wQYPWR.png\&w=2892\&h=1570\&dpl=6ae471fc143c9a4c27550463) Enter a recognizable Application name in **Display Label** field, and optionally upload an icon and click **Next**. ![Enter general information for the JumpCloud OIDC application](/.netlify/images?url=_astro%2Fjumpcloud-oidc-app-general-information.CzI3UOAT.png\&w=2890\&h=1568\&dpl=6ae471fc143c9a4c27550463) Click **Configure Application**. ![JumpCloud Custom OIDC App review step with Configure Application button](/.netlify/images?url=_astro%2Fjumpcloud-configure-application-review.j2ytU8G1.png\&w=1472\&h=795\&dpl=6ae471fc143c9a4c27550463) 2. ## Add Redirect URI [Section titled “Add Redirect URI”](#add-redirect-uri) From the **SSO Configuration Portal**, copy the **Redirect URI** under **Service Provider Details**. ![SSO Configuration Portal showing the JumpCloud OIDC Redirect URI](/.netlify/images?url=_astro%2Fjumpcloud-sso-portal-redirect-uri.BC1hqB7e.png\&w=1872\&h=400\&dpl=6ae471fc143c9a4c27550463) In JumpCloud, open the recently created OIDC application and navigate to **SSO** -> **Configuration Settings**. Paste the copied URI into the **Redirect URI** field. Add the login url of your application in **Login URL** field. ![JumpCloud SSO configuration settings with Redirect URI and Login URL fields](/.netlify/images?url=_astro%2Fjumpcloud-configuration-settings-redirect-and-login-url.DA-X3kvG.png\&w=2928\&h=1578\&dpl=6ae471fc143c9a4c27550463) 3. ## Configure Attributes [Section titled “Configure Attributes”](#configure-attributes) Scroll down to **Attribute Mapping** section, select **Email** and **Profile** as **Standard Scopes** and then click **Activate**. ![JumpCloud attribute mapping with Email and Profile standard scopes selected](/.netlify/images?url=_astro%2Fjumpcloud-attribute-mapping-standard-scopes.C9eWhtJa.png\&w=2934\&h=1578\&dpl=6ae471fc143c9a4c27550463) 4. ## Provide OIDC Configuration [Section titled “Provide OIDC Configuration”](#provide-oidc-configuration) From JumpCloud, copy the **Client ID** and **Client Secret**. For **Issuer URL**, use `https://oauth.id.jumpcloud.com`. ![JumpCloud application activated dialog showing Client ID and Client Secret](/.netlify/images?url=_astro%2Fjumpcloud-client-id-and-secret-modal.CsreCVaX.png\&w=2010\&h=1344\&dpl=6ae471fc143c9a4c27550463) Add these values under **Identity Provider Configuration** in the **SSO Configuration Portal**, then click **Update**. ![SSO Configuration Portal fields for JumpCloud Client ID and Client Secret](/.netlify/images?url=_astro%2Fjumpcloud-sso-portal-client-credentials.DN-dYD8_.png\&w=1866\&h=822\&dpl=6ae471fc143c9a4c27550463) ![SSO Configuration Portal showing the JumpCloud Issuer URL after update](/.netlify/images?url=_astro%2Fjumpcloud-sso-portal-issuer-url.j-BKQtbS.png\&w=1858\&h=874\&dpl=6ae471fc143c9a4c27550463) 5. ## Assign Users/Groups [Section titled “Assign Users/Groups”](#assign-usersgroups) On JumpCloud, navigate to **User Groups** tab. Assign the appropriate user groups to the new OIDC application and click **Save**. ![JumpCloud User Groups tab with assigned groups selected for the OIDC app](/.netlify/images?url=_astro%2Fjumpcloud-user-groups-assignment.H7-SwcfY.png\&w=2932\&h=1578\&dpl=6ae471fc143c9a4c27550463) 6. ## Test Connection [Section titled “Test Connection”](#test-connection) In the **SSO Configuration Portal**, click **Test Connection** to verify your configuration. Note If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. 7. ## Enable Single Sign-On [Section titled “Enable Single Sign-On”](#enable-single-sign-on) Once the test succeeds, click **Enable Connection** to allow assigned users to sign in with JumpCloud OIDC. ![SSO Configuration Portal with Enable Connection button for JumpCloud OIDC](/.netlify/images?url=_astro%2Fjumpcloud-enable-connection.3CaNJojb.png\&w=1874\&h=232\&dpl=6ae471fc143c9a4c27550463) This completes the JumpCloud OIDC SSO setup for your application. --- # DOCUMENT BOUNDARY --- # JumpCloud SAML > Learn how to configure JumpCloud as a SAML identity provider for secure single sign-on (SSO) with your application. This guide walks you through configuring JumpCloud as your SAML identity provider for the application you are onboarding, enabling secure single sign-on for your users. You’ll learn how to set up an enterprise application, configure SAML settings to the host application. By following these steps, your users will be able to seamlessly authenticate using their JumpCloud credentials. ## Download metadata XML [Section titled “Download metadata XML”](#download-metadata-xml) Sign into the SSO Configuration Portal, select **JumpCloud,** then **SAML,** and click on **Configure** Under **Service Provider Details,** click on **Download Metadata XML** ![Download Metadata XML](/.netlify/images?url=_astro%2F0.BVk_5ROJ.png\&w=2256\&h=1088\&dpl=6ae471fc143c9a4c27550463) ## Create enterprise application [Section titled “Create enterprise application”](#create-enterprise-application) 1. Login to your JumpCloud Portal and go to **SSO Applications** ![Locate SSO Applications](/.netlify/images?url=_astro%2F1.pssd_fxM.png\&w=1558\&h=1028\&dpl=6ae471fc143c9a4c27550463) 2. Click on **Add New Application** ![Click on Add New Application](/.netlify/images?url=_astro%2F2.CYy46Vv7.png\&w=2120\&h=896\&dpl=6ae471fc143c9a4c27550463) 3. In the **Create New Application Integration** search box: * Type **Custom SAML App** * Select it from the drop down list * Give your app a name * Select your icon (optional) * Click on **Save** ![Create and save a new application integration](/images/docs/guides/sso-integrations/jumpcloud-saml/2-5.gif) 4. Click on **Configure Application** ![Click on Configure application](/.netlify/images?url=_astro%2F3.DZ5jgu9s.png\&w=2662\&h=1586\&dpl=6ae471fc143c9a4c27550463) ## SAML configuration [Section titled “SAML configuration”](#saml-configuration) 1. Go to the **SSO** tab and upload the downloaded Metadata XML under **Service Provider Metadata→ Upload Metadata** ![Upload Metadata XML under Service Provider Metadata](/.netlify/images?url=_astro%2F4.BBN04DIU.png\&w=1732\&h=1328\&dpl=6ae471fc143c9a4c27550463) 2. Copy the **SP Entity ID** from your SSO Configuration Portal and paste it in both the **IdP Entity ID** and **SP Entity ID** fields on JumpCloud Portal ![Copy SP Entity ID from your SSO Configuration Portal](/.netlify/images?url=_astro%2F5.D2igNtsX.png\&w=2200\&h=1066\&dpl=6ae471fc143c9a4c27550463) ![Paste it under IdP Entity ID and SP Entity ID on JumpCloud Portal](/.netlify/images?url=_astro%2F6.D7RAXpC_.png\&w=1700\&h=1034\&dpl=6ae471fc143c9a4c27550463) 3. Configure ACS URL: * Copy the **ACS URL** from your SSO Configuration Portal * Go to the **ACS URLs** section in JumpCloud Portal * Paste it in the **Default URL** field ![Copy ACS URL from SSO Configuration Portal](/.netlify/images?url=_astro%2F7.BqNw4jEm.png\&w=2172\&h=830\&dpl=6ae471fc143c9a4c27550463) ![Paste it under Default URL on JumpCloud Portal](/.netlify/images?url=_astro%2F8.BgrcZViX.png\&w=1736\&h=1014\&dpl=6ae471fc143c9a4c27550463) ## Attribute mapping [Section titled “Attribute mapping”](#attribute-mapping) 1. In the SSO tab, scroll to find **Attributes** ![Locate Attributes section on JumpCloud Portal](/.netlify/images?url=_astro%2F9.BjP0bSRq.png\&w=1178\&h=1174\&dpl=6ae471fc143c9a4c27550463) 2. Map the attributes: * Check the **Attribute Mapping** section in the SSO Configuration Portal * Map the same attributes on your JumpCloud application ![Attribute mapping from SSO Configuration Portal](/.netlify/images?url=_astro%2F10.8sURzFNn.png\&w=1838\&h=660\&dpl=6ae471fc143c9a4c27550463) ![Attribute Mapping on JumpCloud Portal](/images/docs/guides/sso-integrations/jumpcloud-saml/10-5.gif) ## Assign users [Section titled “Assign users”](#assign-users) Go to the **User Groups** tab. Select appropriate users/groups you want to assign to this application, and click on **Save** once done. ![Assign individuals or groups to your application](/.netlify/images?url=_astro%2F11.DKyxJDLj.png\&w=1790\&h=1342\&dpl=6ae471fc143c9a4c27550463) ## Upload IdP metadata URL [Section titled “Upload IdP metadata URL”](#upload-idp-metadata-url) 1. On your JumpCloud Portal, click on **SSO** and copy the **Copy Metadata URL** ![Copy Metadata URL from your JumpCloud portal](/.netlify/images?url=_astro%2F12.CTGSTojo.png\&w=1704\&h=884\&dpl=6ae471fc143c9a4c27550463) 2. Configure the metadata URL: * Under **Identify Provider Configuration**, select **Configure using Metadata URL** * Paste it under **App Federation Metadata URL** on the SSO Configuration Portal ![Paste Metadata URL on SSO Configuration Portal](/.netlify/images?url=_astro%2F13.D6QZDVaF.png\&w=2184\&h=718\&dpl=6ae471fc143c9a4c27550463) ## Test connection [Section titled “Test connection”](#test-connection) Click on **Test Connection**. If everything is done correctly, you will see a **Success** response as shown below. If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. ![Test SSO configuration](/.netlify/images?url=_astro%2F3.7zjJqSeQ.png\&w=2198\&h=978\&dpl=6ae471fc143c9a4c27550463) ## Enable connection [Section titled “Enable connection”](#enable-connection) Click on **Enable Connection**. This will let all your selected users login to the new application via your JumpCloud SSO. ![Enable SSO on JumpCloud](/.netlify/images?url=_astro%2F4.CY6-zQP7.png\&w=2194\&h=250\&dpl=6ae471fc143c9a4c27550463) Note You can access the SSO Configuration Portal at [](https://your-subdomain.scalekit.dev) (Development) or [](https://your-subdomain.scalekit.com) (Production) --- # DOCUMENT BOUNDARY --- # Microsoft AD FS - SAML > Learn how to configure Microsoft AD FS as a SAML identity provider for secure single sign-on (SSO) with your application. This guide walks you through configuring Single Sign-On (SSO) with Microsoft Active Directory Federation Services (AD FS) as your Identity Provider. #### Before you begin [Section titled “Before you begin”](#before-you-begin) To successfully set up AD FS SAML integration, you’ll need: * Elevated access to your AD FS Management Console * Access to the Admin Portal of the application you’re integrating Microsoft AD FS with Tip Having these prerequisites ready before starting will make the configuration process smoother ## Configuration steps [Section titled “Configuration steps”](#configuration-steps) 1. #### Begin the configuration [Section titled “Begin the configuration”](#begin-the-configuration) Choose Microsoft AD FS as your identity provider ![Setup SSO Connection dialog with ADFS selected as the identity provider](/.netlify/images?url=_astro%2F-1-1.DoY3Yfhj.png\&w=2558\&h=1172\&dpl=6ae471fc143c9a4c27550463) Download Metadata XML file so that you can configure AD FS Server going forward ![Service Provider Details panel with the Download Metadata XML link highlighted](/.netlify/images?url=_astro%2F-1.BkbK6BJ4.png\&w=2260\&h=876\&dpl=6ae471fc143c9a4c27550463) 2. #### Open AD FS Management Console [Section titled “Open AD FS Management Console”](#open-ad-fs-management-console) * Launch Server Manager * Click ‘Tools’ in the top menu * Select ‘AD FS Management’ 3. #### Create a Relying Party Trust [Section titled “Create a Relying Party Trust”](#create-a-relying-party-trust) * In the left navigation pane, expand ‘Trust Relationships’ * Right-click ‘Relying Party Trusts’ * Select ‘Add Relying Party Trust’ * Click ‘Start’ to begin the configuration ![AD FS Management Console with the Add Relying Party Trust option selected](/.netlify/images?url=_astro%2F0-1.C1eDu6B8.png\&w=1262\&h=929\&dpl=6ae471fc143c9a4c27550463) 4. #### Configure Trust Settings [Section titled “Configure Trust Settings”](#configure-trust-settings) * Select ‘Claims aware’ as the trust type * Choose ‘Import data about the relying party from a file’ * Click ‘Next’ to proceed ![Select Data Source step in the Add Relying Party Trust Wizard with the metadata file import option selected](/.netlify/images?url=_astro%2F2.BzOVYbyq.png\&w=768\&h=634\&dpl=6ae471fc143c9a4c27550463) Import the Metadata XML file that you downloaded earlier Note You can configure the relying party trust using either of these methods: * Enter the Metadata URL directly (if network access allows) 5. #### Set Display Name [Section titled “Set Display Name”](#set-display-name) * Enter a descriptive name for your application (e.g., “ExampleApp”) * Click ‘Next’ to continue ![Set display name step in the AD FS relying party trust wizard](/.netlify/images?url=_astro%2F16.qv9-rovY.png\&w=1492\&h=1224\&dpl=6ae471fc143c9a4c27550463) 6. #### Configure Access Control [Section titled “Configure Access Control”](#configure-access-control) * Select an appropriate access control policy * For purposes of this guide, select ‘Permit everyone’ * Click ‘Next’ to proceed 7. #### Review Trust Configuration [Section titled “Review Trust Configuration”](#review-trust-configuration) * Verify the following settings: * Monitoring configuration * Endpoints * Encryption settings * Click ‘Next’ to continue ![Review trust configuration screen in the AD FS wizard](/.netlify/images?url=_astro%2F17.Cz41xxGF.png\&w=1514\&h=1230\&dpl=6ae471fc143c9a4c27550463) The wizard will complete with the ‘Configure claims issuance policy for this application’ option automatically selected ![Finish step of the Add Relying Party Trust Wizard with the claims issuance policy checkbox selected](/.netlify/images?url=_astro%2F6.4omJa0ZL.png\&w=768\&h=634\&dpl=6ae471fc143c9a4c27550463) 8. #### Create claim rule [Section titled “Create claim rule”](#create-claim-rule) Navigate to ‘Relying Party Trusts’ and select recently created app. Then click on ‘Edit Claim Issuance Policy’ from right nav bar. ![Edit claim issuance policy option for the new relying party trust in AD FS](/.netlify/images?url=_astro%2F15.DKZVXYtm.png\&w=3014\&h=1622\&dpl=6ae471fc143c9a4c27550463) Click ‘Add Rule’ to create a new claim rule ![Edit Claim Issuance Policy dialog with the Add Rule button highlighted](/.netlify/images?url=_astro%2F7.CVY_QN4e.png\&w=538\&h=595\&dpl=6ae471fc143c9a4c27550463) Select ‘Send LDAP Attributes as Claims’ template ![Select Rule Template step with Send LDAP Attributes as Claims chosen](/.netlify/images?url=_astro%2F8.CTl2bgd7.png\&w=768\&h=634\&dpl=6ae471fc143c9a4c27550463) 9. #### Map User Attributes [Section titled “Map User Attributes”](#map-user-attributes) * Enter a descriptive rule name (e.g., “Example App”) * Configure the following attribute mappings: * `E-Mail-Addresses` → E-Mail Address * `Given-Name` → Given Name * `Surname` → Surname * `User-Principal-Name` → Name ID * Click ‘Finish’ to complete the mapping ![Edit Rule dialog mapping LDAP attributes to outgoing claim types for email, given name, surname, and name ID](/.netlify/images?url=_astro%2F9.BslyN39j.png\&w=601\&h=642\&dpl=6ae471fc143c9a4c27550463) 10. #### Complete Admin Portal Configuration [Section titled “Complete Admin Portal Configuration”](#complete-admin-portal-configuration) * Navigate to Identity Provider Configuration in the Admin Portal * Select “Configure Manually” * The above endpoints are AD FS endpoints. You can find them listed in AD FS Console > Service > Endpoints > Tokens and Metadata sections. Enter these required details: * Microsoft AD FS Identifier: `http:///adfs/services/trust` * Login URL: `http:///adfs/ls` * Certificate: 1. Access [Federation Metadata URL](https:///FederationMetadata/2007-06/FederationMetadata.xml) 2. Locate the text after the first `X509Certificate` tag 3. Copy and paste this certificate into the “Certificate” field * Click “Update” to save the configuration ![Identity Provider Configuration form with manual entry fields for the IdP entity ID, SAML URL, and X.509 certificate](/.netlify/images?url=_astro%2F12-1.CY8o-PyP.png\&w=2320\&h=1250\&dpl=6ae471fc143c9a4c27550463) 11. #### Test the Integration [Section titled “Test the Integration”](#test-the-integration) * In the Admin Portal, click “Test Connection” * You will be redirected to the AD FS login page * Enter your AD FS credentials * Verify successful redirection back to the Admin Portal with the correct user attributes ![Test single sign-on step showing a successful connection and the received user attributes](/.netlify/images?url=_astro%2F13.v5uvsTqZ.png\&w=2198\&h=978\&dpl=6ae471fc143c9a4c27550463) 12. #### Enable Connection [Section titled “Enable Connection”](#enable-connection) * Click on **Enable Connection** * This will let all your selected users login to the new application via your AD FS SSO ![Enable single sign-on step with the Enable Connection button](/.netlify/images?url=_astro%2F14.BDS_w7Cj.png\&w=2194\&h=250\&dpl=6ae471fc143c9a4c27550463) --- # DOCUMENT BOUNDARY --- # Microsoft Entra ID - OIDC > Learn how to set up OpenID Connect (OIDC) Single Sign-On (SSO) using Microsoft Entra ID, with step-by-step instructions for app registration and OIDC configuration. This guide walks you through configuring Microsoft Entra ID as your OIDC identity provider. You’ll create an app registration, provide OIDC values in the SSO Configuration Portal, map required claims, assign access, test the connection, and enable Single Sign-On. 1. ## Create an Application [Section titled “Create an Application”](#create-an-application) Sign in to **Microsoft Entra ID** in the [Microsoft Azure Portal](https://portal.azure.com/). Go to **App registrations** and click **New registration** to create a new app. ![Microsoft Entra ID App registrations page with New registration button](/.netlify/images?url=_astro%2F0.6jwMmKa9.png\&w=1146\&h=814\&dpl=6ae471fc143c9a4c27550463) Set the **Application name**. Set **Supported Account Types** to **Single tenant only**. ![Application registration form showing app name and single-tenant account type](/.netlify/images?url=_astro%2F2026-03-10-17-47-18.Cr9rmEkc.png\&w=2250\&h=1532\&dpl=6ae471fc143c9a4c27550463) From the SSO Configuration Portal, copy the **Redirect URI** from **Service Provider Details**: ![SSO Configuration Portal showing the Redirect URI in Service Provider Details](/.netlify/images?url=_astro%2F2026-03-10-17-41-08.DsAtY7Ji.png\&w=1882\&h=704\&dpl=6ae471fc143c9a4c27550463) In Entra ID, under **Redirect URI** section, select **Web** and paste the copied redirect URI, then click **Register**. ![Microsoft Entra registration screen with Web Redirect URI configured](/.netlify/images?url=_astro%2F2026-03-10-17-45-37.BFd4OptT.png\&w=2252\&h=1548\&dpl=6ae471fc143c9a4c27550463) 2. ## Generate client credentials [Section titled “Generate client credentials”](#generate-client-credentials) From the application’s **Overview** page in Entra ID, copy **Application (client) ID**. ![Application Overview page highlighting the Application client ID](/.netlify/images?url=_astro%2F2026-03-10-17-50-29.CtJgVX88.png\&w=2520\&h=730\&dpl=6ae471fc143c9a4c27550463) Go to **Certificates & secrets**, click **New client secret**, and create a client secret and copy it. ![Certificates and secrets page with New client secret action](/.netlify/images?url=_astro%2F2026-03-10-17-54-11.dM-K7Les.png\&w=3006\&h=1620\&dpl=6ae471fc143c9a4c27550463) ![New client secret created with value ready to copy](/.netlify/images?url=_astro%2F2026-03-10-17-54-32.DDKs4cdv.png\&w=2738\&h=1262\&dpl=6ae471fc143c9a4c27550463) Add the **Client ID** and **Client Secret** in the SSO Configuration Portal. ![SSO Configuration Portal fields for Client ID and Client Secret](/.netlify/images?url=_astro%2F2026-03-10-17-56-30.o5l5_2Mt.png\&w=1860\&h=808\&dpl=6ae471fc143c9a4c27550463) 3. ## Provide Issuer URL [Section titled “Provide Issuer URL”](#provide-issuer-url) In Entra ID, navigate to application’s **Overview** page -> **Endpoints**. Copy the **OpenID Connect metadata document** URL: ![Application Endpoints dialog showing OpenID Connect metadata document URL](/.netlify/images?url=_astro%2F2026-03-10-18-01-17.BqmuCQIA.png\&w=3018\&h=1614\&dpl=6ae471fc143c9a4c27550463) Paste the copied URL into the **Issuer URL** field in the SSO Configuration Portal and click **Update**. ![SSO Configuration Portal Issuer URL field populated with metadata URL](/.netlify/images?url=_astro%2F2026-03-10-18-02-21.D7nHGriI.png\&w=1862\&h=814\&dpl=6ae471fc143c9a4c27550463) 4. ## Attribute Mapping [Section titled “Attribute Mapping”](#attribute-mapping) Go to **Token configuration** and click **Add optional claim**. Select token type **ID**, then add these claims: `email`, `family_name`, and `given_name`. ![Add optional claim dialog with ID token claims email family_name and given_name selected](/.netlify/images?url=_astro%2F2026-03-10-18-08-25.DOcWy_K_.png\&w=3004\&h=1612\&dpl=6ae471fc143c9a4c27550463) 5. ## Assign Users and Groups [Section titled “Assign Users and Groups”](#assign-users-and-groups) In Entra ID, navigate to **Enterprise applications** and select the recently created **OIDC app**. ![Enterprise applications list with the newly created OIDC app selected](/.netlify/images?url=_astro%2F2026-03-10-18-15-54.UCT6izT4.png\&w=3016\&h=1562\&dpl=6ae471fc143c9a4c27550463) Then navigate to **Users and groups** and click **Add user/group**. ![Users and groups page with Add user or group action](/.netlify/images?url=_astro%2F2026-03-10-18-15-23.D-8hAdOg.png\&w=3022\&h=1516\&dpl=6ae471fc143c9a4c27550463) Assign the required users or groups, and save the assignment. ![Assigned users and groups list for the Entra OIDC enterprise application](/.netlify/images?url=_astro%2F2026-03-10-18-24-04.Df9IrI3A.png\&w=2994\&h=1610\&dpl=6ae471fc143c9a4c27550463) 6. ## Test your SSO connection [Section titled “Test your SSO connection”](#test-your-sso-connection) In the SSO Configuration Portal, click **Test Connection** to verify your configuration. Note If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. 7. ## Enable Single Sign-On [Section titled “Enable Single Sign-On”](#enable-single-sign-on) Once the test succeeds, click **Enable Connection**. ![SSO Configuration Portal with Enable Connection action after successful test](/.netlify/images?url=_astro%2F2026-03-10-18-17-20.CyYGHzIh.png\&w=1846\&h=220\&dpl=6ae471fc143c9a4c27550463) This completes the Microsoft Entra ID OIDC SSO setup for your application. --- # DOCUMENT BOUNDARY --- # Okta - OIDC > Learn how to set up OpenID Connect (OIDC) Single Sign-On (SSO) using Okta as your identity provider, with step-by-step instructions for app integration setup. This guide walks you through configuring Okta as your OIDC identity provider for your application. You’ll create an OIDC app integration in Okta, connect it to the SSO Configuration Portal, assign access, test the connection, and then enable Single Sign-On. 1. ## Create an OIDC Integration [Section titled “Create an OIDC Integration”](#create-an-oidc-integration) Log in to your *Okta Admin Console*. Go to *Applications -> Applications*. ![Open the Applications page in Okta Admin Console](/.netlify/images?url=_astro%2F0.Bi9fvSGC.png\&w=1542\&h=892\&dpl=6ae471fc143c9a4c27550463) In the **Applications** tab, click on **Create App Integration.** ![Create a new app integration in Okta](/.netlify/images?url=_astro%2F1.DLiFybsd.png\&w=1406\&h=922\&dpl=6ae471fc143c9a4c27550463) Select **OIDC - OpenID Connect** as the sign-in method and **Web Application** as the application type, then click **Next**. ![Select OIDC web application in Okta](/.netlify/images?url=_astro%2F2.BLyYVEyn.png\&w=2540\&h=1452\&dpl=6ae471fc143c9a4c27550463) 2. ## Configure OIDC Integration [Section titled “Configure OIDC Integration”](#configure-oidc-integration) In the app configuration form, enter an app name. ![Set app name in Okta](/.netlify/images?url=_astro%2F2026-03-10-14-18-44.Bl1MXM6R.png\&w=2940\&h=1590\&dpl=6ae471fc143c9a4c27550463) From the **SSO Configuration Portal**, copy the **Redirect URI** under **Service Provider Details**. ![Copy Redirect URI from the SSO Configuration Portal](/.netlify/images?url=_astro%2F2026-03-10-14-23-04.BYythTpw.png\&w=1928\&h=698\&dpl=6ae471fc143c9a4c27550463) Back in Okta, paste this value into **Sign-in redirect URIs**. ![Add Redirect URL to Okta](/.netlify/images?url=_astro%2F2026-03-10-14-25-01.DrV0Z8UV.png\&w=2934\&h=1588\&dpl=6ae471fc143c9a4c27550463) Scroll down to the Assignments section. Select **Limit access to selected groups** and assign the appropriate groups to the application. The group assignment can be edited later. ![Assign required groups to the application in Okta](/.netlify/images?url=_astro%2F2026-03-10-14-20-32.QdVh4t1z.png\&w=2936\&h=1590\&dpl=6ae471fc143c9a4c27550463) 3. ## Provide OIDC Configuration [Section titled “Provide OIDC Configuration”](#provide-oidc-configuration) After the app integration is created, copy **Client ID** and **Client Secret** from the **General** tab in Okta: ![Copy client credentials from Okta](/.netlify/images?url=_astro%2F2026-03-10-14-45-43.Bwal_0X0.png\&w=2928\&h=1578\&dpl=6ae471fc143c9a4c27550463) Add these values under **Identity Provider Configuration** in the **SSO Configuration Portal**: ![Add client credentials in SSO configuration portal](/.netlify/images?url=_astro%2F2026-03-10-14-47-17.lqTCJxtz.png\&w=1870\&h=806\&dpl=6ae471fc143c9a4c27550463) Click the profile section in the top navigation bar in Okta and copy the **Okta Tenant Domain**. We will use this value to construct the Issuer URL. ![Copy Okta tenant domain from profile menu](/.netlify/images?url=_astro%2F2026-03-10-15-42-33.C98eiey-.png\&w=2922\&h=1586\&dpl=6ae471fc143c9a4c27550463) Construct the **Issuer URL** using the following format: `https://[okta-tenant-domain]` Add this Issuer URL in the **SSO Configuration Portal**: ![Add Issuer URL in SSO configuration portal](/.netlify/images?url=_astro%2F2026-03-10-14-51-07.Cws3R1mT.png\&w=1868\&h=816\&dpl=6ae471fc143c9a4c27550463) Once all values are entered, click **Update**. ![Completed IdP configuration in the SSO Configuration Portal](/.netlify/images?url=_astro%2F2026-03-10-14-51-52.BzD-eP5J.png\&w=1846\&h=880\&dpl=6ae471fc143c9a4c27550463) 4. ## Assign People/Groups [Section titled “Assign People/Groups”](#assign-peoplegroups) In Okta, go to the **Assignments** tab. ![Assign people or groups to the Okta app integration](/.netlify/images?url=_astro%2F4.DX07vo_Y.png\&w=1204\&h=478\&dpl=6ae471fc143c9a4c27550463) Click **Assign**, then choose **Assign to People** or **Assign to Groups**. Assign the appropriate people or groups to this integration and click **Done**. ![Assign users or groups to the Okta app](/.netlify/images?url=_astro%2F2026-03-10-14-59-18.DjklXxRN.png\&w=2932\&h=1580\&dpl=6ae471fc143c9a4c27550463) 5. ## Test Connection [Section titled “Test Connection”](#test-connection) In the **SSO Configuration Portal**, click **Test Connection**. If everything is configured correctly, you will see a **Success** response. Note If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. 6. ## Enable Single Sign-On [Section titled “Enable Single Sign-On”](#enable-single-sign-on) Click **Enable Connection** to allow assigned users to sign in through Okta OIDC. ![Enable connection](/.netlify/images?url=_astro%2F2026-03-10-15-22-15.BSEKDbIL.png\&w=1866\&h=234\&dpl=6ae471fc143c9a4c27550463) This completes the Okta OIDC SSO setup for your application. --- # DOCUMENT BOUNDARY --- # Okta SAML > Learn how to set up SAML-based Single Sign-On (SSO) using Okta as your Identity Provider, with step-by-step instructions for enterprise application configuration. This guide walks you through configuring Okta as your SAML identity provider for the application you are onboarding, enabling secure single sign-on for your users. You’ll learn how to set up an enterprise application, configure SAML settings to the host application. By following these steps, your users will be able to seamlessly authenticate using their Okta credentials. ## Create Enterprise Application [Section titled “Create Enterprise Application”](#create-enterprise-application) 1. Login to your *Okta Admin Console*. Go to *Applications→ Applications*. ![Okta Admin Console with the Applications menu item highlighted](/.netlify/images?url=_astro%2F0.BakodZRZ.png\&w=1542\&h=892\&dpl=6ae471fc143c9a4c27550463) 2. In the **Applications** tab, click on **Create App Integration.** ![Okta Applications page with the Create App Integration button highlighted](/.netlify/images?url=_astro%2F1.IsoAY_Ly.png\&w=1406\&h=922\&dpl=6ae471fc143c9a4c27550463) 3. Choose **SAML 2.0**, and click on **Next.** ![Create a new app integration dialog with SAML 2.0 selected as the sign-in method](/.netlify/images?url=_astro%2F2.DkynxeSj.png\&w=1840\&h=1108\&dpl=6ae471fc143c9a4c27550463) 4. Give your app a name, choose your app visibility settings, and click on **Next.** ![General Settings step of the Create SAML Integration wizard with app name and logo fields](/.netlify/images?url=_astro%2F3.BB3z9eaj.png\&w=1368\&h=1084\&dpl=6ae471fc143c9a4c27550463) ## SAML Configuration [Section titled “SAML Configuration”](#saml-configuration) 1. Copy the **SSO URL** from the **SSO Configuration Portal**. Paste this link in the space for **SSO URL** on the **Okta Admin Console**. ![SSO Configuration Portal showing the Single Sign-On URL to copy into Okta](/.netlify/images?url=_astro%2F4.CHr3Qapy.png\&w=2292\&h=1116\&dpl=6ae471fc143c9a4c27550463) ![Configure SAML step in Okta with the Single sign-on URL field filled in](/.netlify/images?url=_astro%2F5.8eM-fLKR.png\&w=1894\&h=1398\&dpl=6ae471fc143c9a4c27550463) 2. Copy the **Audience URI (SP Entity ID)** from the SSO Configuration Portal, and paste it in your **Okta Admin Console** in the space for **Audience URI.** ![SSO Configuration Portal showing the Audience URI (SP Entity ID) to copy into Okta](/.netlify/images?url=_astro%2F6.D0_xmfF5.png\&w=2292\&h=1116\&dpl=6ae471fc143c9a4c27550463) ![Configure SAML step in Okta with the Audience URI (SP Entity ID) field highlighted](/.netlify/images?url=_astro%2F7.Dss7F_Tw.png\&w=1898\&h=1400\&dpl=6ae471fc143c9a4c27550463) 3. You can leave the Default Relay State as blank. Similarly, select your preferences for the Name ID format, Application Username, and Update application username on fields. ![Okta SAML settings for Default RelayState, Name ID format, and application username options](/.netlify/images?url=_astro%2F8.Duf235Yu.png\&w=1496\&h=696\&dpl=6ae471fc143c9a4c27550463) ## Attribute Mapping [Section titled “Attribute Mapping”](#attribute-mapping) 1. The following user profile attributes should be mapped in Okta so that Scalekit receives user details during SSO login: * **Email Address** of the user (required) * **First Name** of the user (optional) * **Last Name** of the user (optional) Check the **Attribute Statements** section in the **SSO Configuration Portal** for the exact attribute names and values to use, then map the same attributes on your Okta Admin Console. You can either use the **Add expression** buttons or the legacy configuration method. ![Attribute mapping on SSO Configuration Portal](/.netlify/images?url=_astro%2F20.B4Gf6htn.png\&w=1454\&h=730\&dpl=6ae471fc143c9a4c27550463) 2. You will have to enter each attribute one by one as shown below. click on **Save** once you have added the name and value for the attribute, ![Attribute mapping on Okta Admin Console](/.netlify/images?url=_astro%2F21.D89CEsJG.png\&w=1400\&h=694\&dpl=6ae471fc143c9a4c27550463) 3. Ensure that you map all the required attributes as shown on the SSO Configuration Portal. ![Attribute mapping completed on Okta Admin Console](/.netlify/images?url=_astro%2F22.BLccS1xS.png\&w=1426\&h=1036\&dpl=6ae471fc143c9a4c27550463) ## Assign User/Group [Section titled “Assign User/Group”](#assign-usergroup) 1. Go to the **Assignments** tab. ![Locate Assignments tab](/.netlify/images?url=_astro%2F11.DMqg1BEa.png\&w=1682\&h=874\&dpl=6ae471fc143c9a4c27550463) 2. Click on **Assign** on the top navigation bar, select **Assign to People/Groups.** ![Select Assign to People or Groups](/.netlify/images?url=_astro%2F12.DP8pv860.png\&w=1204\&h=478\&dpl=6ae471fc143c9a4c27550463) 3. Click on **Assign** next to the people you want to assign it to. Click on **Save and Go Back**, and click on **Done.** ![Assign specific individuals or groups to app](/.netlify/images?url=_astro%2F13.BcgYv1Zp.png\&w=1218\&h=1070\&dpl=6ae471fc143c9a4c27550463) ## Finalize App [Section titled “Finalize App”](#finalize-app) 1. Preview your SAML Assertion generated, and click on **Next.** ![Preview SAML Assertion](/.netlify/images?url=_astro%2F14.zj3txre8.png\&w=1542\&h=706\&dpl=6ae471fc143c9a4c27550463) 2. Fill the feedback form, and click on **Finish** once done. ![Feedback form after configuring SAML](/.netlify/images?url=_astro%2F15.Clnftf3c.png\&w=1680\&h=1358\&dpl=6ae471fc143c9a4c27550463) ## Upload IdP Metadata URL [Section titled “Upload IdP Metadata URL”](#upload-idp-metadata-url) 1. On the **Sign On** tab copy the **Metadata URL** from the **Metadata Details** section on **Okta Admin Console.** ![Copy Metadata URL from Okta Admin Console](/.netlify/images?url=_astro%2F16.C7WuWMoS.png\&w=1198\&h=1332\&dpl=6ae471fc143c9a4c27550463) 2. Under **Identify Provider Configuration,** select **Configure using Metadata URL,** and paste it under **App Federation Metadata URL** on the **SSO Configuration Portal.** ![Paste Metadata URL on SSO Configuration Portal](/.netlify/images?url=_astro%2F17.CKSPRCwL.png\&w=2180\&h=672\&dpl=6ae471fc143c9a4c27550463) ## Test Connection [Section titled “Test Connection”](#test-connection) Click on **Test Connection.** If everything is done correctly, you will see a **Success** response as shown below. ![Test SSO configuration](/.netlify/images?url=_astro%2F3.7zjJqSeQ.png\&w=2198\&h=978\&dpl=6ae471fc143c9a4c27550463) Note If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. ## Enable Connection [Section titled “Enable Connection”](#enable-connection) Click on **Enable Connection.** This will let all your selected users login to the new application via your Okta SSO. ![Enable SSO on Okta Admin Console](/.netlify/images?url=_astro%2F4.CY6-zQP7.png\&w=2194\&h=250\&dpl=6ae471fc143c9a4c27550463) With this, you are done configuring your Okta application for an SSO login setup. --- # DOCUMENT BOUNDARY --- # OneLogin - OIDC > Learn how to set up OpenID Connect (OIDC) Single Sign-On (SSO) using OneLogin, with step-by-step instructions for OIDC application setup. This guide walks you through configuring OneLogin as your OIDC identity provider. You’ll create an OIDC application, add the redirect URI, provide the required OIDC values in the SSO Configuration Portal, assign access, test the connection, and then enable Single Sign-On. 1. ## Create an Application [Section titled “Create an Application”](#create-an-application) Sign in to the **OneLogin Admin Console**. Go to **Applications -> Applications**. ![Open the Applications menu in the OneLogin Admin Console](/.netlify/images?url=_astro%2Fonelogin-applications-menu.BLx5v6MZ.png\&w=2086\&h=1062\&dpl=6ae471fc143c9a4c27550463) Click **Add App**. ![Applications page in OneLogin with the Add App button highlighted](/.netlify/images?url=_astro%2Fonelogin-add-app-button.DF4PsE8f.png\&w=2586\&h=762\&dpl=6ae471fc143c9a4c27550463) In the **Find Application** search box, search for **OpenId Connect (OIDC)** and select it from the results list. ![OneLogin Find Application results with OpenId Connect (OIDC) selected](/.netlify/images?url=_astro%2Fonelogin-openid-connect-app-selection.DN55gp4s.png\&w=2662\&h=1010\&dpl=6ae471fc143c9a4c27550463) Add suitable application name in **Display Name** field and optionally upload an icon. Then click **Save**. ![OneLogin OIDC application form with Display Name and icon upload fields](/.netlify/images?url=_astro%2Fonelogin-openid-connect-app-details.BsvlZfMK.png\&w=2890\&h=1464\&dpl=6ae471fc143c9a4c27550463) 2. ## Add Redirect URI [Section titled “Add Redirect URI”](#add-redirect-uri) From the **SSO Configuration Portal**, copy the **Redirect URI** under **Service Provider Details**. ![SSO Configuration Portal showing the OneLogin OIDC Redirect URI](/.netlify/images?url=_astro%2Fonelogin-sso-portal-redirect-uri.C9DwhpXj.png\&w=1862\&h=406\&dpl=6ae471fc143c9a4c27550463) On OneLogin, navigate to **Configuration** tab. Paste the copied URI into **Redirect URIs** section and then click **Save**. ![OneLogin Configuration tab with Redirect URIs section populated for the OIDC app](/.netlify/images?url=_astro%2Fonelogin-redirect-uri-configuration.DPUukULa.png\&w=2886\&h=1422\&dpl=6ae471fc143c9a4c27550463) 3. ## Provide OIDC Configuration [Section titled “Provide OIDC Configuration”](#provide-oidc-configuration) On OneLogin, Navigate to **SSO** tab. Copy the **Client ID**, **Client Secret** and **Issuer URL**. ![OneLogin SSO tab showing Client ID, Client Secret, and Issuer URL](/.netlify/images?url=_astro%2Fonelogin-client-id-client-secret-and-issuer-url.VDJbD6xZ.png\&w=2378\&h=1352\&dpl=6ae471fc143c9a4c27550463) Add these values under **Identity Provider Configuration** in the **SSO Configuration Portal**, then click **Update**. ![SSO Configuration Portal fields for OneLogin Client ID and Client Secret](/.netlify/images?url=_astro%2Fonelogin-sso-portal-client-credentials.DFHD2Qd5.png\&w=1860\&h=808\&dpl=6ae471fc143c9a4c27550463) ![SSO Configuration Portal showing the OneLogin Issuer URL after update](/.netlify/images?url=_astro%2Fonelogin-sso-portal-issuer-url.BQCcLsQu.png\&w=1878\&h=900\&dpl=6ae471fc143c9a4c27550463) 4. ## Assign Users/Groups [Section titled “Assign Users/Groups”](#assign-usersgroups) On OneLogin, navigate to **Users** tab and click the user you want to assign to the application. ![OneLogin Users tab with a user selected for application assignment](/.netlify/images?url=_astro%2Fonelogin-users-tab-select-user.x2_E0UJk.png\&w=2638\&h=1146\&dpl=6ae471fc143c9a4c27550463) Once the user page opens, navigate to **Applications** tab from the left-side menu. Then click on **+** symbol. ![OneLogin user Applications tab with the add application action](/.netlify/images?url=_astro%2Fonelogin-user-applications-add-application.Bla1zTyK.png\&w=2906\&h=1230\&dpl=6ae471fc143c9a4c27550463) Select the recently created OIDC application from the **Select application** dropdown and click on **Continue**. ![OneLogin application assignment dialog with the new OIDC app selected](/.netlify/images?url=_astro%2Fonelogin-user-applications-select-application.CeVz0gcp.png\&w=1110\&h=608\&dpl=6ae471fc143c9a4c27550463) 5. ## Test Single Sign-On [Section titled “Test Single Sign-On”](#test-single-sign-on) In the **SSO Configuration Portal**, click **Test Connection** to verify your configuration. Note If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. 6. ## Enable Connection [Section titled “Enable Connection”](#enable-connection) Once the test succeeds, click **Enable Connection** to allow assigned users to sign in with OneLogin OIDC. ![SSO Configuration Portal with Enable Connection button for OneLogin OIDC](/.netlify/images?url=_astro%2Fonelogin-enable-connection.CAN6aReG.png\&w=1860\&h=230\&dpl=6ae471fc143c9a4c27550463) This completes the OneLogin OIDC SSO setup for your application. --- # DOCUMENT BOUNDARY --- # OneLogin SAML > A step-by-step guide to setting up Single Sign-On with OneLogin as the Identity Provider, including creating an enterprise application, configuring SAML, attribute mapping, assigning users, uploading IdP metadata, testing the connection, and enabling SSO. This guide walks you through configuring OneLogin as your SAML identity provider for the application you are onboarding, enabling secure single sign-on for your users. You’ll learn how to set up an enterprise application, configure SAML settings to the host application. By following these steps, your users will be able to seamlessly authenticate using their OneLogin credentials. 1. ## Creating enterprise application [Section titled “Creating enterprise application”](#creating-enterprise-application) Login to your **OneLogin Portal**. Go to **Applications→ Applications.** ![Locate Applications](/.netlify/images?url=_astro%2F0.BeFLTmK0.png\&w=2086\&h=1062\&dpl=6ae471fc143c9a4c27550463) Click on **Add App.** ![Click on Add App](/.netlify/images?url=_astro%2F1.DJgsfl-m.png\&w=2586\&h=762\&dpl=6ae471fc143c9a4c27550463) In the **Find Application** search box, type in **SAML Custom Connector (Advanced)**, and select it from the drop down list. ![Select SAML Custom Connector from drop down (GIF)](/images/docs/guides/sso-integrations/onelogin-saml/2-5.gif) Give your app a name that reflects the application you’ll be connecting it to, so users can easily recognize it in their OneLogin portal., select your icon (optional) and then click on **Save.** ![Click on Save](/.netlify/images?url=_astro%2F2.Dk4_F7R-.png\&w=2540\&h=1296\&dpl=6ae471fc143c9a4c27550463) 2. ## SAML configuration [Section titled “SAML configuration”](#saml-configuration) On the Application page click on **Configuration.** ![Locate Configuration](/.netlify/images?url=_astro%2F3.DdfvKgwb.png\&w=2308\&h=1276\&dpl=6ae471fc143c9a4c27550463) From your **SSO Configuration Portal**, copy the **ACS (Consumer) URL**. Go back to your **OneLogin Admin Portal**, and paste it in the **Recipient**, **ACS (Consumer) URL Validator**, and **ACS(Consumer) URL** fields. ![Copy ACS (Consumer) URL on SSO Configuration Portal](/.netlify/images?url=_astro%2F4.CfHUid6X.png\&w=2194\&h=1060\&dpl=6ae471fc143c9a4c27550463) **OneLogin Admin Portal** ![OneLogin SAML Custom Connector configuration with Recipient, ACS (Consumer) URL Validator, and ACS (Consumer) URL fields highlighted](/.netlify/images?url=_astro%2F2025-12-18-14-28-46.BK5ps4c-.png\&w=2938\&h=1368\&dpl=6ae471fc143c9a4c27550463) Similarly, copy the **Audience (Entity ID) f**rom your SSO Configuration Portal. Go back to your **OneLogin Admin Portal**, and paste it in the **Audience (EntityID).** ![Copy Audience (Entity ID) on SSO Configuration Portal](/.netlify/images?url=_astro%2F6.DAcgiWj7.png\&w=2198\&h=1068\&dpl=6ae471fc143c9a4c27550463) ![](/.netlify/images?url=_astro%2F7.H2z-QhcJ.png\&w=2890\&h=1276\&dpl=6ae471fc143c9a4c27550463) Click on **Save**. ![Locate Save](/.netlify/images?url=_astro%2F8.uJ6aAmAa.png\&w=2582\&h=922\&dpl=6ae471fc143c9a4c27550463) 3. ## Attribute mapping [Section titled “Attribute mapping”](#attribute-mapping) Go to the **Parameters** tab on **OneLogin Admin Portal**, and click on the plus (+) sign to add attributes. ![Locate Parameters tab](/.netlify/images?url=_astro%2F9.Dc4CJKli.png\&w=2617\&h=1044\&dpl=6ae471fc143c9a4c27550463) Check the **Attribute Mapping** section in the **SSO Configuration Portal**, and carefully map the **exact** **same attributes** on your **OneLogin Admin Portal**. ![Check attributes on SSO Configuration Portal](/.netlify/images?url=_astro%2F10.5K9f5GrO.png\&w=1838\&h=662\&dpl=6ae471fc143c9a4c27550463) ![Paste attributes on OneLogin Admin Portal](/images/docs/guides/sso-integrations/onelogin-saml/10-5.gif) 4. ## Assign user/group [Section titled “Assign user/group”](#assign-usergroup) Go to the **Users** tab. ![Locate Users under Users tab](/.netlify/images?url=_astro%2F11.QVruT9Bk.png\&w=1638\&h=806\&dpl=6ae471fc143c9a4c27550463) Click the user you want to assign to the application. ![Select user to assign](/.netlify/images?url=_astro%2F12.Bv9Xz3Es.png\&w=2558\&h=576\&dpl=6ae471fc143c9a4c27550463) Click on the **Applications** tab. Click on the **+** sign to assign the newly created application. ![Add application to previously selected user](/.netlify/images?url=_astro%2F13.DXLWQWhi.png\&w=2556\&h=766\&dpl=6ae471fc143c9a4c27550463) Select the newly created application from the drop down, and click on **Continue.** ![Select application from drop-down](/.netlify/images?url=_astro%2F14.DLRlndBF.png\&w=1244\&h=706\&dpl=6ae471fc143c9a4c27550463) Click on **Save**. ![Save user assignment to application](/.netlify/images?url=_astro%2F14.DLRlndBF.png\&w=1244\&h=706\&dpl=6ae471fc143c9a4c27550463) 5. ## Upload IdP metadata URL [Section titled “Upload IdP metadata URL”](#upload-idp-metadata-url) On **OneLogin Admin Portal**, click on SSO. Copy the **Issuer URL**. ![Copy Issuer URL on OneLogin Admin Portal](/.netlify/images?url=_astro%2F16.bNMHsUgi.png\&w=2062\&h=1336\&dpl=6ae471fc143c9a4c27550463) Under **Identify Provider Configuration,** select **Configure using Metadata URL,** and paste it under **App Federation Metadata URL** on the **SSO Configuration Portal.** ![Paste Issuer URL on SSO Configuration Portal](/.netlify/images?url=_astro%2F17.xkpppPlL.png\&w=2184\&h=716\&dpl=6ae471fc143c9a4c27550463) 6. ## Test connection [Section titled “Test connection”](#test-connection) Click on **Test Connection.** If everything is done correctly, you will see a **Success** response as shown below. If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. ![Test SSO Configuration](/.netlify/images?url=_astro%2F3.7zjJqSeQ.png\&w=2198\&h=978\&dpl=6ae471fc143c9a4c27550463) 7. ## Enable connection [Section titled “Enable connection”](#enable-connection) Click on **Enable Connection.** This will let all your selected users login to the new application via your **OneLogin Admin Portal** SSO. ![Enable SSO on Onelogin Admin Console](/.netlify/images?url=_astro%2F19.SQJdJ7n1.png\&w=2216\&h=268\&dpl=6ae471fc143c9a4c27550463) With this, we are done configuring your **OneLogin Admin Portal** application for an SSO login setup. --- # DOCUMENT BOUNDARY --- # Ping Identity - OIDC > Learn how to set up OpenID Connect (OIDC) Single Sign-On (SSO) using Ping Identity, with step-by-step instructions for OIDC application setup. This guide walks you through configuring Ping Identity as your OIDC identity provider. You’ll create an OIDC web application, add the redirect URL, provide the required OIDC values in the SSO Configuration Portal, configure user claims, test the connection, and then enable Single Sign-On. 1. ## Create an OIDC App [Section titled “Create an OIDC App”](#create-an-oidc-app) Log in to **Ping Identity Admin Console**. Navigate to **Applications -> Applications**, then click on **+** button to add a new application. ![Ping Identity Applications page with the add application button](/.netlify/images?url=_astro%2Fpingidentity-applications-page-add-application.dhJQhFBZ.png\&w=2916\&h=1394\&dpl=6ae471fc143c9a4c27550463) Once **Add Application** modal opens up, enter suitable **Application Name** and choose **OIDC Web App** as the Application Type. Then click on **Save**. ![Ping Identity Add Application dialog with Application Name entered and OIDC Web App selected](/.netlify/images?url=_astro%2Fpingidentity-create-oidc-web-app.3JUh6u-a.png\&w=2912\&h=1566\&dpl=6ae471fc143c9a4c27550463) 2. ## Configure Redirect URL [Section titled “Configure Redirect URL”](#configure-redirect-url) From the **SSO Configuration Portal**, copy the **Redirect URI** under **Service Provider Details**. ![SSO Configuration Portal showing the Ping Identity OIDC Redirect URI](/.netlify/images?url=_astro%2Fpingidentity-sso-portal-redirect-uri.CalDFd9a.png\&w=1860\&h=406\&dpl=6ae471fc143c9a4c27550463) In Ping Identity, navigate to **Configuration** tab of recently created application and then click the **Edit** icon. ![Ping Identity Configuration tab for the new OIDC app with the edit action highlighted](/.netlify/images?url=_astro%2Fpingidentity-configuration-overview.BEokSwwr.png\&w=2920\&h=1554\&dpl=6ae471fc143c9a4c27550463) Scroll down to **Redirect URIs**, paste the copied URI into **Sign-in redirect URI**, and then click **Save**. ![Ping Identity redirect URI settings with the Sign-in redirect URI field populated](/.netlify/images?url=_astro%2Fpingidentity-redirect-uri-configuration.Cw9TrmOs.png\&w=2906\&h=1518\&dpl=6ae471fc143c9a4c27550463) 3. ## Provide OIDC Configuration [Section titled “Provide OIDC Configuration”](#provide-oidc-configuration) In Ping Identity, navigate to **Overview** tab of recently created application and copy **Client ID**, **Client Secret** and **Issuer ID** (serves as Issuer URL). ![Ping Identity Overview tab showing Client ID, Client Secret, and Issuer ID](/.netlify/images?url=_astro%2Fpingidentity-client-id-client-secret-and-issuer-id.BSdOyfnh.png\&w=2440\&h=1590\&dpl=6ae471fc143c9a4c27550463) Add the above values under **Identity Provider Configuration** in the **SSO Configuration Portal**, then click **Update**. ![SSO Configuration Portal fields for Ping Identity Client ID and Client Secret](/.netlify/images?url=_astro%2Fpingidentity-sso-portal-client-credentials.DW7_uLQM.png\&w=1858\&h=820\&dpl=6ae471fc143c9a4c27550463) ![SSO Configuration Portal showing the Ping Identity Issuer URL after update](/.netlify/images?url=_astro%2Fpingidentity-sso-portal-issuer-url.DvwPv5Tj.png\&w=1854\&h=880\&dpl=6ae471fc143c9a4c27550463) 4. ## Configure Attributes [Section titled “Configure Attributes”](#configure-attributes) Refer to the list of attributes shown on **SSO Configuration Portal**, these need to be added on Ping Identity. ![SSO Configuration Portal attribute mapping section for Ping Identity OIDC](/.netlify/images?url=_astro%2Fpingidentity-sso-portal-required-attributes.iUy6Qq1U.png\&w=1862\&h=850\&dpl=6ae471fc143c9a4c27550463) In Ping Identity, navigate to **Attribute Mappings** tab and click on **Pencil** icon to add attributes. ![PingIdentity Attribute Mappings tab with the edit action](/.netlify/images?url=_astro%2Fpingidentity-attribute-mappings-edit.BNkTESuS.png\&w=2450\&h=1322\&dpl=6ae471fc143c9a4c27550463) Click on **Add** button and add all attributes shown on **SSO Configuration Portal** to Ping Identity and then click **Save**. ![PingIdentity attribute mappings editor showing the required attributes added](/.netlify/images?url=_astro%2Fpingidentity-attribute-mappings-add-attributes.CuQ7V67C.png\&w=2444\&h=1534\&dpl=6ae471fc143c9a4c27550463) Once you have finished the above step, turn on the toggle button to enable the application. ![PingIdentity application toggle enabled after the attribute configuration is complete](/.netlify/images?url=_astro%2Fpingidentity-enable-application-toggle.CW0fdPEB.png\&w=2448\&h=1486\&dpl=6ae471fc143c9a4c27550463) 5. ## Test Connection [Section titled “Test Connection”](#test-connection) In the **SSO Configuration Portal**, click **Test Connection** to verify your configuration. Note If the connection fails, you’ll see an error, the reason for the error, and a way to solve that error right on the screen. 6. ## Enable Single Sign-On [Section titled “Enable Single Sign-On”](#enable-single-sign-on) Once the test succeeds, click **Enable Connection** to allow users in your organization to sign in with Ping Identity OIDC. ![SSO Configuration Portal with Enable Connection button for Ping Identity OIDC](/.netlify/images?url=_astro%2Fpingidentity-enable-connection.CTuGxMJH.png\&w=1868\&h=244\&dpl=6ae471fc143c9a4c27550463) This completes the Ping Identity OIDC SSO setup for your application. --- # DOCUMENT BOUNDARY --- # PingIdentity SAML > Learn how to configure PingIdentity as a SAML identity provider for secure single sign-on (SSO) with your application. This guide walks you through configuring Ping Identity as your SAML identity provider for the application you are onboarding, enabling secure single sign-on for your users. You’ll learn how to set up an enterprise application, configure SAML settings to the host application. By following these steps, your users will be able to seamlessly authenticate using their Ping Identity credentials. 1. ### Create a custom SAML app in PingIdentity [Section titled “Create a custom SAML app in PingIdentity”](#create-a-custom-saml-app-in-pingidentity) Log in to PingOne Admin Console. Select Applications → Applications. ![Custom SAML app](/.netlify/images?url=_astro%2F0-ping-oidentity-saml.DKvasXIK.png\&w=2932\&h=1598\&dpl=6ae471fc143c9a4c27550463) Add a New SAML Application → Click **+ Add Application**. Enter an **Application Name** and select the **SAML Application** as the Application Type. Click **Configure**. ![Naming the custom SAML app](/.netlify/images?url=_astro%2F0.1-ping-identity-saml.8SlRDUdN.png\&w=2940\&h=1658\&dpl=6ae471fc143c9a4c27550463) 2. ### Configure the Service Provider in Ping Identity [Section titled “Configure the Service Provider in Ping Identity”](#configure-the-service-provider-in-ping-identity) Log in to your SSO configuration portal and click on Single Sign-on (SSO) → Ping Identity → SAML 2.0 for the organization you want to configure it for. ![SSO Configuration Portal](/.netlify/images?url=_astro%2F1-ping-identity-saml.CmRZ1XQq.png\&w=1908\&h=1358\&dpl=6ae471fc143c9a4c27550463) Now, copy the following details from the SSO Configuration Portal: * **ACS URL** (Assertion Consumer Service URL) * **SP Entity ID** (Service Provider Entity ID) * **SP Metadata URL** Paste the details copied from your SSO configuration portal into the respective fields under SAML configuration in the Ping Identity dashboard: * Method 1: Import Metadata ![Import Metadata](/.netlify/images?url=_astro%2F1.1-ping-identity-saml.DPlp0S1W.png\&w=1861\&h=1662\&dpl=6ae471fc143c9a4c27550463) * Method 2: Import from URL ![Import from URL](/.netlify/images?url=_astro%2F1.2-ping-identity-saml.tLpFaw23.png\&w=720\&h=708\&dpl=6ae471fc143c9a4c27550463) * Method 3: Manually Enter ![Manually Enter](/.netlify/images?url=_astro%2F1.3-ping-identity-saml.Cko2VJKF.png\&w=1592\&h=1568\&dpl=6ae471fc143c9a4c27550463) 3. ### Configure Attribute mapping & assign users/groups [Section titled “Configure Attribute mapping & assign users/groups”](#configure-attribute-mapping--assign-usersgroups) #### Attribute mapping [Section titled “Attribute mapping”](#attribute-mapping) For the user profile details to be shared with us at the time of user login as part of SAML response payload, SAML Attributes need to be configured in your Identity Provider portal. To ensure seamless login, the below user profile details are needed: * Email Address * First Name * Last Name To configure these attributes, locate **Attribute Mapping** section in the SAML Configuration page in your Identity Provider’s application, and carefully map the attributes with the Attribute names exactly as shown in the below image. ![Attribute Mapping](/.netlify/images?url=_astro%2F2.1-ping-identity-saml.Q0BC4EsB.png\&w=720\&h=711\&dpl=6ae471fc143c9a4c27550463) #### Assign user/group [Section titled “Assign user/group”](#assign-usergroup) To finish the Service Provider section of the SAML configuration, you need to “add” the users who need to access to this application. Find the User/Group assignment section in your Identity Provider application and select and assign all the required users or user groups that need access to this application via Single Sign-on. ![Assign users & groups](/.netlify/images?url=_astro%2F2.2-ping-identity-saml.W6GRXgKp.png\&w=1592\&h=1576\&dpl=6ae471fc143c9a4c27550463) 4. ### Configure Identity Provider in your SSO configuration portal [Section titled “Configure Identity Provider in your SSO configuration portal”](#configure-identity-provider-in-your-sso-configuration-portal) In your SSO configuration portal, navigate to the Identity Provider Configuration section to complete the setup. You can do this in two ways: * Method 1: Enter the Metadata URL and click update. ![Configure using Metadata URL](/.netlify/images?url=_astro%2F3.1-ping-identity-saml.BpvngQ4R.png\&w=2008\&h=656\&dpl=6ae471fc143c9a4c27550463) * Method 2: Configure manually To do so, enter the IdP entity ID, IdP Single Sign-on URL, and upload the x.509 certificate that you downloaded from Ping Identity. Then, click update. ![Configure using Metadata URL](/.netlify/images?url=_astro%2F3.2-ping-identity-saml.DyU6ufJR.png\&w=2006\&h=1220\&dpl=6ae471fc143c9a4c27550463) 5. ### Verify successful connection by simulating SSO upon clicking Test Connection [Section titled “Verify successful connection by simulating SSO upon clicking Test Connection”](#verify-successful-connection-by-simulating-sso-upon-clicking-test-connection) To verify whether the SAML SSO configuration is completed correctly, click on **Test Connection** on the SSO Configuration Portal. If everything is done correctly, you will see a **Success** response as shown below. ![Test Single Sign On](/.netlify/images?url=_astro%2F3.7zjJqSeQ.png\&w=2198\&h=978\&dpl=6ae471fc143c9a4c27550463) If there’s a misconfiguration, our test will identify the errors and will offer you a way to correct the configuration right on the screen. 6. ### Enable your Single Sign-on connection [Section titled “Enable your Single Sign-on connection”](#enable-your-single-sign-on-connection) After you successfully verified that the connection is configured correctly, you can enable the connection to let your users login to this application via Single Sign-on. Click on **Enable Connection**. ![Enable SSO Connection](/.netlify/images?url=_astro%2F4.CY6-zQP7.png\&w=2194\&h=250\&dpl=6ae471fc143c9a4c27550463) With this, we are done configuring Ping Identity SAML for your application for an SSO login setup. --- # DOCUMENT BOUNDARY --- # Shibboleth SAML > A step-by-step guide to setting up Single Sign-On with Shibboleth as the Identity Provider, including creating an enterprise application, configuring SAML, attribute mapping, assigning users, uploading IdP metadata, testing the connection, and enabling SSO. This guide walks you through configuring Shibboleth as your SAML identity provider for the application you are onboarding, enabling secure single sign-on for your users. You’ll learn how to set up a Shibboleth identity provider, configure SAML settings, map user attributes, and connect it to your application. By following these steps, your users will be able to seamlessly authenticate using their Shibboleth credentials. Note This guide is written for Shibboleth Identity Provider (IdP) version 4.0.1. If you need help with the initial Shibboleth IdP setup, please refer to the [official Shibboleth documentation](https://shibboleth.atlassian.net/wiki/spaces/IDP5/overview) and [download Shibboleth version v4.0.1](https://shibboleth.net/downloads/identity-provider/latest4/). While other versions may work similarly, the specific steps and configuration options shown here are for v4.0.1. ## Configure Shibboleth Identity Provider [Section titled “Configure Shibboleth Identity Provider”](#configure-shibboleth-identity-provider) 1. ### Access Shibboleth configuration files [Section titled “Access Shibboleth configuration files”](#access-shibboleth-configuration-files) Navigate to your Shibboleth IdP installation directory. The configuration files are typically located in the `conf/` directory. Key configuration files you’ll need to modify: * `conf/idp.properties` * `conf/relying-party.xml` * `conf/metadata-providers.xml` * `conf/saml-nameid.xml` * `conf/attributes/inetOrgPerson.xml` 2. ### Configure Entity ID [Section titled “Configure Entity ID”](#configure-entity-id) Open the `conf/idp.properties` file and locate the entity ID configuration. The entity ID should be based on your Shibboleth IdP host. ```properties # Example entity ID configuration idp.entityId = https://your-shibboleth-url/idp/shibboleth ``` Copy this entity ID value and paste it into the **Entity ID** field in your SSO Configuration Portal. 3. ### Configure SAML SSO URL [Section titled “Configure SAML SSO URL”](#configure-saml-sso-url) In your Shibboleth metadata file (`metadata/idp-metadata.xml`), locate the `SingleSignOnService` element with HTTP-Redirect binding: ```xml ``` Copy the `Location` attribute value and paste it into the **IdP Single Sign-on URL** field in your SSO Configuration Portal. 4. ### Configure signing options [Section titled “Configure signing options”](#configure-signing-options) In the `conf/idp.properties` file, ensure the following signing configuration: ```properties # When true, the decision to sign assertions is taken from WantAssertionsSigned property of SP metadata. # When false, the decision to sign assertions is taken from the p:signAssertions property of relying-party.xml # true is the default and recommended value. idp.saml.honorWantAssertionsSigned=true ``` In the `conf/relying-party.xml` file, configure the relying party settings: ```xml ``` Replace `ONBOARDED_APP_SP_ENTITY_ID` with your Entity ID from the SSO Configuration Portal. For example: `https://your-app.scalekit.dev/sso/v1/saml/conn_123456789` 5. ### Configure security certificate [Section titled “Configure security certificate”](#configure-security-certificate) In your `metadata/idp-metadata.xml` file, locate the `` elements. Copy the second certificate (front-channel configuration) and paste it into the **Security Certificate** field in your SSO Configuration Portal. ```xml ``` ## Configure Service Provider metadata [Section titled “Configure Service Provider metadata”](#configure-service-provider-metadata) 1. ### Download SP metadata [Section titled “Download SP metadata”](#download-sp-metadata) In your SSO Configuration Portal, save the SSO configuration and click **Download Metadata** to download the Service Provider metadata file. Refer to [Generic SAML](/guides/integrations/sso-integrations/generic-saml) for detailed instructions. 2. ### Configure metadata provider [Section titled “Configure metadata provider”](#configure-metadata-provider) Move the downloaded metadata file to your Shibboleth IdP metadata directory: ```plaintext /opt/shibboleth-idp/metadata/scalekit-metadata.xml ``` 3. ### Update metadata-providers.xml [Section titled “Update metadata-providers.xml”](#update-metadata-providersxml) Open `conf/metadata-providers.xml` and add the following configuration: ```xml md:SPSSODescriptor ``` Replace the Entity ID with the value from your SSO Configuration Portal. ## Configure attribute mapping [Section titled “Configure attribute mapping”](#configure-attribute-mapping) 1. ### Configure SAML NameID [Section titled “Configure SAML NameID”](#configure-saml-nameid) Open `conf/saml-nameid.xml` and ensure the following configuration is present in the `` section: ```xml ``` 2. ### Configure user attributes [Section titled “Configure user attributes”](#configure-user-attributes) Open `conf/attributes/inetOrgPerson.xml` and configure the attribute mappings. Ensure the following attributes are properly mapped: ```xml mail SAML2StringTranscoder SAML1StringTranscoder email urn:mace:dir:attribute-def:mail E-mail givenName SAML2StringTranscoder SAML1StringTranscoder givenname urn:mace:dir:attribute-def:givenName Given name sn SAML2StringTranscoder SAML1StringTranscoder surname urn:mace:dir:attribute-def:sn Surname ``` 3. ### Map attributes in SSO Configuration Portal [Section titled “Map attributes in SSO Configuration Portal”](#map-attributes-in-sso-configuration-portal) In your SSO Configuration Portal, ensure the attribute mapping section matches the attributes configured in your Shibboleth IdP: * **Email**: `email` * **First Name**: `givenname` * **Last Name**: `surname` ## Configure Identity Provider in SSO Configuration Portal [Section titled “Configure Identity Provider in SSO Configuration Portal”](#configure-identity-provider-in-sso-configuration-portal) 1. ### Upload IdP metadata URL [Section titled “Upload IdP metadata URL”](#upload-idp-metadata-url) In your SSO Configuration Portal, under **Identity Provider Configuration**, select **Configure using Metadata URL**. Enter your Shibboleth IdP metadata URL: ```plaintext https://your-shibboleth-url/idp/shibboleth ``` 2. ### Test the connection [Section titled “Test the connection”](#test-the-connection) Click **Test Connection** to verify that your Shibboleth IdP is properly configured. If successful, you’ll see a success message. If the connection fails, review the error message and check your configuration settings. 3. ### Enable the connection [Section titled “Enable the connection”](#enable-the-connection) Once the test is successful, click **Enable Connection** to activate the SSO integration. ## Advanced configurations Optional [Section titled “Advanced configurations ”](#advanced-configurations--) Note These advanced configurations are optional and can be implemented based on your security requirements. ### Encrypted assertions [Section titled “Encrypted assertions”](#encrypted-assertions) To enable encrypted assertions, update your `conf/idp.properties`: ```properties # Set to true to make encryption optional idp.encryption.optional = true ``` And in `conf/relying-party.xml`, ensure `p:encryptAssertions="true"` is set. ### SAML signature method [Section titled “SAML signature method”](#saml-signature-method) Shibboleth supports SHA256 and SHA1 algorithms for signing certificates. Configure your preferred algorithm in your certificate generation process. ### IdP-initiated SSO [Section titled “IdP-initiated SSO”](#idp-initiated-sso) To test IdP-initiated SSO, use the following URL format: ```plaintext https://your-shibboleth-url/idp/profile/SAML2/Unsolicited/SSO?providerId=ONBOARDED_APP_SP_ENTITY_ID&target=YOUR_RELAY_STATE_URL ``` Replace `ONBOARDED_APP_SP_ENTITY_ID` with your Entity ID and `YOUR_RELAY_STATE_URL` with your desired redirect URL. ## Restart and test Optional [Section titled “Restart and test ”](#restart-and-test-) 1. #### Restart Shibboleth IdP [Section titled “Restart Shibboleth IdP”](#restart-shibboleth-idp) After making all configuration changes, restart your Shibboleth IdP service to apply the changes. 2. #### Test authentication [Section titled “Test authentication”](#test-authentication) Navigate to your application and attempt to sign in using SSO. You should be redirected to your Shibboleth IdP login page. 3. #### Verify user attributes [Section titled “Verify user attributes”](#verify-user-attributes) After successful authentication, verify that user attributes are properly mapped and displayed in your application. With this configuration, your Shibboleth IdP is now integrated with your application, enabling secure single sign-on for your users. Users can authenticate using their Shibboleth credentials and access your application seamlessly. --- # DOCUMENT BOUNDARY --- # SCIM integrations > Step by Step guide to provisioning over own SCIM implementation SCIM (System for Cross-domain Identity Management) is a standardized protocol for automating user provisioning between identity providers and applications. This section provides guides for setting up SCIM integration with various identity providers. Choose your identity provider from the guides below to get started with SCIM integration: ### Microsoft Entra ID (Azure AD) Automate user provisioning with Microsoft Entra ID [Know more →](/guides/integrations/scim-integrations/azure-scim) ### Okta Automate user provisioning with Okta [Know more →](/guides/integrations/scim-integrations/okta-scim) ![OneLogin logo](/assets/logos/onelogin.svg) ### OneLogin Automate user provisioning with OneLogin [Know more →](/guides/integrations/scim-integrations/onelogin) ![JumpCloud logo](/assets/logos/jumpcloud.png) ### JumpCloud Automate user provisioning with JumpCloud [Know more →](/guides/integrations/scim-integrations/jumpcloud) ### Google Workspace Automate user provisioning with Google Workspace [Know more →](/guides/integrations/scim-integrations/google-dir-sync/) ![PingIdentity logo](/assets/logos/pingidentity.png) ### PingIdentity Automate user provisioning with PingIdentity [Know more →](/guides/integrations/scim-integrations/pingidentity-scim) ### Generic SCIM Configure SCIM provisioning with any SCIM-compliant identity provider [Know more →](/guides/integrations/scim-integrations/generic-scim) --- # DOCUMENT BOUNDARY --- # Microsoft Azure AD > Integrate Microsoft Entra ID with the host application for seamless user management This guide helps administrators sync their EntraID directory with an application they want to onboard to their organization. Integrating your application with Entra ID automates user management tasks and ensures access rights stay up-to-date. This registration sets up the following: 1. **Endpoint**: This is the URL where EntraID sends requests to the onboarded app, acting as a communication point between them. 2. **Bearer Token**: Used by EntraID to authenticate its requests to the endpoint, ensuring security and authorization. These components enable seamless synchronization between your application and the EntraID directory. 1. ## Create an endpoint and API token [Section titled “Create an endpoint and API token”](#create-an-endpoint-and-api-token) Select the “SCIM Provisioning” tab to display a list of Directory Providers. Choose “Entra ID” as your Directory Provider. If the Admin Portal is not accessible from the app, request instructions from the app owner. ![Setting up Directory Sync in the admin portal of an app being onboarded: Entra ID selected as the provider, awaiting configuration](/.netlify/images?url=_astro%2F1.CQS3bBUE.png\&w=3024\&h=1728\&dpl=6ae471fc143c9a4c27550463) Click “Configure” after selecting “EntraID” to generate an Endpoint URL and Bearer token for your organization, allowing the app to listen to events and maintain synchronization. ![Endpoint URL and Bearer token for your organization.](/.netlify/images?url=_astro%2F00-2.D96-Qheg.png\&w=2546\&h=1252\&dpl=6ae471fc143c9a4c27550463) 2. ## Add a new application in Entra ID [Section titled “Add a new application in Entra ID”](#add-a-new-application-in-entra-id) To send user-related updates to the app you want to onboard, create a new app in Microsoft Entra ID. Go to the Microsoft Azure portal and select “Microsoft Entra ID”. ![Microsoft Entra ID in the Azure portal.](/.netlify/images?url=_astro%2F01.CeRcx4O1.png\&w=3444\&h=1490\&dpl=6ae471fc143c9a4c27550463) In the “Manage > Enterprise applications” tab, click ”+ New application”. ![Adding a new application in Microsoft Entra ID.](/.netlify/images?url=_astro%2F02.dya-ABTH.png\&w=3428\&h=1388\&dpl=6ae471fc143c9a4c27550463) Click ”+ Create your own application” in the modal that opens on the right. ![Creating a new application in Microsoft Entra ID.](/.netlify/images?url=_astro%2F03.XR0kXsrp.png\&w=3444\&h=1962\&dpl=6ae471fc143c9a4c27550463) Name the app you want to onboard (e.g., “Hero SaaS”) and click “Create”, leaving other defaults as-is. ![Creating a new application in Microsoft Entra ID.](/.netlify/images?url=_astro%2F04.C1s6LF6_.png\&w=3442\&h=1662\&dpl=6ae471fc143c9a4c27550463) 3. ## Configure provisioning settings [Section titled “Configure provisioning settings”](#configure-provisioning-settings) In the created application go to “Manage → Provisioning” ![Open a provisioning tab from created application](/.netlify/images?url=_astro%2F04.C1s6LF6_.png\&w=3442\&h=1662\&dpl=6ae471fc143c9a4c27550463) In the “Hero SaaS” app’s overview, select “Manage > Provisioning” from the left sidebar. ![Configuring provisioning for the "Hero SaaS" app.](/.netlify/images?url=_astro%2F05.apLN7m-U.png\&w=3024\&h=1186\&dpl=6ae471fc143c9a4c27550463) Set the Provisioning Mode to “Automatic”. In the Admin Credentials section, set: * Tenant URL: *Endpoint* * Secret Token: *Bearer Token generated previously* ![Setup Provisioning Mode and Admin Credentials.](/.netlify/images?url=_astro%2F06.CypNkJ9c.png\&w=3020\&h=1236\&dpl=6ae471fc143c9a4c27550463) Once the credentials are configured, Test your connection and click “Save”. In the Mappings section, click “Provision Microsoft Entra ID Users” and toggle “Enabled” to “Yes”. ![Making sure the "Provision Microsoft Entra ID Users" is enabled.](/.netlify/images?url=_astro%2F07.CFwmk-YB.png\&w=3022\&h=1426\&dpl=6ae471fc143c9a4c27550463) ![Making sure the "Provision Microsoft Entra ID Users" is enabled.](/.netlify/images?url=_astro%2F08.rxOhmgro.png\&w=3442\&h=1634\&dpl=6ae471fc143c9a4c27550463) Close the modal and reload the page for changes to take effect. Go to “Overview > Manage > Provisioning” and ensure “Provisioning Status” is toggled “On”. ![Making sure the "Provisioning Status" is toggled "On".](/.netlify/images?url=_astro%2F010.DaG4ASiO.png\&w=3020\&h=1282\&dpl=6ae471fc143c9a4c27550463) Entra ID is now set up to send events to Hero SaaS when users are added or removed. 4. ## Map custom attributes (optional) [Section titled “Map custom attributes (optional)”](#map-custom-attributes-optional) By default, Entra ID syncs standard attributes such as email, first name, last name, and display name. To sync a custom attribute (for example, a department code or employee ID), you must map it explicitly in the provisioning configuration. In your app’s **Provisioning** settings, click **Edit attribute mappings** under the **Mappings** section. At the bottom of the page, select **Show advanced options**, then click **Edit attribute list for \[app name]**. Add the custom target attribute as a new SCIM extension schema field (for example, `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber`). This ensures the attribute exists for mapping. In the attribute mapping list, click **Add new mapping** at the bottom. Configure the mapping: * **Mapping type**: Select **Direct**. * **Source attribute**: Select the Entra ID attribute that contains the value you want to sync (for example, `employeeId` or a custom extension attribute like `extension__`). * **Target attribute**: Select or type the matching SCIM attribute name as configured in Scalekit (for example, `urn:ietf:params:scim:schemas:extension:enterprise:2.0:User:employeeNumber`). Click **Ok**, then save the provisioning configuration. Note Custom extension attributes in Entra ID follow the naming pattern `extension__`, where `` is the Application (client) ID from your Entra app registration. You can find the Application (client) ID in the Entra ID portal under **App registrations > Your app > Application (client) ID**. Entra-synced on-prem Active Directory extension attributes appear in this same format (for example, `extension__employeeNumber`). Entra ID takes up to 40 minutes for attribute changes to propagate to the application during a sync cycle. To test immediately, use **Provision on demand**. 5. ## Assign User and Group [Section titled “Assign User and Group”](#assign-user-and-group) In the created application, go to “Users and groups” and click ”+ Add user/group” ![Go to Users and Groups tab for assign a user](/.netlify/images?url=_astro%2F022.CphZwAWR.png\&w=1440\&h=500\&dpl=6ae471fc143c9a4c27550463) Click the button under the “Users and Groups”. In the menu, select the users and groups that you want to add to the SCIM application, and click “Select”. ![Assign users to application](/.netlify/images?url=_astro%2F023.CITSkxnj.png\&w=1440\&h=782\&dpl=6ae471fc143c9a4c27550463) Once the users are selected, the “Assign” button is automatically enabled. Click “Assign”. 6. ## Test user and group provisioning [Section titled “Test user and group provisioning”](#test-user-and-group-provisioning) In the Hero SaaS Application, go to “Provision on demand”. Input a user name from your user list and click “Provision”. ![Provisioning a user/group on demand.](/.netlify/images?url=_astro%2F020.BVzVczj2.png\&w=3006\&h=1050\&dpl=6ae471fc143c9a4c27550463) Once provisioned, the user should appear in the admin portal, showing how many users have access to the Hero SaaS app. ![Group (Admins) provisioned in the admin portal.](/.netlify/images?url=_astro%2F013.rUdzM7KU.png\&w=2520\&h=1124\&dpl=6ae471fc143c9a4c27550463) Note Provisioning or deprovisioning users can be done from “Manage > User and groups > Add user/group”. [Entra ID takes up to 40 minutes](https://learn.microsoft.com/en-us/entra/identity/app-provisioning/use-scim-to-provision-users-and-groups#getting-started:~:text=Once%20connected%2C%20Microsoft%20Entra%20ID%20runs%20a%20synchronization%20process.%20The%20process%20runs%20every%2040%20minutes.%20The%20process%20queries%20the%20application%27s%20SCIM%20endpoint%20for%20assigned%20users%20and%20groups%2C%20and%20creates%20or%20modifies%20them%20according%20to%20the%20assignment%20details.) for the changes to propagate to the application. --- # DOCUMENT BOUNDARY --- # Generic SCIM > Learn how to configure a generic SCIM identity provider for automated user provisioning and management with your application. This guide walks you through configuring a generic SCIM identity provider for your application, enabling automated user provisioning and management for your users. You’ll learn how to set up SCIM integration, configure endpoint credentials, assign users and groups, and map roles. 1. ## Directory details [Section titled “Directory details”](#directory-details) Open the Admin Portal from the app being onboarded and select the “SCIM Provisioning” tab. A list of Directory Providers will be displayed. Choose “Custom Provider” as your Directory Provider. If the Admin Portal is not accessible from the app, request instructions from the app owner. After selecting “Custom Provider,” click “Configure.” This action will generate an Endpoint URL and Bearer token for your organization, allowing the app to listen to events and maintain synchronization with your organization. Copy and paste the **Endpoint URL** and the **Bearer Token** into your Custom Provider. Use the copy icons next to each field to copy the credentials. Important Make sure to copy your new bearer token now. You won’t be able to see it again. 2. ## Configure SCIM application in your identity provider [Section titled “Configure SCIM application in your identity provider”](#configure-scim-application-in-your-identity-provider) Log in to your identity provider’s admin dashboard and navigate to the Applications or Integrations section. Create a new SCIM application or integration. Select SCIM 2.0 as the provisioning protocol. Enter the **Endpoint URL** and **Bearer Token** you copied from the SCIM Configuration Portal into the appropriate fields in your identity provider. This typically includes: * SCIM 2.0 Base URL (paste the Endpoint URL) * OAuth Bearer Token or API Token (paste the Bearer Token) Test the API credentials if your identity provider provides this option to verify the connection. 3. ## Assign users and groups [Section titled “Assign users and groups”](#assign-users-and-groups) Assign appropriate users and groups you wish to provision with your application in your Custom Provider account. Complete the provisioning setup and assign users or groups according to your identity provider’s interface. This typically involves: * Navigating to the Assignments or Users section * Selecting individual users or groups to provision * Configuring any user attribute mappings if required After assigning users and groups, your identity provider will begin sending provisioning requests to your application’s SCIM endpoint. 4. ## Group based role assignment [Section titled “Group based role assignment”](#group-based-role-assignment) Map directory groups to your application’s roles. Users without an explicit role assignment will be assigned the default Administrator role. In the SCIM Configuration Portal, navigate to the Group Based Role Assignment section. Once groups are synced from your directory, you can map each directory group to a specific role in your application. This allows you to automatically assign roles to users based on their group membership in your identity provider, ensuring users receive the appropriate permissions when they are provisioned. 5. ## Verify successful connection [Section titled “Verify successful connection”](#verify-successful-connection) After completing these steps, verify that the users and groups are successfully synced by visiting the Users and Groups tabs in the Admin Portal. You can also check the Events tab to monitor provisioning activities and ensure that user creation, updates, and deactivations are being processed correctly. With this, we are done configuring your application for SCIM-based user provisioning with a generic SCIM identity provider. --- # DOCUMENT BOUNDARY --- # Google Workspace Directory > Integrate Google Workspace with the host application for seamless user management This guide helps administrators sync their Google Workspace directory with an application they want to onboard to their organization. Integrating your application with Google Workspace automates user management tasks and ensures access rights stay up-to-date. 1. ## Access the directory configuration screen [Section titled “Access the directory configuration screen”](#access-the-directory-configuration-screen) Navigate to the Admin Portal of your application and select the “SCIM Provisioning” tab. You’ll see a list of available directory providers. ![Directory Sync configuration screen with various provider options.](/.netlify/images?url=_astro%2F1.CQS3bBUE.png\&w=3024\&h=1728\&dpl=6ae471fc143c9a4c27550463) 2. ## Select Google Workspace [Section titled “Select Google Workspace”](#select-google-workspace) From the list of directory providers, locate and click on “Google Workspace”. ![Select Google Workspace from the available directory providers.](/.netlify/images?url=_astro%2F2.TXVFof2w.png\&w=1210\&h=691\&dpl=6ae471fc143c9a4c27550463) 3. ## Begin configuration [Section titled “Begin configuration”](#begin-configuration) Click on the “Configure” button to start setting up the Google Workspace integration. ![Click Configure to begin setting up the Google Workspace integration.](/.netlify/images?url=_astro%2F3.BGFAlsuv.png\&w=1210\&h=691\&dpl=6ae471fc143c9a4c27550463) 4. ## Authorize Google Workspace [Section titled “Authorize Google Workspace”](#authorize-google-workspace) To establish the connection, you need to authorize access to your Google Workspace directory. Click on “Authorize Google Workspace”. ![Click Authorize Google Workspace to begin the authorization process.](/.netlify/images?url=_astro%2F5.CZhTpbxq.png\&w=1210\&h=691\&dpl=6ae471fc143c9a4c27550463) 5. ## Sign in with Google admin account [Section titled “Sign in with Google admin account”](#sign-in-with-google-admin-account) You’ll be redirected to Google’s authentication page. Sign in with your Google Workspace administrator account. If you’re already signed in with multiple accounts, select “Use another account” to ensure you’re using your administrator account. ![Select "Use another account" if you need to sign in with a different Google account.](/.netlify/images?url=_astro%2F6.C5VuSPZB.png\&w=1279\&h=731\&dpl=6ae471fc143c9a4c27550463) 6. ## Enter administrator credentials [Section titled “Enter administrator credentials”](#enter-administrator-credentials) Enter your Google Workspace administrator email address and password when prompted. ![Enter your Google Workspace administrator email address.](/.netlify/images?url=_astro%2F7.kWm28OYJ.png\&w=1210\&h=691\&dpl=6ae471fc143c9a4c27550463) 7. ## Grant required permissions [Section titled “Grant required permissions”](#grant-required-permissions) When prompted, review and confirm the permissions requested by the application. These permissions allow the application to read user and group information from your Google Workspace directory. ![Review the requested permissions for directory access.](/.netlify/images?url=_astro%2F16.S_AvrKMH.png\&w=1210\&h=691\&dpl=6ae471fc143c9a4c27550463) Click “Continue” to grant the necessary permissions. ![Click Continue to grant directory access permissions.](/.netlify/images?url=_astro%2F19.BQerPaAG.png\&w=1210\&h=691\&dpl=6ae471fc143c9a4c27550463) 8. ## Select groups to sync [Section titled “Select groups to sync”](#select-groups-to-sync) After authorization, you’ll see the groups available in your Google Workspace directory. Select the groups you want to synchronize with your application. ![Select which Google Workspace groups you want to sync with your application.](/.netlify/images?url=_astro%2F21.CdFRFUIj.png\&w=1424\&h=814\&dpl=6ae471fc143c9a4c27550463) 9. ## Map IdP groups to application roles [Section titled “Map IdP groups to application roles”](#map-idp-groups-to-application-roles) Map IdP groups to application roles to control access to your application. This needs to be enabled by the host application. ![Map IdP groups to application roles to control access to your application.](/.netlify/images?url=_astro%2F22-5.DCQq23rD.png\&w=2150\&h=1560\&dpl=6ae471fc143c9a4c27550463) 10. ## Enable directory sync [Section titled “Enable directory sync”](#enable-directory-sync) After selecting your groups, click “Enable Sync” to activate the integration. ![Click Enable Sync to start synchronizing users and groups from Google Workspace.](/.netlify/images?url=_astro%2F26.Br2FdDGh.png\&w=1210\&h=691\&dpl=6ae471fc143c9a4c27550463) Note If you encounter issues during synchronization: 1. **Authorization errors**: Ensure you have sufficient privileges to authorize us to access your users and groups information from your Google Workspace directory. 2. **Missing users/groups**: We automatically fetch latest users and groups from Google Workspace directory once every hour. If you would like to trigger a sync manually, use the “Sync Now” button in the Actions menu. --- # DOCUMENT BOUNDARY --- # JumpCloud Directory > Learn how to sync your JumpCloud directory with your application for automated user provisioning and management using SCIM. This guide helps administrators sync their JumpCloud directory with an application they want to onboard to their organization. Integrating your application with JumpCloud automates user management tasks and ensures access rights stay up-to-date. This registration sets up the following: 1. **Endpoint**: This is the URL where JumpCloud sends requests to the onboarded app, acting as a communication point between them. 2. **Bearer Token**: Used by JumpCloud to authenticate its requests to the endpoint, ensuring security and authorization. These components enable seamless synchronization between your application and the JumpCloud directory. 1. ## Create an endpoint and API token [Section titled “Create an endpoint and API token”](#create-an-endpoint-and-api-token) Open the Admin Portal and select the “SCIM Provisioning” tab. A list of Directory Providers will be displayed. Choose “JumpCloud” as your Directory Provider. If the Admin Portal is not accessible from the app, request instructions from the app owner. ![SCIM Provisioning Setup](/.netlify/images?url=_astro%2F1-select-jumpcloud.C4cBWmOg.png\&w=1996\&h=1090\&dpl=6ae471fc143c9a4c27550463) ![SCIM Provisioning Setup](/.netlify/images?url=_astro%2F1-2-scimconfigs.Bw-b_DTR.png\&w=2010\&h=1466\&dpl=6ae471fc143c9a4c27550463) This action will generate an Endpoint URL and Bearer token for your organization, allowing the app to listen to events and maintain synchronization with your organization. 2. ## Add a new application in JumpCloud [Section titled “Add a new application in JumpCloud”](#add-a-new-application-in-jumpcloud) Go to the JumpCloud Admin Portal > SSO Applications and click on ”+ Add New Application.” ![Add New Application](/.netlify/images?url=_astro%2F2-add-new-app.D7dDnWjE.png\&w=1440\&h=542\&dpl=6ae471fc143c9a4c27550463) Create a custom application by trying to do an non-existent application search. ![Application Selection](/.netlify/images?url=_astro%2F3-custom-integration.Eh3vJT8o.png\&w=3024\&h=1140\&dpl=6ae471fc143c9a4c27550463) Click “Next” and choose the features you would like to enable. Since your application wants to provision new users and user updates from JumpCloud, select “Export users to this app (Identity Management)” ![Feature Selection](/.netlify/images?url=_astro%2F4-export-users.DqnxmZx6.png\&w=3008\&h=1708\&dpl=6ae471fc143c9a4c27550463) Finally, enter the general info such as display name (this example uses “YourApp”) and click “Save Application” ![Successful addition](/.netlify/images?url=_astro%2F5-success-app-creation.QNOKo7Pu.png\&w=3022\&h=1712\&dpl=6ae471fc143c9a4c27550463) 3. ## Configure provisioning settings [Section titled “Configure provisioning settings”](#configure-provisioning-settings) Click on “Configure Application” and proceed to configure the application settings. This opens a modal with “Identity Management” selected. Enter the Endpoint URL and Bearer Token provided in the Step 1. ![Configure Application Settings](/.netlify/images?url=_astro%2F6-scim-config-page.iPFOiYMx.png\&w=2544\&h=1718\&dpl=6ae471fc143c9a4c27550463) 4. ## Configure group management [Section titled “Configure group management”](#configure-group-management) JumpCloud uses groups as the primary way provision users to your application. ![Provisioning Settings](/.netlify/images?url=_astro%2F7-group-management.Bx7mLD3r.png\&w=2542\&h=1716\&dpl=6ae471fc143c9a4c27550463) Click “Activate”. 5. ## Assign users and groups [Section titled “Assign users and groups”](#assign-users-and-groups) To assign groups and users to the newly integrated application: ![User Assignment](/.netlify/images?url=_astro%2F8-group-assigned.C4iBMJHM.png\&w=2548\&h=1704\&dpl=6ae471fc143c9a4c27550463) 1. Navigate to “User Groups” from the top navigation panel. 2. If required, create a new group under “User Management” → “User Groups”. 3. Add users to the group. If no users exist, create them under “User Management” → “Users”. 4. Select the group to be assigned to the application and click “Save”. 5. Confirm the newly created group is assigned to the application. Tip Make sure to organize your users into groups for easier management and assignment of permissions. 6. ## Group based Role Assignment Configuration [Section titled “Group based Role Assignment Configuration”](#group-based-role-assignment-configuration) To automatically assign roles to users based on their group membership, configure appropriate group to role mapping in the SCIM Configuration Portal. 7. ## Verify successful connection [Section titled “Verify successful connection”](#verify-successful-connection) After completing these steps, verify that the users and groups are successfully synced by visiting Users and Groups tab in the Admin Portal. ![Verification Process](/.netlify/images?url=_astro%2F9-synced-user.txzrA8bK.png\&w=1982\&h=1668\&dpl=6ae471fc143c9a4c27550463) Note When an group is disassociated from an app in JumpCloud (“YourApp”), JumpCloud sends an group update event that unassigns all the group users to your app. However, the group association is not removed automatically. --- # DOCUMENT BOUNDARY --- # Okta Directory > Learn how to sync your Okta Directory with your application for automated user provisioning and management using SCIM. This guide is designed to help administrators seamlessly sync their Okta Directory with an application they want to onboard to their organization. By integrating your application with Okta, you can automate user management tasks and ensure that access rights are consistently up-to-date. This registration sets up the following: 1. **Endpoint**: This is the URL where Okta will send requests to the app you are onboarding. It acts as a communication point between Okta and your application. 2. **Bearer Token**: This token is used by Okta to authenticate its requests to the endpoint. It ensures that the requests are secure and authorized. By setting up these components, you enable seamless synchronization between your application and the Okta directory. 1. ## Create an endpoint and API token [Section titled “Create an endpoint and API token”](#create-an-endpoint-and-api-token) Open the Admin Portal from the app being onboarded and select the “SCIM Provisioning” tab. A list of Directory Providers will be displayed. Choose “Okta” as your Directory Provider. If the Admin Portal is not accessible from the app, request instructions from the app owner. ![Okta SCIM](/.netlify/images?url=_astro%2F0.DMVGZBR9.png\&w=1436\&h=710\&dpl=6ae471fc143c9a4c27550463) ![Okta directory sync setup: Endpoint URL and one-time visible bearer token provided.](/.netlify/images?url=_astro%2F5.BDN_v6Vw.png\&w=1834\&h=716\&dpl=6ae471fc143c9a4c27550463) After selecting “Okta,” click “Configure.” This action will generate an Endpoint URL and Bearer token for your organization, allowing the app to listen to events and maintain synchronization with your organization. 2. ## Add a new application in Okta [Section titled “Add a new application in Okta”](#add-a-new-application-in-okta) Log in to the Okta admin dashboard and navigate to “Applications” in the main menu. ![Okta app catalog: SCIM 2.0 Test App integration options displayed.](/.netlify/images?url=_astro%2F1-scim-search.CCyBpUkD.png\&w=3092\&h=1945\&dpl=6ae471fc143c9a4c27550463) If you haven’t previously created a SCIM application in Okta, select “Browse App Catalog.” Otherwise, choose it from your existing list of applications. In the Okta Application dashboard, search for “SCIM 2.0 Test App (OAuth Bearer Token)” and select the corresponding result. Click “Add Integration” on the subsequent page. ![Adding SCIM 2.0 Test App integration in Okta for app being onboarded](/.netlify/images?url=_astro%2F2.Cq-a3UX9.png\&w=3024\&h=1893\&dpl=6ae471fc143c9a4c27550463) Provide a descriptive name for the app, then proceed by clicking “Next.” ![Naming the app 'Hero SaaS' during SCIM 2.0 Test App integration in Okta.](/.netlify/images?url=_astro%2F3.Dd-07UK_.png\&w=3018\&h=1888\&dpl=6ae471fc143c9a4c27550463) The default configuration is typically sufficient for most applications. However, if your directory requires additional settings, such as Attribute Statements, configure these on the Sign-On Options page. Complete the application creation process by clicking “Done.” 3. ## Enable sending and receiving events in provisioning settings [Section titled “Enable sending and receiving events in provisioning settings”](#enable-sending-and-receiving-events-in-provisioning-settings) In your application’s Enterprise Okta admin panel, navigate to the “Provisioning” tab and select “Configure API Integration.” ![Enabling API Integration in Okta for app being onboarded.](/.netlify/images?url=_astro%2F4.B7EGyeQ-.png\&w=3104\&h=1968\&dpl=6ae471fc143c9a4c27550463) Copy the Endpoint URL and Bearer Token from your Admin Portal and paste them into the *SCIM 2.0 Base URL* field and *OAuth Bearer Token* field, respectively. Verify the configuration by clicking “Test API Credentials,” then save the settings. ![Verifying SCIM credentials for Hero SaaS integration in Okta](/.netlify/images?url=_astro%2F6.CaukcGaU.png\&w=3018\&h=1888\&dpl=6ae471fc143c9a4c27550463) Give provisioning permissions to the API integration. This is necessary to allow Okta to send and receive events to the app. Upon successful configuration, the Provisioning tab will display a new set of options. These options will be utilized to complete the provisioning process for your application. ![Saving verified SCIM API integration settings for Hero SaaS in Okta](/.netlify/images?url=_astro%2F7.0a3Wq58T.png\&w=3018\&h=1895\&dpl=6ae471fc143c9a4c27550463) 4. ## Configure provisioning options [Section titled “Configure provisioning options”](#configure-provisioning-options) In the “To App” navigation section, enable the following options: * Create Users * Update User Attributes * Deactivate Users ![Granting provisioning permissions to Hero SaaS app in Okta SCIM integration](/.netlify/images?url=_astro%2F4.1.BXM3aqPb.png\&w=3022\&h=1888\&dpl=6ae471fc143c9a4c27550463) After enabling these options, click “Save” to apply the changes. These settings allow Okta to perform user provisioning actions in your application, including creating new user accounts, updating existing user information, and deactivating user accounts when necessary. 5. ## Assign users and groups [Section titled “Assign users and groups”](#assign-users-and-groups) ![Assigning users to Hero SaaS in Okta: Options to assign to individuals or groups](/.netlify/images?url=_astro%2F10.FoKsuCaF.png\&w=3022\&h=1894\&dpl=6ae471fc143c9a4c27550463) To assign users to the SAML Application: 1. Navigate to the “Assignments” tab. 2. From the “Assign” dropdown, select “Assign to People.” 3. Choose the users you want to provision and click “Assign.” 4. A form will open for each user. Review and populate the user’s metadata fields. 5. Scroll to the bottom and click “Save and Go Back.” 6. Repeat this process for all users, then select “Done.” ![Assigning users to Hero SaaS in Okta: Selecting individuals for access](/.netlify/images?url=_astro%2F12.Cf66BaYw.png\&w=3022\&h=1893\&dpl=6ae471fc143c9a4c27550463) Assigning groups does not sync group membership Adding groups under the **Assignments** tab grants their members access to the application. It does **not** push group structures to the application via SCIM — to sync group membership and enable group-based role assignment, complete **Step 6: Push groups**. 6. ## Push groups and sync group membership [Section titled “Push groups and sync group membership”](#push-groups-and-sync-group-membership) To push groups and sync group membership: 1. Navigate to the “Push Groups” tab. 2. From the “Push Groups” dropdown, select “Find groups by name.” 3. Search for and select the group you want to push. 4. Ensure the “Push Immediately” box is checked. 5. Click “Save.” ![Pushing group memberships to SCIM 2.0 Test App: Configuring the 'Avengers' group in Okta](/.netlify/images?url=_astro%2F15.7COWTi0T.png\&w=3024\&h=1888\&dpl=6ae471fc143c9a4c27550463) IMPORTANT For accurate group membership synchronization, ensure that the same groups are not configured for push groups and group assignments. If the same groups are configured in both assignments and push groups, manual group pushes may be required for accurate membership reflection.\ [Okta documentation](https://support.okta.com/help/s/article/Group-Push-Common-Issues?language=en_US) Deactivated users are skipped by Group Push If a user is removed from an **assigning group** first, Okta deactivates them for the app and sends that event correctly. But Okta excludes deactivated users from Group Push updates, so a subsequent removal from a **push group** is not pushed to the application — the user keeps showing up in that group’s membership downstream. To fix a stuck membership, reactivate the user in Okta and run Group Push again for each group the user belongs to.\ See Okta’s [Group Push Common Issues](https://support.okta.com/help/s/article/Group-Push-Common-Issues?language=en_US) article for more on this behavior. 7. ## Group based Role Assignment Configuration [Section titled “Group based Role Assignment Configuration”](#group-based-role-assignment-configuration) To automatically assign roles to users based on their group membership, configure appropriate group to role mapping in the SCIM Configuration Portal. ![Pushing group memberships to SCIM 2.0 Test App: Configuring the 'Avengers' group in Okta](/.netlify/images?url=_astro%2Fgbra.BsEwopaT.png\&w=2030\&h=1168\&dpl=6ae471fc143c9a4c27550463) 8. ## Verify successful connection [Section titled “Verify successful connection”](#verify-successful-connection) After completing these steps, verify that the users and groups are successfully synced by visiting Users and Groups tab in the Admin Portal. ![Verification Process](/.netlify/images?url=_astro%2Fverify.C34VXqG5.png\&w=1864\&h=1482\&dpl=6ae471fc143c9a4c27550463) --- # DOCUMENT BOUNDARY --- # OneLogin Directory > Learn how to sync your OneLogin directory with your application for automated user provisioning and management using SCIM. This guide helps administrators sync their OneLogin directory with an application they want to onboard. Integrating your application with OneLogin automates user management tasks and keeps access rights up-to-date. Setting up the integration involves: 1. **Endpoint**: The URL where OneLogin sends requests to your application, enabling communication between them. 2. **Bearer Token**: A token OneLogin uses to authenticate its requests to the endpoint, ensuring security and authorization. By setting up these components, you enable seamless synchronization between your application and the OneLogin directory. 1. ## Create an endpoint and API token [Section titled “Create an endpoint and API token”](#create-an-endpoint-and-api-token) Open the SCIM configuration portal and select the **SCIM Provisioning** tab. Choose **OneLogin** as your Directory Provider and click on **Configure**. ![Setting up Directory Sync in the admin portal of an app being onboarded: OneLogin selected as the provider, awaiting configuration](/.netlify/images?url=_astro%2F0-1.D3xuY6YO.png\&w=2268\&h=1248\&dpl=6ae471fc143c9a4c27550463) 2. ## Add a new application in OneLogin [Section titled “Add a new application in OneLogin”](#add-a-new-application-in-onelogin) Open OneLogin’s **Administration** portal. Click **Applications** from the top navigation panel. ![OneLogin Administration Applications](/.netlify/images?url=_astro%2F2.CIZ3WlOD.png\&w=3024\&h=1964\&dpl=6ae471fc143c9a4c27550463) Click **Add App** to add a new application. ![The OneLogin Applications page displays a list of apps with options to download JSON or add a new app.](/.netlify/images?url=_astro%2F3.C0ajCMDJ.png\&w=3016\&h=1034\&dpl=6ae471fc143c9a4c27550463) Search for **SCIM with SAML (SCIM v2 Enterprise)** and select it. ![OneLogin application search results for SCIM Provisioner with SAML displaying SCIM v2 Enterprise option.](/.netlify/images?url=_astro%2F4.4SYApMxX.png\&w=3022\&h=784\&dpl=6ae471fc143c9a4c27550463) Give a suitable app name(e.g., **Hero SaaS App**) and then click **Save**. ![Configuring the portal settings for the application in OneLogin, including display name and icon options.](/.netlify/images?url=_astro%2F5.DUZ4kYAe.png\&w=3112\&h=1718\&dpl=6ae471fc143c9a4c27550463) Go to the **SCIM configuration portal** and copy the **Endpoint URL** and **Bearer Token** for the SCIM integration. ![OneLogin directory sync setup: Endpoint URL and one-time visible bearer token provided](/.netlify/images?url=_astro%2F0-2.0GTrlug7.png\&w=2258\&h=1012\&dpl=6ae471fc143c9a4c27550463) On OneLogin, go to the **Configuration** tab in the left navigation panel. Add the above copied values in the **SCIM Base URL** and **SCIM Bearer Token** fields. Then click the **Enable** button. ![Configure credentials in the OneLogin dashboard.](/.netlify/images?url=_astro%2F20.CGDipbFD.png\&w=3024\&h=1632\&dpl=6ae471fc143c9a4c27550463) Go to the **Provisioning** tab, enable provisioning, and click **Save**. ![Setting up provisioning workflow for SCIM Provisioner with SAML in OneLogin, including options for user creation, deletion, and suspension actions.](/.netlify/images?url=_astro%2F21.DRqvHKMS.png\&w=3109\&h=1708\&dpl=6ae471fc143c9a4c27550463) 3. ## Provision users [Section titled “Provision users”](#provision-users) Go to **Users** and click on a user you want to provision. ![OneLogin Users dashboard displaying user information, including roles, last login time, and account status.](/.netlify/images?url=_astro%2F7.B8xRGSP6.png\&w=2972\&h=1542\&dpl=6ae471fc143c9a4c27550463) Note You can create a new user for testing. Ensure users have a **username** property, which will be treated as a unique identifier in SCIM implementations. Using an email address as the username is also allowed. Go to the **Applications** tab from the left navigation bar, click **+**, and assign the recently created application. Click **Continue**. ![Assigning a new login to a user in OneLogin](/.netlify/images?url=_astro%2F8.Bd38Ai2c.png\&w=2998\&h=886\&dpl=6ae471fc143c9a4c27550463) The user provisioning action will remain in pending state for the application. Click on **Pending**. ![Provision user to SCIM application.](/.netlify/images?url=_astro%2F22.hQWiw3ly.png\&w=3024\&h=1104\&dpl=6ae471fc143c9a4c27550463) In the new modal, click on **Approve** to approve provisioning of the user in the application. ![OneLogin user provisioning dialog for creating Kitty Flake in Hero SaaS App, with options to approve or skip the action.](/.netlify/images?url=_astro%2F23.pzvGm59K.png\&w=3024\&h=854\&dpl=6ae471fc143c9a4c27550463) The status should change to **Provisioned** within a few seconds. ![OneLogin user profile for Kitty Flake displaying assigned applications, with Hero SaaS App provisioned and admin-configured.](/.netlify/images?url=_astro%2F10.Dmb1ISv9.png\&w=2972\&h=966\&dpl=6ae471fc143c9a4c27550463) 4. ## Configure group provisioning [Section titled “Configure group provisioning”](#configure-group-provisioning) From the top navigation, click on **Users** and select **Roles** from the dropdown. ![Navigate to roles tab.](/.netlify/images?url=_astro%2F24.9wmN1XaG.png\&w=2178\&h=1140\&dpl=6ae471fc143c9a4c27550463) Click on **New Role**. ![Create new role.](/.netlify/images?url=_astro%2F25.CbpLBmIr.png\&w=1440\&h=560\&dpl=6ae471fc143c9a4c27550463) Enter the **Role name**(this will be the name of the group). Select the recently created SCIM application and click Save. ![Add role name and assign it to SCIM application.](/.netlify/images?url=_astro%2F26.DhsBSxjv.png\&w=1440\&h=420\&dpl=6ae471fc143c9a4c27550463) Now select the created Role. Click the **Users** tab for the role. Search for any users you’d like to assign to that role, click on **Check** and then click on **Add To Role**. Click on **Save**. ![Add users to the new role.](/.netlify/images?url=_astro%2F27.sI1NfbrC.png\&w=1440\&h=500\&dpl=6ae471fc143c9a4c27550463) Navigate to **Applications** from the top bar and then click on the recently created application. ![Navigate to created SCIM application.](/.netlify/images?url=_astro%2F28.CX0Puxad.png\&w=2428\&h=999\&dpl=6ae471fc143c9a4c27550463) Go to the **Parameters** tab from the left navigation and click on the **Groups** row. ![Navigate to parameters tab and then select groups row.](/.netlify/images?url=_astro%2F29.DzoHZgj4.png\&w=3024\&h=1210\&dpl=6ae471fc143c9a4c27550463) Once the modal opens up, check **Include in User Provisioning** and then click on **Save**. ![Set user provisioning option.](/.netlify/images?url=_astro%2F30.CBieA8pg.png\&w=3024\&h=1250\&dpl=6ae471fc143c9a4c27550463) Navigate to **Rules** tab from left navigation and click on **Add Rule**. ![Create a new rule.](/.netlify/images?url=_astro%2F31.DWKIZziQ.png\&w=1972\&h=1002\&dpl=6ae471fc143c9a4c27550463) Give a suitable name to the rule (e.g., Assign Group to SCIM app) and set the action to **Set Groups in Hero SaaS App** for each **role** with any value. Then click **Save**. ![Configuring a new mapping for group assignment in the Hero SaaS App using OneLogin.](/.netlify/images?url=_astro%2F32.DUmjGFAi.png\&w=3024\&h=1624\&dpl=6ae471fc143c9a4c27550463) Navigate to **Users** tab from the left nav bar. You can see new users(belonging to the above created role) populated on the screen. For each of such user, click on **Pending**. ![Users from the recently created role are listed here.](/.netlify/images?url=_astro%2F33.DvuyWyfR.png\&w=1440\&h=558\&dpl=6ae471fc143c9a4c27550463) Once the modal opens up, click on **Approve**. The user belonging to the role will be provisioned to the application. ![Approve user provisioning to the application.](/.netlify/images?url=_astro%2F34.Dgh-289E.png\&w=2544\&h=1124\&dpl=6ae471fc143c9a4c27550463) 5. ## Group based role assignment [Section titled “Group based role assignment”](#group-based-role-assignment) Now on the **SCIM configuration portal**, configure appropriate group to role mapping to automatically assign roles to users in the application based on their group membership in OneLogin. Then click on **Save**. ![Assigning roles to user based on group membership.](/.netlify/images?url=_astro%2F35.Bk1mm_nL.png\&w=2420\&h=1284\&dpl=6ae471fc143c9a4c27550463) 6. ## Verify successful connection [Section titled “Verify successful connection”](#verify-successful-connection) After completing these steps, verify that the users and groups are successfully synced by visiting **Users** and **Groups** tab in the **SCIM configuration portal**. ![Verificy SCIM integration.](/.netlify/images?url=_astro%2F36.DiSITiGf.png\&w=2254\&h=1440\&dpl=6ae471fc143c9a4c27550463) --- # DOCUMENT BOUNDARY --- # PingIdentity Directory > Learn how to sync your PingIdentity Directory with your application for automated user provisioning and management using SCIM This guide helps administrators sync their PingIdentity directory with an application they want to onboard to their organization. Integrating your application with PingIdentity automates user management tasks and ensures access rights stay up-to-date. Setting up the integration involves two key components: 1. **Endpoint**: This is the URL where PingIdentity sends requests to the application you are onboarding. It acts as a communication point between PingIdentity and your application. 2. **Bearer Token**: This token is used by PingIdentity to authenticate its requests to the endpoint. It ensures that the requests are secure and authorized. By setting up these components, you enable seamless synchronization between your application and the PingIdentity directory. 1. ## Generate SCIM credentials [Section titled “Generate SCIM credentials”](#generate-scim-credentials) Open the Admin Portal from the application being onboarded and navigate to the **SCIM Provisioning** tab. Choose **PingIdentity** as your Directory Provider and click **Configure**. The Admin Portal automatically generates and displays an **Endpoint URL** and a **Bearer token**. Copy these values as you will need them to configure PingIdentity. ![Endpoint URL and Bearer token generated for the organization](/.netlify/images?url=_astro%2F1-generate-creds.DzPLW3KP.png\&w=2570\&h=1612\&dpl=6ae471fc143c9a4c27550463) Note If the “SCIM Provisioning” tab is not visible, contact the app owner to enable it for your organization. 2. ## Navigate to PingIdentity Provisioning [Section titled “Navigate to PingIdentity Provisioning”](#navigate-to-pingidentity-provisioning) Log in to your PingIdentity admin console (typically at `console.pingone.com`). Navigate to the **Integrations** dropdown in the main menu and select **Provisioning**. ![PingIdentity console showing Integrations > Provisioning selection](/.netlify/images?url=_astro%2F2-integrations-section.C-LvuCdG.png\&w=3014\&h=2078\&dpl=6ae471fc143c9a4c27550463) 3. ## Create a new connection [Section titled “Create a new connection”](#create-a-new-connection) Click the **+ (plus)** icon at the top of the dashboard and select **New Connection**. ![Clicking the + icon to create a new connection in PingIdentity](/.netlify/images?url=_astro%2F3-new-connection.Dz00Bmwv.png\&w=3014\&h=2078\&dpl=6ae471fc143c9a4c27550463) 4. ## Select SCIM Outbound connector [Section titled “Select SCIM Outbound connector”](#select-scim-outbound-connector) In the modal that appears: 1. **Select Identity Store**: Click **Select** to choose an identity store. ![Select Identity Store modal](/.netlify/images?url=_astro%2Fselect-identity-store.Bo7qiTog.png\&w=2486\&h=910\&dpl=6ae471fc143c9a4c27550463) 2. **Choose SCIM Outbound**: From the catalog, select **SCIM Outbound**. ![SCIM Outbound connector in catalog](/.netlify/images?url=_astro%2Fscim-outbound-catalog.Dx7PuNU2.png\&w=2484\&h=1806\&dpl=6ae471fc143c9a4c27550463) 3. **Name and Description**: Provide a name for the application you are onboarding (e.g., “Hero SaaS”) and add an optional description. Click **Next**. ![Name and Description fields for connection](/.netlify/images?url=_astro%2Fname-description.Nbci6Ddk.png\&w=2528\&h=1826\&dpl=6ae471fc143c9a4c27550463) 5. ## Configure connection settings [Section titled “Configure connection settings”](#configure-connection-settings) In the connection settings screen: * **SCIM Endpoint URL**: Paste the **Endpoint URL** from the Admin Portal * **Authentication Method**: Select **OAuth 2 Bearer Token** * **Bearer Token**: Paste the **Bearer Token** from the Admin Portal * Click **Test Connection** to verify the connection works correctly ![Connection configuration with SCIM endpoint and bearer token](/.netlify/images?url=_astro%2Fconfig-setup.DQT7YDr0.png\&w=2966\&h=1760\&dpl=6ae471fc143c9a4c27550463) After successful testing, click **Next** to proceed. 6. ## Configure preferences and save [Section titled “Configure preferences and save”](#configure-preferences-and-save) Leave all preferences at their default settings and click **Save** to finish creating the connection. ![Configure preferences with default settings](/.netlify/images?url=_astro%2Fconfigure-pref.BxmIQKHX.png\&w=3014\&h=2078\&dpl=6ae471fc143c9a4c27550463) 7. ## Configure provisioning rules [Section titled “Configure provisioning rules”](#configure-provisioning-rules) After creating the connection, you must define the rules for data synchronization. Click the **+ (plus)** icon again and select **New Rule** from the dropdown menu. ![Creating a new provisioning rule](/.netlify/images?url=_astro%2Fcreate-rule.BFLmbeNS.png\&w=2492\&h=1280\&dpl=6ae471fc143c9a4c27550463) In the rule configuration modal, set the following: * **Source**: Select **PingOne** * **Connection**: Choose the connection you created in the previous step ![Rule configuration with source, connection, and name](/.netlify/images?url=_astro%2Fsetup-rule.uLfcWCub.png\&w=3014\&h=2078\&dpl=6ae471fc143c9a4c27550463) In the next step provide a meaningful name for the rule (for example, the application name), and click **Next**. In the next step, define the conditions that determine which users should be provisioned to the SCIM application. Configure appropriate conditions based on your requirements (such as group membership or user attributes). To enable group provisioning, ensure that the correct group is selected or included in the rule. This allows the specified group and its users to be pushed to the target SCIM application. Finally, review the configuration and save the rule. ![Configuring provisioning rule conditions in PingIdentity](/.netlify/images?url=_astro%2Fadd-condition.BQ3mxmA7.png\&w=1568\&h=1892\&dpl=6ae471fc143c9a4c27550463) 8. ## Verify the integration [Section titled “Verify the integration”](#verify-the-integration) With the setup complete, verify that users and groups are synchronizing correctly: 1. **Sync a Group**: In PingIdentity, create or select a group. This group should appear in the Admin Portal under **SCIM Provisioning** almost immediately. 2. **Sync User Data**: Add users to that group. Their profile data will be sent to your application and synchronized in real-time. ![Synced users and groups in Admin Portal](/.netlify/images?url=_astro%2Fsynced-users.B6jwN0K2.png\&w=3095\&h=1799\&dpl=6ae471fc143c9a4c27550463) Confirm the synchronization by visiting the Users/Groups tab in the Admin Portal. --- # DOCUMENT BOUNDARY --- # Social connections > Learn how to integrate social login providers with Scalekit to enable secure social authentication for your users. Scalekit makes it easy to add social login options to your application. This allows your users to sign in using their existing accounts from popular platforms like Google, GitHub, and more. ### Google Enable users to sign in with their Google accounts using OAuth 2.0 [Know more →](/guides/integrations/social-connections/google) ### GitHub Allow users to authenticate using their GitHub credentials [Know more →](/guides/integrations/social-connections/github) ### Microsoft Integrate Microsoft accounts for seamless user authentication [Know more →](/guides/integrations/social-connections/microsoft) ### GitLab Enable GitLab-based authentication for your application [Know more →](/guides/integrations/social-connections/gitlab) ### LinkedIn Let users sign in with their LinkedIn accounts using OAuth 2.0 [Know more →](/guides/integrations/social-connections/linkedin) ### Salesforce Enable Salesforce-based authentication for your application [Know more →](/guides/integrations/social-connections/salesforce) ### Facebook Let users sign in with their Facebook accounts using OAuth 2.0 [Know more →](/guides/integrations/social-connections/facebook/) --- # DOCUMENT BOUNDARY --- # Facebook as your sign in option > Add Sign in with Facebook to your app: create a Meta app, register the Scalekit redirect URI and connect the app to Scalekit for social login. This guide is for developers who want users to sign in to their app with a Facebook account. By the end, you’ll have a Facebook connection in Scalekit that uses your own Meta app, and a successful test sign-in. ![A diagram showing "Your Application" connecting to "Scalekit" via OpenID Connect, which links to Facebook using OAuth 2.0.](/.netlify/images?url=_astro%2Fposter-scalekit-social.BTpvXQK7.png\&w=5776\&h=1924\&dpl=6ae471fc143c9a4c27550463) Before you start, you need: * A Scalekit environment * A [Meta for Developers](https://developers.facebook.com/) account ## Add a Facebook connection in Scalekit [Section titled “Add a Facebook connection in Scalekit”](#add-a-facebook-connection-in-scalekit) 1. Open social login settings In the Scalekit dashboard, go to **Authentication** > **Auth Methods** > **Social Login**. 2. Open the Facebook connection Next to **Facebook**, click **Edit**. ![Scalekit dashboard Social Login list under Authentication, Auth Methods, with Facebook below Google, Microsoft, GitHub, LinkedIn, GitLab and Salesforce.](/.netlify/images?url=_astro%2Ffacebook-social-login.DzZij01v.png\&w=2000\&h=738\&dpl=6ae471fc143c9a4c27550463) 3. Copy the redirect URI The **OAuth Configuration** page shows a **Redirect URI**. Copy it; you register it with Meta in the next section. Keep this page open to paste the **Client ID** and **Client Secret** from your Meta app. The **Scopes** are set to `email` and `public_profile`. ![Facebook OAuth Configuration page in Scalekit, showing the Redirect URI, empty Client ID and Client Secret fields, and the email and public_profile scopes.](/.netlify/images?url=_astro%2Ffacebook-oauth-config.BacHx8zU.png\&w=2000\&h=788\&dpl=6ae471fc143c9a4c27550463) ## Create a Meta app [Section titled “Create a Meta app”](#create-a-meta-app) 1. Create the app In [Meta for Developers](https://developers.facebook.com/apps/), click **Create app**. When asked for a use case, choose **Authenticate and request data from users with Facebook Login**. 2. Add the email permission In the app dashboard, go to **Use cases** and click **Customize** on the Facebook Login use case. On **Permissions and features**, click **Add** next to `email`. Scalekit requests `email` and `public_profile`, and it needs the email address to sign the user in. Check that `email` and `public_profile` both show **Ready for testing**. Leave the other permissions as they are. ![Meta app use case Permissions and features page, with the email and public_profile permissions showing Ready for testing.](/.netlify/images?url=_astro%2Ffacebook-permissions.CGvfXbQQ.png\&w=2000\&h=425\&dpl=6ae471fc143c9a4c27550463) 3. Register the redirect URI In the same use case, open **Settings**. Paste the Scalekit redirect URI into **Valid OAuth Redirect URIs**, make sure **Client OAuth login** and **Web OAuth login** are on, and save. 4. Copy the app credentials In **App settings** > **Basic**, copy the **App ID**. Click **Show** next to **App secret** and copy the secret. ![Meta app settings Basic page, showing the App ID field and the App secret field with its Show button.](/.netlify/images?url=_astro%2Ffacebook-app-credentials.1wpHPziC.png\&w=1350\&h=82\&dpl=6ae471fc143c9a4c27550463) ## Connect the Meta app to Scalekit [Section titled “Connect the Meta app to Scalekit”](#connect-the-meta-app-to-scalekit) 1. Paste the App ID into **Client ID** and the App secret into **Client Secret** 2. Click **Save changes** 3. Click **Enable** ## Test the connection [Section titled “Test the connection”](#test-the-connection) 1. Click **Test Connection** in Scalekit 2. Sign in with a Facebook account that has a role on your Meta app (admin, developer or tester), and approve the consent screen 3. Check the summary table: it lists the attributes Scalekit received from Facebook, such as the user’s ID, name and email ![Successful Facebook test connection in Scalekit, listing the attributes received from Facebook: sub, email, name, first_name, id and last_name.](/.netlify/images?url=_astro%2Ffacebook-test-connection.DEmVTOIB.png\&w=840\&h=545\&dpl=6ae471fc143c9a4c27550463) ## Go live [Section titled “Go live”](#go-live) While your Meta app is in development mode, only people with a role on the app can sign in. To let any Facebook user sign in, complete the steps Meta lists on the app dashboard (such as adding a privacy policy URL) and switch the app to **Live**. ## How Facebook sign-in behaves [Section titled “How Facebook sign-in behaves”](#how-facebook-sign-in-behaves) * **Email address.** Scalekit signs the user in with the email address on their Facebook account. If the account has no email address, or the user removes the email permission on the consent screen, the sign-in fails with a missing email error. Ask the user to sign in another way, or to add an email address to their Facebook account and try again. * **Email verification.** Facebook doesn’t tell Scalekit whether the email address is verified. Scalekit asks the user to verify their email the first time they sign in with Facebook. * **Profile picture.** Scalekit sends the user’s Facebook profile picture. Users who haven’t set one get no picture rather than Facebook’s default placeholder. * **App secret proof.** Scalekit signs its Graph API calls with an `appsecret_proof`, so you can turn on **Require App Secret** in your Meta app’s advanced settings. Tip To send users straight to Facebook instead of the hosted login page, pass `provider=facebook` in the [authorization URL](/sso/guides/authorization-url/). ## Next steps [Section titled “Next steps”](#next-steps) Add the connection to your login flow: see [Social logins](/authenticate/auth-methods/social-logins/). --- # DOCUMENT BOUNDARY --- # GitHub as your sign in option > Learn how to integrate GitHub Sign-In with Scalekit, enabling secure social authentication for your users with step-by-step OAuth configuration instructions. Scalekit enables apps to easily let users sign in using GitHub as their social connector. This guide walks you through the process of setting up the connection between Scalekit and GitHub, and using the Scalekit SDK to add “Sign in with GitHub” to your application. ![A diagram showing "Your Application" connecting to "Scalekit" via OpenID Connect, which links to GitHub using OAuth 2.0.](/.netlify/images?url=_astro%2Fgithub-1.CzWW-w4F.png\&w=2512\&h=1420\&dpl=6ae471fc143c9a4c27550463) By the end of this guide, you will be able to: 1. Set up an OAuth 2.0 connection between Scalekit and GitHub 2. Scalekit SDK to add “Sign in with GitHub” to your application ## Connect GitHub with Scalekit [Section titled “Connect GitHub with Scalekit”](#connect-github-with-scalekit) **Navigate to social login settings** Open your Scalekit dashboard and navigate to Social Login under the Authentication section. ![Scalekit dashboard showcasing social login setup with various platform integration options.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) **Add a new GitHub connection** Click the ”+ Add Connection” button and select GitHub from the list of available options. ![Add social login connections: Google, Microsoft, GitHub, Github, Salesforce.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) Add social login connections: GitHub ## Configure OAuth settings [Section titled “Configure OAuth settings”](#configure-oauth-settings) The OAuth Configuration details page helps you set up the connection: * Note the **Redirect URI** provided for your app. You’ll use this URL to register with GitHub. * **Client ID** and **Client Secret** are generated by GitHub when you register an OAuth App. They enable Scalekit to authenticate your app and establish trust with GitHub. ![Configure OAuth settings](/.netlify/images?url=_astro%2Fgithub-1.CzWW-w4F.png\&w=2512\&h=1420\&dpl=6ae471fc143c9a4c27550463) GitHub OAuth configuration in Scalekit, showing redirect URI, client credentials, and scopes for social login setup. **Set up GitHub OAuth 2.0** GitHub lets you set up OAuth through the Microsoft Identity Platform. [Follow GitHub’s instructions to set up OAuth 2.0](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app). 1. Navigate to GitHub’s OAuth Apps settings page 2. Click “New OAuth App” to create a new application 3. Fill in the application details: * Application name: Your app’s name * Homepage URL: Your application’s homepage * Application description: Brief description of your app * Authorization callback URL: Use the Redirect URI from Scalekit 4. Click “Register application” to create the OAuth App 5. Copy the generated Client ID and Client Secret 6. Paste these credentials into the Scalekit Dashboard 7. Click “Save Changes” in Scalekit to complete the setup ![GitHub OAuth configuration for social login, showing redirect URI, client ID, and scopes for authentication.](/.netlify/images?url=_astro%2Fgithub-1.CzWW-w4F.png\&w=2512\&h=1420\&dpl=6ae471fc143c9a4c27550463) ## Test the connection [Section titled “Test the connection”](#test-the-connection) Click the “Test Connection” button in Scalekit. You will be redirected to the GitHub Consent screen to authorize access. A summary table will show the information that will be sent to your app. ![Test connection success](/.netlify/images?url=_astro%2Fgithub-2.RCFzSrUN.png\&w=3602\&h=3310\&dpl=6ae471fc143c9a4c27550463) --- # DOCUMENT BOUNDARY --- # GitLab as your sign in option > Learn how to integrate GitLab Sign-In with Scalekit, enabling secure social authentication for your users with step-by-step OAuth configuration instructions. Scalekit enables apps to easily let users sign in using GitLab as their social connector. This guide walks you through the process of setting up the connection between Scalekit and GitLab, and using the Scalekit SDK to add “Sign in with GitLab” to your application. ![A diagram showing "Your Application" connecting to "Scalekit" via OpenID Connect, which links to GitLab using OAuth 2.0.](/.netlify/images?url=_astro%2Fposter-scalekit-social.BTpvXQK7.png\&w=5776\&h=1924\&dpl=6ae471fc143c9a4c27550463) By the end of this guide, you will be able to: 1. Set up an OAuth 2.0 connection between Scalekit and GitLab 2. Scalekit SDK to add “Sign in with GitLab” to your application ## Set up GitLab connection [Section titled “Set up GitLab connection”](#set-up-gitlab-connection) ### Access social login settings [Section titled “Access social login settings”](#access-social-login-settings) Open your Scalekit dashboard and navigate to Social Login under the Authentication section. ![Scalekit dashboard showcasing social login setup with various platform integration options.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) ### Add GitLab connection [Section titled “Add GitLab connection”](#add-gitlab-connection) Click the ”+ Add Connection” button and select GitLab from the list of available options. ![Add social login connections: Google, Microsoft, GitHub, GitLab, Salesforce.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) ## Configure OAuth settings [Section titled “Configure OAuth settings”](#configure-oauth-settings) The OAuth Configuration details page helps you set up the connection: * Note the **Redirect URI** provided for your app. You’ll use this URL to register with GitLab. * **Client ID** and **Client Secret** are generated by GitLab when you register an OAuth App. They enable Scalekit to authenticate your app and establish trust with GitLab. ![GitLab OAuth configuration for social login, showing redirect URI, client ID, and scopes for authentication.](/.netlify/images?url=_astro%2Fgitlab-1.yH1eNycx.png\&w=2894\&h=1468\&dpl=6ae471fc143c9a4c27550463) ### Set up GitLab OAuth 2.0 [Section titled “Set up GitLab OAuth 2.0”](#set-up-gitlab-oauth-20) GitLab lets you set up OAuth through the Microsoft Identity Platform. [Follow GitLab’s instructions to set up OAuth 2.0](https://docs.gitlab.co.jp/ee/integration/oauth_provider.html). 1. Navigate to GitLab’s OAuth Applications settings page 2. Click “New Application” to create a new OAuth application 3. Fill in the application details: * Name: Your app’s name * Redirect URI: Use the Redirect URI from Scalekit * Scopes: Select the required scopes for your application 4. Click “Save application” to create the OAuth App 5. Copy the generated Application ID and Secret 6. Paste these credentials into the Scalekit Dashboard 7. Click “Save Changes” in Scalekit to complete the setup ![GitLab OAuth configuration for social login, showing redirect URI, client ID, and scopes for authentication.](/.netlify/images?url=_astro%2Fgitlab-2.Co5P6Jrn.png\&w=3544\&h=3362\&dpl=6ae471fc143c9a4c27550463) ## Test the connection [Section titled “Test the connection”](#test-the-connection) Click the “Test Connection” button in Scalekit. You will be redirected to the GitLab Consent screen to authorize access. A summary table will show the information that will be sent to your app. ![Test connection success](/.netlify/images?url=_astro%2F5-successful-test-connection.2vG1rYWi.png\&w=2922\&h=1812\&dpl=6ae471fc143c9a4c27550463) --- # DOCUMENT BOUNDARY --- # Google as your sign in option > Learn how to integrate Google Sign-In with Scalekit, enabling secure social authentication for your users with step-by-step OAuth configuration instructions. Scalekit enables apps to easily let users sign in using Google as their social connector. This guide walks you through the process of setting up the connection between Scalekit and Google, and using the Scalekit SDK to add “Sign in with Google” to your application. By the end of this guide, you will be able to: 1. Test Google sign-in without setting up Google OAuth credentials (dev only) 2. Set up an OAuth 2.0 connection between Scalekit and Google 3. Implement ‘Sign in with Google’ in your application using the Scalekit SDK ## Set up Google connection [Section titled “Set up Google connection”](#set-up-google-connection) ### Access social login settings [Section titled “Access social login settings”](#access-social-login-settings) Open your Scalekit dashboard and navigate to Social Login under the Authentication section. ![Scalekit dashboard showcasing social login setup with various platform integration options.](/.netlify/images?url=_astro%2F1-navigate-to-social-logins.0QTBAQVD.png\&w=2622\&h=908\&dpl=6ae471fc143c9a4c27550463) ### Add Google connection [Section titled “Add Google connection”](#add-google-connection) Click the ”+ Add Connection” button and select Google from the list of available options. ![Add social login connections: Google, Microsoft, GitHub, GitLab, Salesforce.](/.netlify/images?url=_astro%2F2-list-social-logins.DVSLNcJ6.png\&w=2554\&h=914\&dpl=6ae471fc143c9a4c27550463) ## Test with Scalekit credentials [Section titled “Test with Scalekit credentials”](#test-with-scalekit-credentials) For faster development and testing, Scalekit provides pre-configured Google OAuth credentials, allowing you to test the authentication flow without setting up your own Google OAuth client. This is particularly useful when you want to quickly validate Google sign-in functionality in your app without dealing with OAuth setup. It also helps if you’re still in the early stages of development and don’t have Google credentials yet, or if you need to test the behavior before setting up a production-ready connection. Under OAuth Configuration, select **Use Scalekit credentials** and **save** the changes. Once done, you can now directly test the setup by clicking **Test Connection**. ![Use Scalekit credentials to test connection](/.netlify/images?url=_astro%2F2-1-test-scalekit-credentials.CN9EcV37.png\&w=2940\&h=1656\&dpl=6ae471fc143c9a4c27550463) ## Set up with your own credentials [Section titled “Set up with your own credentials”](#set-up-with-your-own-credentials) ### Configure OAuth settings [Section titled “Configure OAuth settings”](#configure-oauth-settings) The OAuth Configuration details page helps you set up the connection: * Note the **Redirect URI** provided for your app. You’ll use this URL to register with Google. * **Client ID** and **Client Secret** are generated by Google when you register an OAuth App. They enable Scalekit to authenticate your app and establish trust with Google. ### Get Google OAuth client credentials [Section titled “Get Google OAuth client credentials”](#get-google-oauth-client-credentials) 1. Open the [Google Cloud Platform Console](https://console.cloud.google.com/). From the projects list, select an existing project or create a new one. 2. Navigate to the [Google Auth Platform’s overview page](https://console.cloud.google.com/auth/overview). * Click **Get Started** and provide details such as app information, audience, and contact information. * **Important**: Select **External** audience type. You must use External for social login because: * **Internal** only works for whitelisted Google Workspace accounts (your own employees) * **External** allows anyone with a Google account to sign in to your app * **Internal** cannot be used for public-facing authentication * Complete the process by clicking **Create**. 3. On the “Overview” page, click the **Create OAuth Client** button to start setting up your app’s OAuth client. 4. Choose the appropriate application type (e.g., web application) from the dropdown menu. 5. Copy the redirect URI from your Google Social Login configuration and paste it into the **Authorized Redirect URIs** field. The URI should follow this format (for development environment): `https://{your-subdomain}.scalekit.dev`. 6. **Save and retrieve credentials**: Click **Save** to finalize the setup. You will be redirected to a list of Google OAuth Clients. Select the newly created client and copy the **Client ID** and **Client Secret** from the additional information section. 7. **Enter credentials in social login configuration**: Paste the copied client credentials into their respective fields on your Google Social Login page. 8. Click **Test Connection** to simulate and verify the Google Sign-In flow. Google OAuth consent screen behavior Before using custom credentials in production, understand what users will see on Google’s consent screen: | Audience Type | Consent Screen Behavior | When To Use | | ------------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------- | | **Internal** | Shows your App Name and logo from Branding settings | Only for your own employees using whitelisted Google Workspace domains | | **External** | Shows `{env_name}.scalekit.dev` domain until Google verifies your app | For public users—anyone with a Google account can sign in | **Why you must use External for social login:** * **Internal** restricts access to pre-approved email domains you control. Public users with `@gmail.com` or other Google accounts cannot sign in. * **External** is required because social login is for anyone, not just your employees. * Until Google completes verification of your External app, users see `scalekit.dev` instead of your custom domain. After verification, your App Name and logo appear on the consent screen. **Note:** This is Google’s OAuth behavior—not Scalekit’s. The verification is separate from Scalekit’s domain verification for Enterprise SSO. For Google’s verification requirements and timeline, refer to [Google’s OAuth consent screen verification guide](https://support.google.com/cloud/answer/13463073). ![Google OAuth configuration in Scalekit, showing redirect URI, client credentials, and scopes for social login setup.](/.netlify/images?url=_astro%2F3-google-oauth-config.Bgp8TxoS.png\&w=2892\&h=1537\&dpl=6ae471fc143c9a4c27550463) * Use the Redirect URI from Scalekit as the Callback URL in Google’s setup * Copy the generated Client ID and Client Secret into the Scalekit Dashboard After completing the setup, click “Save Changes” in Scalekit for the changes to take effect. ![Google OAuth configuration for social login, showing redirect URI, client ID, and scopes for authentication.](/.netlify/images?url=_astro%2F4-after-oauth-config.Cxv2tNHN.png\&w=2818\&h=1594\&dpl=6ae471fc143c9a4c27550463) ### Configure login prompt behavior [Section titled “Configure login prompt behavior”](#configure-login-prompt-behavior) Scalekit offers flexibility to control how and when users are prompted for reauthentication, consent, or account selection. Below are the available options for customizing user sign-in behavior: * **Auto sign-in (default)**: Automatically completes the login process without showing any confirmation prompts. This is ideal for single Google account users who are already logged in and have previously provided consent. * **Consent**: The authorization server prompts the user for consent before returning information to the client. * **Select account**: The authorization server prompts the user to select a user account. This allows a user who has multiple accounts at the authorization server to select amongst the multiple accounts that they may have current sessions for. * **None**: The authorization server does not display any authentication or user consent screens; it will return an error if the user is not already authenticated and has not pre-configured consent for the requested scopes. You can use none to check for existing authentication and/or consent. ## Verify the connection [Section titled “Verify the connection”](#verify-the-connection) Click the “Test Connection” button in Scalekit. You will be redirected to the Google Consent screen to authorize access. A summary table will show the information that will be sent to your app. ![Test connection success](/.netlify/images?url=_astro%2F5-successful-test-connection.2vG1rYWi.png\&w=2922\&h=1812\&dpl=6ae471fc143c9a4c27550463) --- # DOCUMENT BOUNDARY --- # LinkedIn as your sign in option > Learn how to integrate LinkedIn Sign-In with Scalekit, enabling secure social authentication for your users with step-by-step OAuth configuration instructions. Scalekit enables apps to easily let users sign in using LinkedIn as their social connector. This guide walks you through the process of setting up the connection between Scalekit and LinkedIn, and using the Scalekit SDK to add “Sign in with LinkedIn” to your application. ![A diagram showing "Your Application" connecting to "Scalekit" via OpenID Connect, which links to LinkedIn using OAuth 2.0.](/.netlify/images?url=_astro%2Fposter-scalekit-social.BTpvXQK7.png\&w=5776\&h=1924\&dpl=6ae471fc143c9a4c27550463) By the end of this guide, you will be able to: 1. Set up an OAuth 2.0 connection between Scalekit and LinkedIn 2. Use the Scalekit SDK to add “Sign in with LinkedIn” to your application ## Connect LinkedIn with Scalekit [Section titled “Connect LinkedIn with Scalekit”](#connect-linkedin-with-scalekit) 1. Navigate to social login settings Open your Scalekit dashboard and navigate to Social Login under the Authentication section. ![Scalekit dashboard showcasing social login setup with various platform integration options.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) 2. Add a new LinkedIn connection Click the ”+ Add Connection” button and select LinkedIn from the list of available options. ## Configure OAuth settings [Section titled “Configure OAuth settings”](#configure-oauth-settings) The OAuth Configuration details page helps you set up the connection: * Note the **Redirect URI** provided for your app. You’ll use this URL to register with LinkedIn. * **Client ID** and **Client Secret** are generated by LinkedIn when you register an OAuth App. They enable Scalekit to authenticate your app and establish trust with LinkedIn. ## Set up LinkedIn OAuth 2.0 [Section titled “Set up LinkedIn OAuth 2.0”](#set-up-linkedin-oauth-20) LinkedIn lets you set up OAuth through the LinkedIn Developer Platform. [Follow LinkedIn’s instructions to set up OAuth 2.0](https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?tabs=HTTPS1). 1. Use the Redirect URI from Scalekit as the Redirect URI in LinkedIn’s setup 2. Copy the generated Client ID and Client Secret into the Scalekit Dashboard 3. Click “Save Changes” in Scalekit for the changes to take effect ![LinkedIn OAuth configuration for social login, showing redirect URI, client ID, and scopes for authentication.](/.netlify/images?url=_astro%2Flinkedin-1.xr0pxyVQ.png\&w=2770\&h=1476\&dpl=6ae471fc143c9a4c27550463) ## Test the connection [Section titled “Test the connection”](#test-the-connection) 1. Click the “Test Connection” button in Scalekit 2. You will be redirected to the LinkedIn Consent screen to authorize access 3. A summary table will show the information that will be sent to your app ![Test connection success](/.netlify/images?url=_astro%2F5-successful-test-connection.2vG1rYWi.png\&w=2922\&h=1812\&dpl=6ae471fc143c9a4c27550463) --- # DOCUMENT BOUNDARY --- # Microsoft as your sign in option > Learn how to integrate Microsoft Sign-In with Scalekit, enabling secure social authentication for your users with step-by-step OAuth configuration instructions. Scalekit enables apps to easily let users sign in using Microsoft as their social connector. This guide walks you through the process of setting up the connection between Scalekit and Microsoft, and using the Scalekit SDK to add “Sign in with Microsoft” to your application. ![A diagram showing "Your Application" connecting to "Scalekit" via OpenID Connect, which links to Microsoft using OAuth 2.0.](/.netlify/images?url=_astro%2Fposter-scalekit-social.BTpvXQK7.png\&w=5776\&h=1924\&dpl=6ae471fc143c9a4c27550463) By the end of this guide, you will be able to: 1. Set up an OAuth 2.0 connection between Scalekit and Microsoft 2. Use the Scalekit SDK to add “Sign in with Microsoft” to your application ## Connect Microsoft with Scalekit [Section titled “Connect Microsoft with Scalekit”](#connect-microsoft-with-scalekit) 1. Navigate to social login settings Open your Scalekit dashboard and navigate to Social Login under the Authentication section. ![Scalekit dashboard showcasing social login setup with various platform integration options.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) 2. Add a new Microsoft connection Click the ”+ Add Connection” button and select Microsoft from the list of available options. ![Add social login connections: Google, Microsoft, GitHub, GitLab, Salesforce.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) Add social login connections: Microsoft ## Configure OAuth settings [Section titled “Configure OAuth settings”](#configure-oauth-settings) The OAuth Configuration details page helps you set up the connection: * Note the **Redirect URI** provided for your app. You’ll use this URL to register with Microsoft. * **Client ID** and **Client Secret** are generated by Microsoft when you register an OAuth App. They enable Scalekit to authenticate your app and establish trust with Microsoft. ![Microsoft OAuth configuration in Scalekit, showing redirect URI, client credentials, and scopes for social login setup.](/.netlify/images?url=_astro%2Fmicrosoft-1.7KcDT0o6.png\&w=2766\&h=1470\&dpl=6ae471fc143c9a4c27550463) ## Set up Microsoft OAuth 2.0 [Section titled “Set up Microsoft OAuth 2.0”](#set-up-microsoft-oauth-20) Microsoft lets you set up OAuth through the Microsoft Identity Platform. [Follow Microsoft’s instructions to set up OAuth 2.0](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app). 1. Use the Redirect URI from Scalekit as the [Redirect URI in Microsoft’s setup](https://learn.microsoft.com/en-us/entra/identity-platform/quickstart-register-app?tabs=certificate#add-a-redirect-uri) 2. Copy the generated Client ID and Client Secret into the Scalekit Dashboard 3. Click “Save Changes” in Scalekit for the changes to take effect ![Microsoft OAuth configuration for social login, showing redirect URI, client ID, and scopes for authentication.](/.netlify/images?url=_astro%2Fmicrosoft-2.C41XslL9.png\&w=3116\&h=2388\&dpl=6ae471fc143c9a4c27550463) ## Choose the user experience for login prompt [Section titled “Choose the user experience for login prompt”](#choose-the-user-experience-for-login-prompt) Scalekit offers flexibility to control how and when users are prompted for reauthentication, consent, or account selection. Below are the available options for customizing user sign-in behavior: * *Auto Sign-in (default)*: Automatically completes the login process without showing any confirmation prompts. This is ideal for single account users who are already logged in and have previously provided consent. * *Consent*: The authorization server triggers a consent screen after sign-in, asking the user to grant permissions to the app. * *Select Account*: The authorization server prompts the user to select a user account. This allows a user who has multiple accounts at the authorization server to select amongst the multiple accounts that they may have current sessions for. * *Login*: Forces the user to re-enter their credentials and log in, even if a valid session already exists. * *None*: Performs a background authentication check without displaying any screens. If the user is not authenticated or hasn’t provided consent, an error will be returned. ## Test the connection [Section titled “Test the connection”](#test-the-connection) 1. Click the “Test Connection” button in Scalekit 2. You will be redirected to the Microsoft Consent screen to authorize access 3. A summary table will show the information that will be sent to your app ![Test connection success](/.netlify/images?url=_astro%2F5-successful-test-connection.2vG1rYWi.png\&w=2922\&h=1812\&dpl=6ae471fc143c9a4c27550463) Test connection success, showing the consent screen and summary table. --- # DOCUMENT BOUNDARY --- # Salesforce as your sign in option > Learn how to integrate Salesforce Sign-In with Scalekit, enabling secure social authentication for your users with step-by-step OAuth configuration instructions. Scalekit enables apps to easily let users sign in using Salesforce as their social connector. This guide walks you through the process of setting up the connection between Scalekit and Salesforce, and using the Scalekit SDK to add “Sign in with Salesforce” to your application. ![A diagram showing "Your Application" connecting to "Scalekit" via OpenID Connect, which links to Salesforce using OAuth 2.0.](/.netlify/images?url=_astro%2Fposter-scalekit-social.BTpvXQK7.png\&w=5776\&h=1924\&dpl=6ae471fc143c9a4c27550463) By the end of this guide, you will be able to: 1. Set up an OAuth 2.0 connection between Scalekit and Salesforce 2. Implement “Sign in with Salesforce” in your application using the Scalekit SDK ## Set up Salesforce connection [Section titled “Set up Salesforce connection”](#set-up-salesforce-connection) ### Access social login settings [Section titled “Access social login settings”](#access-social-login-settings) Open your Scalekit dashboard and navigate to Social Login under the Authentication section. ![Scalekit dashboard showcasing social login setup with various platform integration options.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) ### Add Salesforce connection [Section titled “Add Salesforce connection”](#add-salesforce-connection) Click the ”+ Add Connection” button and select Salesforce from the list of available options. ![Add social login connections: Google, Microsoft, GitHub, Salesforce.](/.netlify/images?url=_astro%2F2-list-social-logins.Xr8o_GLe.png\&w=3024\&h=1612\&dpl=6ae471fc143c9a4c27550463) Add social login connections: Salesforce ## Configure OAuth settings [Section titled “Configure OAuth settings”](#configure-oauth-settings) The OAuth Configuration details page helps you set up the connection: * Note the **Redirect URI** provided for your app. You’ll use this URL to register with Salesforce. * **Client ID** and **Client Secret** are generated by Salesforce when you register an OAuth App. They enable Scalekit to authenticate your app and establish trust with Salesforce. ![Salesforce OAuth configuration in Scalekit, showing redirect URI, client credentials, and scopes for social login setup.](/.netlify/images?url=_astro%2Fsalesforce-1.BEBC3a71.png\&w=3368\&h=1478\&dpl=6ae471fc143c9a4c27550463) ### Set up Salesforce OAuth 2.0 [Section titled “Set up Salesforce OAuth 2.0”](#set-up-salesforce-oauth-20) Salesforce lets you set up OAuth through the Microsoft Identity Platform. [Follow Salesforce’s instructions to set up OAuth 2.0](https://dub.sh/connected-app-create-salesforce) 1. Use the Redirect URI from Scalekit as the Redirect URI in Salesforce’s setup. The URI should follow this format: * Development: `https://{your-subdomain}.scalekit.dev` * Production: `https://{your-subdomain}.scalekit.com` 2. Copy the generated Client ID and Client Secret into the Scalekit Dashboard 3. Click “Save Changes” in Scalekit for the changes to take effect ## Test the connection [Section titled “Test the connection”](#test-the-connection) Click the “Test Connection” button in Scalekit. You will be redirected to the Salesforce Consent screen to authorize access. A summary table will show the information that will be sent to your app. ![Test connection success](/.netlify/images?url=_astro%2F5-successful-test-connection.2vG1rYWi.png\&w=2922\&h=1812\&dpl=6ae471fc143c9a4c27550463) --- # DOCUMENT BOUNDARY --- # Migrate to Full Stack Auth > Step-by-step guide to move user, organization, and auth flows from existing systems to Scalekit. Migrating authentication is a big job. **But moving to Scalekit pays dividends**: you off-load SSO integrations, SCIM provisioning, session handling, and more—so your team can focus on product. This guide walks you through a **safe, incremental migration** from any existing solution to **Scalekit’s full-stack auth platform**. This migration guide helps you: * Export user and organization data from your current system * Import data into Scalekit using APIs or SDKs * Update your application’s authentication flows * Test and deploy the new authentication system Need a hand? Our Solutions team has run dozens of successful migrations. [Contact us](/support/contact-us) and we’ll craft a smooth cut-over plan together. 1. ## Audit and export your data [Section titled “Audit and export your data”](#audit-and-export-your-data) Before you switch to Scalekit, create a comprehensive inventory of your existing setup and export your data: **Code audit:** * Sign-up and login flows * Session middleware and token validation * Role-based access control (RBAC) logic * Email verification flows * Logout and session termination **Data export:** * User records (emails, names, verification status) * Organization/tenant structure * Role assignments and permissions * Authentication provider configurations (if using SSO) **Backup plan:** * Export a sample JWT token or session cookie to understand your current format * Set up a feature flag to route traffic back to the old system if needed * Document your rollback procedure The minimal user schema looks like this: | **Field** | **Description** | **Status** | | ---------------- | -------------------------------------------- | ---------- | | `email` | Primary login identifier. | Required | | `first_name` | The user’s given name. | Optional | | `last_name` | The user’s family name. | Optional | | `email_verified` | Boolean flag. Treated as `false` if omitted. | Optional | 2. ## Import organizations and users [Section titled “Import organizations and users”](#import-organizations-and-users) Transform your exported data to match Scalekit’s format. The `external_id` field is crucial—it stores your original primary key, enabling seamless lookups between your system and Scalekit. * Node.js ```bash npm install @scalekit-sdk/node ``` * Python ```sh pip install scalekit-sdk-python ``` * Go ```sh go get -u github.com/scalekit-inc/scalekit-sdk-go ``` * Java ```groovy /* Gradle users - add the following to your dependencies in build file */ implementation "com.scalekit:scalekit-sdk-java:2.1.3" ``` ```xml com.scalekit scalekit-sdk-java 2.1.3 ``` **Create organizations first:** * cURL Create an organization ```bash curl "$SCALEKIT_ENVIRONMENT_URL/api/v1/organizations" \ --request POST \ --header 'Content-Type: application/json' \ --data '{ "display_name": "Megasoft Inc", "external_id": "org_123", "metadata": { "plan": "enterprise" } }' ``` * Node.js Create organizations ```javascript const organizations = [ { display_name: "Megasoft Inc", external_id: "org_123", metadata: { plan: "enterprise" } }, { display_name: "Acme Corp", external_id: "org_456", metadata: { plan: "starter" } } ]; for (const org of organizations) { const result = await scalekit.organization.createOrganization( org.display_name, { externalId: org.external_id, metadata: org.metadata } ); console.log(`Created organization: ${result.id}`); } ``` * Python Create organizations ```python 1 from scalekit.v1.organizations.organizations_pb2 import CreateOrganization 2 3 organizations = [ 4 {"display_name": "Megasoft Inc", "external_id": "org_123", "metadata": {"plan": "enterprise"}}, 5 {"display_name": "Acme Corp", "external_id": "org_456", "metadata": {"plan": "starter"}} 6 ] 7 8 for org in organizations: 9 result = scalekit_client.organization.create_organization( 10 CreateOrganization( 11 display_name=org["display_name"], 12 external_id=org["external_id"], 13 metadata=org["metadata"] 14 ) 15 ) 16 print(f"Created organization: {result.id}") ``` * Go Create organizations ```go 1 organizations := []struct { 2 DisplayName string 3 ExternalID string 4 Metadata map[string]interface{} 5 }{ 6 {"Megasoft Inc", "org_123", map[string]interface{}{"plan": "enterprise"}}, 7 {"Acme Corp", "org_456", map[string]interface{}{"plan": "starter"}}, 8 } 9 10 for _, org := range organizations { 11 result, err := scalekit.Organization.CreateOrganization( 12 ctx, 13 org.DisplayName, 14 scalekit.CreateOrganizationOptions{ 15 ExternalID: org.ExternalID, 16 Metadata: org.Metadata, 17 }, 18 ) 19 if err != nil { 20 log.Fatal(err) 21 } 22 fmt.Printf("Created organization: %s\n", result.ID) 23 } ``` * Java Create organizations ```java List> organizations = Arrays.asList( Map.of("display_name", "Megasoft Inc", "external_id", "org_123", "metadata", Map.of("plan", "enterprise")), Map.of("display_name", "Acme Corp", "external_id", "org_456", "metadata", Map.of("plan", "starter")) ); for (Map org : organizations) { CreateOrganization createOrganization = CreateOrganization.newBuilder() .setDisplayName((String) org.get("display_name")) .setExternalId((String) org.get("external_id")) .putAllMetadata((Map) org.get("metadata")) .build(); Organization result = scalekitClient.organizations().create(createOrganization); System.out.println("Created organization: " + result.getId()); } ``` **Then create users within organizations:** * cURL Create a user inside an organization ```bash 1 curl "$SCALEKIT_ENVIRONMENT_URL/api/v1/organizations/{organization_id}/users" \ 2 --request POST \ 3 --header 'Content-Type: application/json' \ 4 --data '{ 5 "email": "user@example.com", 6 "external_id": "usr_987", 7 "membership": { 8 "roles": ["admin"], 9 "metadata": { "department": "engineering" } 10 }, 11 "user_profile": { 12 "first_name": "John", 13 "last_name": "Doe", 14 "locale": "en-US" 15 } 16 }' ``` * Node.js Create users in organizations ```javascript const { user } = await scalekit.user.createUserAndMembership("org_123", { email: "user@example.com", externalId: "usr_987", metadata: { department: "engineering", location: "nyc-office" }, userProfile: { firstName: "John", lastName: "Doe", }, }); ``` * Python Create users in organizations ```python from scalekit.v1.users.users_pb2 import CreateUser from scalekit.v1.commons.commons_pb2 import UserProfile user_msg = CreateUser( email="user@example.com", external_id="usr_987", metadata={"department": "engineering", "location": "nyc-office"}, user_profile=UserProfile( first_name="John", last_name="Doe" ) ) create_resp, _ = scalekit_client.user.create_user_and_membership("org_123", user_msg) ``` * Go Create users in organizations ```go 1 newUser := &usersv1.CreateUser{ 2 Email: "user@example.com", 3 ExternalId: "usr_987", 4 Metadata: map[string]string{ 5 "department": "engineering", 6 "location": "nyc-office", 7 }, 8 UserProfile: &usersv1.CreateUserProfile{ 9 FirstName: "John", 10 LastName: "Doe", 11 }, 12 } 13 14 cuResp, err := scalekit.User().CreateUserAndMembership(ctx, "org_123", newUser, false) 15 if err != nil { 16 log.Fatal(err) 17 } ``` * Java Create users in organizations ```java CreateUser createUser = CreateUser.newBuilder() .setEmail("user@example.com") .setExternalId("usr_987") .putMetadata("department", "engineering") .putMetadata("location", "nyc-office") .setUserProfile( CreateUserProfile.newBuilder() .setFirstName("John") .setLastName("Doe") .build()) .build(); CreateUserAndMembershipResponse cuResp = scalekitClient.users() .createUserAndMembership("org_123", createUser); System.out.println("Created user: " + cuResp.getUser().getId()); ``` - **Batch** your imports—run them in parallel for speed but respect rate limits - Include `"sendInvitationEmail": false` when creating users to skip invite emails. Scalekit will automatically set the membership status to `active` and mark the email as verified. 3. ## Configure redirects and roles [Section titled “Configure redirects and roles”](#configure-redirects-and-roles) The authentication callback URL is necessary for tokens to return safely. However, depending on your application, you may want to add more redirects (such as post-logout URLs, so you can control the user experience and destination after logout). Head to **Settings → Redirects** in the dashboard. Review our [redirect URI guide](/guides/dashboard/redirects/) for validation rules and wildcard configuration. **Set up roles:** Define roles in Scalekit to control what actions users can perform in your application. When users log in, Scalekit provides their assigned roles to your application. * Create your roles under **User Management → Roles** or via the SDK * While importing users, include the `roles` array in the `membership` object. [Read more about roles](/authenticate/authz/create-roles-permissions/). * Need organization-specific roles? [Reach out to discuss](/support/contact-us) your requirements 4. ## Update your application code [Section titled “Update your application code”](#update-your-application-code) **Replace session middleware:** Replace legacy JWT validation with the Scalekit SDK or our **JWKS endpoint**. Verify: * Access tokens are accepted across all routes * Refresh tokens renew seamlessly * Ensure your application’s checks use the `roles` claim from Scalekit’s tokens ([learn more](/authenticate/authz/create-roles-permissions/)) Tip Use our language SDKs for ready-to-use middlewares in Node, Go, Python, and Java. **Customize your Login Page:** Your application redirects users to a **Scalekit-hosted login page**. Tailor the experience by updating your logo, colours, copy, and legal links in the dashboard. **Update secondary flows:** * Verify email prompt * [Branding (logo, colours, legal copy)](/fsa/guides/login-page-branding/) 5. ## Deploy and monitor [Section titled “Deploy and monitor”](#deploy-and-monitor) Execute your migration carefully with proper monitoring: **Pre-deployment testing:** * Test login flows with a few migrated users * Verify session management and token validation * Check role-based access control **Deployment steps:** 1. Deploy your updated application code 2. Enable the feature flag to route traffic to Scalekit 3. Monitor authentication success rates and error logs 4. Have your rollback plan ready **Post-deployment monitoring:** * Watch authentication error rates * Monitor session creation and validation * Check user feedback and support tickets * Verify SSO connections work correctly Tip Start with a small percentage of users (5-10%) before rolling out to everyone. ## Frequently Asked Questions [Section titled “Frequently Asked Questions”](#frequently-asked-questions) Why can’t users log in after migration? * Verify callback URLs are registered in Scalekit dashboard * Check that `external_id` mappings are correct * Ensure email addresses match exactly between systems Why is session validation failing? * Update JWT validation to use Scalekit’s JWKS endpoint * Verify token expiration and refresh logic * Check that role claims are read correctly Why aren’t SSO connections working? * Confirm organization has SSO enabled in features * Verify identity provider configuration * Test with IdP-initiated login Password migration Password-based authentication migrations are on the way. If you need to migrate existing passwords, please [contact us](/support/contact-us). --- # DOCUMENT BOUNDARY --- # Agent connectors > Search connectors and tools for Slack, Google Workspace, Salesforce, and more. # Connectors Copy Markdown[Open markdown](/agentkit/connectors.md) 33,000+ ready-to-call tools for your agent. Scalekit handles each user's sign-in, token refresh and tool calls. Search apps or a task, like "send an email"`/` ## [Back to all connectors](?#ckc-directory) ### The best tool in each app Best match first. Each row opens the tool on its connector page. [](#) ### ### Apps that can do this ### You can still connect your agent to it: * [**Add your own connector** · any OAuth, API key or MCP service](/agentkit/bring-your-own-connector/overview/) * [**Call its API through Scalekit** · custom tools with managed auth](/agentkit/tools/custom-tools/) * [**Request a connector** · tell us what your agent needs](https://scalekitsupport.portal.usepylon.com/forms/request-a-connector-tool) ## Popular [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/sales_force.svg)Salesforce74 tools](/agentkit/connectors/salesforce/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/notion.svg)Notion51 tools](/agentkit/connectors/notion/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/slack.svg)Slack96 tools](/agentkit/connectors/slack/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/exa.svg)Exa21 tools](/agentkit/connectors/exa/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/gmail.svg)Gmail48 tools](/agentkit/connectors/gmail/)[![](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/granola.svg)Granola MCP server14 tools](/agentkit/connectors/granolamcp/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/hub_spot.svg)HubSpot457 tools](/agentkit/connectors/hubspot/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/github.png)GitHub217 tools](/agentkit/connectors/github/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/linkedin.svg)LinkedIn59 tools](/agentkit/connectors/linkedin/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/google_sheets.svg)Google Sheets48 tools](/agentkit/connectors/googlesheets/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/bigquery.svg)Google BigQuery47 tools](/agentkit/connectors/bigquery/)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/confluence.svg)Confluence147 tools](/agentkit/connectors/confluence/) ## Start from a task [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/gmail.svg)![](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/outlook.svg)Send an emailGmail · Outlook](?q=send%20an%20email)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/jira.svg)![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/linear.svg)Create an issueJira · Linear](?q=create%20an%20issue)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/slack.svg)![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/microsoft-teams.svg)![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/discord.svg)Read channel messagesSlack · Teams · Discord](?q=read%20channel%20messages)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/google_calendar.svg)![](https://cdn.scalekit.cloud/sk-connect/assets/provider-icons/outlook.svg)![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/microsoft-teams.svg)Schedule a meetingGoogle Calendar · Outlook · Teams](?q=schedule%20a%20meeting)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/hub_spot.svg)![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/sales_force.svg)![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/pipedrive.svg)Update a CRM dealHubSpot · Salesforce · Pipedrive](?q=update%20a%20crm%20deal)[![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/notion.svg)![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/confluence.svg)Write a pageNotion · Confluence](?q=write%20a%20page) ## All connectors ### Filters Clear all #### Category All614AI151Productivity147Developer Tools125Analytics123CRM & Sales103Marketing93Communication73Automation65Search54Collaboration52Accounting & Finance45Files & Documents45Media42Project Management40Transcription30Monitoring29Design26Databases22Customer Support21Healthcare & life sciences16Calendar14 14 more Built by \[x]Any\[ ]Scalekit348\[ ]Vendor MCP266 Credentials \[ ]Scalekit credentialsConnect without registering your own OAuth app Filters**614 connectors**Sort:A–Z [![](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjYiIGhlaWdodD0iMjYiIHZpZXdCb3g9IjAgMCAyNiAyNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTMgMEMyMC4xNzk2IDAgMjYgNS44MjAzOSAyNiAxM0MyNiAyMC4xOCAyMC4xNzk2IDI2IDEzIDI2QzUuODIwMzkgMjYgMCAyMC4xOCAwIDEzQzEuMjg4NTVlLTA3IDUuODIwMzkgNS44MjAzOSAxLjI4ODU5ZS0wNyAxMyAwWk0xMi4wMzIyIDUuMjY1NjJDMTEuNTA3MiA1LjI2NTYyIDExLjI0NDUgNS4yNjYwNiAxMS4wNDM5IDUuMzY4MTZDMTAuODY3NSA1LjQ1ODA3IDEwLjcyMzcgNS42MDE4OCAxMC42MzM4IDUuNzc4MzJDMTAuNTMxOCA1Ljk3ODg1IDEwLjUzMTIgNi4yNDE3OSAxMC41MzEyIDYuNzY2NlY5Ljc0MjE5QzEwLjUzMTIgOS44NzMyOSAxMC41MzEzIDkuOTM5MzkgMTAuNTQ3OSAxMEMxMC41NjI1IDEwLjA1MzYgMTAuNTg2OCAxMC4xMDQyIDEwLjYxOTEgMTAuMTQ5NEMxMC42NTU4IDEwLjIwMDUgMTAuNzA2OCAxMC4yNDI1IDEwLjgwODYgMTAuMzI1MkwxMS41NjE1IDEwLjkzNjVDMTEuNjg0MiAxMS4wMzYyIDExLjc0NTMgMTEuMDg2NSAxMS43Njc2IDExLjE0NjVDMTEuNzg3IDExLjE5OSAxMS43ODcxIDExLjI1NyAxMS43Njc2IDExLjMwOTZDMTEuNzQ1MSAxMS4zNjk0IDExLjY4MzkgMTEuNDE5MSAxMS41NjE1IDExLjUxODZMMTAuODA4NiAxMi4xMjk5QzEwLjcwNjggMTIuMjEyNiAxMC42NTU4IDEyLjI1NDYgMTAuNjE5MSAxMi4zMDU3QzEwLjU4NjggMTIuMzUwOSAxMC41NjI1IDEyLjQwMTUgMTAuNTQ3OSAxMi40NTUxQzEwLjUzMTMgMTIuNTE1NyAxMC41MzEyIDEyLjU4MTcgMTAuNTMxMiAxMi43MTI5TDEwLjUzMTMgMTkuMjMzNEMxMC41MzEzIDE5Ljc1ODYgMTAuNTMxNiAyMC4wMjIxIDEwLjYzMzggMjAuMjIyN0MxMC43MjM3IDIwLjM5ODkgMTAuODY3NiAyMC41NDIgMTEuMDQzOSAyMC42MzE4QzExLjI0NDUgMjAuNzMzOSAxMS41MDcyIDIwLjczNDQgMTIuMDMyMiAyMC43MzQ0SDEzLjk2NzhDMTQuNDkyOCAyMC43MzQ0IDE0Ljc1NTUgMjAuNzM0IDE0Ljk1NjEgMjAuNjMxOEMxNS4xMzI0IDIwLjU0MiAxNS4yNzYzIDIwLjM5ODkgMTUuMzY2MiAyMC4yMjI3QzE1LjQ2ODQgMjAuMDIyMSAxNS40Njg4IDE5Ljc1ODYgMTUuNDY4OCAxOS4yMzM0VjE2LjI1NzhDMTUuNDY4OCAxNi4xMjY3IDE1LjQ2ODcgMTYuMDYwNiAxNS40NTIxIDE2QzE1LjQzNzUgMTUuOTQ2NCAxNS40MTMyIDE1Ljg5NTggMTUuMzgwOSAxNS44NTA2QzE1LjM0NDIgMTUuNzk5NSAxNS4yOTMyIDE1Ljc1NzUgMTUuMTkxNCAxNS42NzQ4TDE0LjQzODUgMTUuMDYzNUMxNC4zMTU5IDE0Ljk2MzkgMTQuMjUzOCAxNC45MTQ0IDE0LjIzMTQgMTQuODU0NUMxNC4yMTE5IDE0LjgwMTkgMTQuMjExOSAxNC43NDMxIDE0LjIzMTQgMTQuNjkwNEMxNC4yNTM4IDE0LjYzMDUgMTQuMzE1OSAxNC41ODEgMTQuNDM4NSAxNC40ODE0TDE1LjE5MTQgMTMuODcwMUMxNS4yOTMyIDEzLjc4NzQgMTUuMzQ0MiAxMy43NDU0IDE1LjM4MDkgMTMuNjk0M0MxNS40MTMyIDEzLjY0OTEgMTUuNDM3NSAxMy41OTg1IDE1LjQ1MjEgMTMuNTQ0OUMxNS40Njg3IDEzLjQ4NDMgMTUuNDY4OCAxMy40MTgzIDE1LjQ2ODggMTMuMjg3MVY2Ljc2NjZDMTUuNDY4OCA2LjI0MTggMTUuNDY4MiA1Ljk3ODg1IDE1LjM2NjIgNS43NzgzMkMxNS4yNzYzIDUuNjAxODggMTUuMTMyNSA1LjQ1ODA3IDE0Ljk1NjEgNS4zNjgxNkMxNC43NTU1IDUuMjY2MDYgMTQuNDkyOCA1LjI2NTYyIDEzLjk2NzggNS4yNjU2MkgxMi4wMzIyWiIgZmlsbD0iYmxhY2siLz48L3N2Zz4=)1PasswordScalekit connectorConnect to the 1Password Events Reporting API to pull your 1Password Business account's security events: successful and failed sign-in attempts, usage of...Access token · Scalekit connectorAccess token4 tools](/agentkit/connectors/onepassword/) [![](data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4IiB2aWV3Qm94PSIwIDAgMjQgMjQiPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xNzg1XzkyOTQxKSI+PHBhdGggZD0iTTE2My4zMjQgMjAuMjk2MVYxNC4xNzcxQzE2My4zMjQgMTIuOTIzMSAxNjMuNTg5IDEyLjA4MzMgMTY0LjExOSAxMS42NTc1QzE2NC42NDkgMTEuMjMxOCAxNjUuNTAxIDExLjAxODkgMTY2LjY3MyAxMS4wMTg5SDE2Ny44MDVWOC4yNDY4SDE2Ni44NDJDMTY2LjA3NyA4LjIyOTUxIDE2NS4zMTYgOC4zNjA2MiAxNjQuNjAxIDguNjMyODZDMTY0LjAxNyA4Ljg1ODMyIDE2My41MDcgOS4yNDIzNCAxNjMuMTI5IDkuNzQxNzJMMTYyLjU0OSA4LjI0NjhIMTYwLjM4VjIwLjI5NjFIMTYzLjMyNFpNMTUyLjgxNiAxMC40ODc3QzE1My42NTIgMTAuNDg3NyAxNTQuMjg2IDEwLjcyODcgMTU0LjcyIDExLjIxMDVDMTU1LjE3NyAxMS43NTExIDE1NS40MzIgMTIuNDMzMiAxNTUuNDQzIDEzLjE0MDhIMTQ5Ljk3MUMxNDkuOTgyIDEyLjQzMDggMTUwLjI0MyAxMS43NDc1IDE1MC43MDggMTEuMjEwNUMxNTAuOTU3IDEwLjk1OTkgMTUxLjI1NyAxMC43NjU3IDE1MS41ODggMTAuNjQxQzE1MS45MTggMTAuNTE2MyAxNTIuMjcyIDEwLjQ2NCAxNTIuNjI0IDEwLjQ4NzdIMTUyLjgxNlpNMTU1LjI3NCAxNi42NTlDMTU1LjA4IDE3LjEwNDIgMTU0Ljc1MiAxNy40Nzc4IDE1NC4zMzYgMTcuNzI4MUMxNTMuOTIgMTcuOTc4MyAxNTMuNDM2IDE4LjA5MjYgMTUyLjk1MiAxOC4wNTUySDE1Mi43NkMxNTEuODU4IDE4LjA1NTIgMTUxLjE4MSAxNy44MDg1IDE1MC43MjkgMTcuMzA5MkMxNTAuMjUyIDE2Ljc2NjcgMTQ5Ljk3NSAxNi4wNzczIDE0OS45NDUgMTUuMzU1N0gxNTguMzhWMTQuMTI3OEMxNTguMzggMTIuMjQ2OCAxNTcuODgyIDEwLjc0NDEgMTU2Ljg4NSA5LjYxOThDMTU1Ljg4OSA4LjQ5NTQ3IDE1NC41MzQgNy45MzMzIDE1Mi44MjEgNy45MzMzSDE1Mi42M0MxNTAuOTA4IDcuOTMzMyAxNDkuNTQ1IDguNDkxNiAxNDguNTQzIDkuNjA4MkMxNDcuNTQgMTAuNzI0OCAxNDcuMDM4IDEyLjE2NzUgMTQ3LjAzNiAxMy45MzYyVjE0LjcyODdDMTQ3LjAyNyAxNS41NDI0IDE0Ny4xNjMgMTYuMzUxMyAxNDcuNDM3IDE3LjExNzZDMTQ3LjY4MSAxNy44MDc5IDE0OC4wNjcgMTguNDM5NyAxNDguNTY5IDE4Ljk3MjVDMTQ5LjA3NyAxOS40OTkgMTQ5LjY5MyAxOS45MDk5IDE1MC4zNzQgMjAuMTc3MUMxNTEuMTM5IDIwLjQ3NTQgMTUxLjk1NCAyMC42MjIzIDE1Mi43NzUgMjAuNjA5NkgxNTIuOTY3QzE1My45NzYgMjAuNjM0MyAxNTQuOTc0IDIwLjQwMjYgMTU1Ljg2OSAxOS45MzYyQzE1Ni43MDUgMTkuNDcxNCAxNTcuMzkxIDE4Ljc3NzUgMTU3Ljg0NiAxNy45MzYyTDE1NS4yNzQgMTYuNjU5Wk0xMzcuMTk5IDIwLjI5NjFWMTMuMjU5OUMxMzcuMTg2IDEyLjUzOTMgMTM3LjQxNSAxMS44MzU0IDEzNy44NDkgMTEuMjU5OUMxMzguMDYyIDEwLjk5NSAxMzguMzM1IDEwLjc4NDcgMTM4LjY0NSAxMC42NDY0QzEzOC45NTYgMTAuNTA4MSAxMzkuMjk1IDEwLjQ0NTcgMTM5LjYzNCAxMC40NjQ1SDEzOS45MjRDMTQwLjIxNyAxMC40NTExIDE0MC41MDkgMTAuNTAwMSAxNDAuNzgxIDEwLjYwODJDMTQxLjA1NCAxMC43MTYzIDE0MS4zIDEwLjg4MTEgMTQxLjUwNCAxMS4wOTE1QzE0MS44OTggMTEuNTA5NSAxNDIuMDg0IDEyLjIwMDQgMTQyLjA4NCAxMy4xNjQxVjIwLjI5NjFIMTQ1LjA1MVYxMi42NTMyQzE0NS4wNTEgMTEuMjM5NSAxNDQuNjU5IDEwLjA5NTkgMTQzLjg5IDkuMjMwODNDMTQzLjUwMSA4Ljc5OTc1IDE0My4wMjEgOC40NTk1MyAxNDIuNDg2IDguMjM0MjlDMTQxLjk1MSA4LjAwOTA2IDE0MS4zNzMgNy45MDQzMiAxNDAuNzkyIDcuOTI3NUgxNDAuNDU2QzEzOS44MzMgNy45MjMxNyAxMzkuMjE3IDguMDUwOCAxMzguNjQ3IDguMzAxOTZDMTM4LjA5MyA4LjU0MjE4IDEzNy41OTggOC44OTk3IDEzNy4xOTYgOS4zNDk4NVYzLjQyODIySDEzNC4yMzVWMjAuMjk2MUgxMzcuMTk5Wk0xMjkuNDQgMTcuNjU3NUMxMjkuMjQ1IDE3LjQ4OTIgMTI5LjE0OSAxNy4xNzI4IDEyOS4xNDkgMTYuNzA1NFYxMC42MjEzSDEzMi4wNTJWOC4yMzUxOUgxMjkuMTQ5VjUuNTg0OTdIMTI2LjYxOEwxMjYuMzA1IDguMjM1MTlIMTI0LjM0VjEwLjYyMTNIMTI2LjIxOFYxNi43MTdDMTI2LjIxOCAxNy44MjU5IDEyNi40ODMgMTguNjk2NyAxMjcuMDEzIDE5LjMyOTVDMTI3LjU0MyAxOS45NjIzIDEyOC40MzUgMjAuMjc4NyAxMjkuNjg5IDIwLjI3ODdIMTMyLjAyNlYxNy44OTI3SDEzMC4zMTZDMTMwLjAwNSAxNy45MTcgMTI5LjY5NiAxNy44MjcyIDEyOS40NDYgMTcuNjQwMU0xMTcuMTI5IDE4LjA4NzFDMTE2LjYzNyAxOC4xMTkzIDExNi4xNDcgMTcuOTkyMSAxMTUuNzMzIDE3LjcyNDNDMTE1LjU3NSAxNy42MDg3IDExNS40NDkgMTcuNDU2IDExNS4zNjQgMTcuMjc5OEMxMTUuMjggMTcuMTAzNyAxMTUuMjQgMTYuOTA5NCAxMTUuMjQ4IDE2LjcxNDFWMTYuNjE1NEMxMTUuMjQ1IDE2LjQwMjkgMTE1LjI5MiAxNi4xOTI3IDExNS4zODYgMTYuMDAyMkMxMTUuNDgxIDE1LjgxMTggMTE1LjYyIDE1LjY0NjggMTE1Ljc5MSAxNS41MjExQzExNi4xNTUgMTUuMjQwNSAxMTYuNzkzIDE1LjA5OTIgMTE3LjcwNyAxNS4wOTczSDExOS45MDFWMTUuNTU1OUMxMTkuOTAxIDE2LjQyNDggMTE5LjY2NCAxNy4wNjczIDExOS4xOSAxNy40ODM0QzExOC42NjggMTcuOTE4MSAxMTguMDAyIDE4LjE0MTggMTE3LjMyNCAxOC4xMTA0TDExNy4xMjkgMTguMDg3MVpNMTE2LjUyNSAyMC41OTIyQzExNy4yMzYgMjAuNjA2NCAxMTcuOTQxIDIwLjQ1ODUgMTE4LjU4NiAyMC4xNTk3QzExOS4yMDMgMTkuODU3NSAxMTkuNzI1IDE5LjM5MDMgMTIwLjA5MyAxOC44MDk5TDEyMC42MDEgMjAuMjc4N0gxMjIuODQyVjEyLjM5MTlDMTIyLjg0MiAxMC44OTYxIDEyMi40MDcgOS43NzE3MiAxMjEuNTM4IDkuMDE4OTRDMTIwLjY3IDguMjY2MTUgMTE5LjQ1NyA3Ljg4ODc5IDExNy45MDEgNy44ODY4NUgxMTcuNjExQzExNi4yMTUgNy44ODY4NSAxMTUuMTEyIDguMTc3MTMgMTE0LjMxMSA4Ljc0MDI3QzExMy41NjQgOS4yMzQyNyAxMTMuMDExIDkuOTcwMjcgMTEyLjc0MyAxMC44MjQ1TDExNS4zNTYgMTEuODg2OUMxMTUuNTAyIDExLjQ4MTUgMTE1Ljc2NiAxMS4xMjg5IDExNi4xMTMgMTAuODczOEMxMTYuNTM5IDEwLjU4MjkgMTE3LjA0OSAxMC40MzkyIDExNy41NjUgMTAuNDY0NUgxMTcuODU1QzExOC41NDYgMTAuNDY0NSAxMTkuMDYgMTAuNjMyOSAxMTkuMzk5IDEwLjk2OTZDMTE5LjU3NCAxMS4xNjA2IDExOS43MSAxMS4zODUgMTE5Ljc5NiAxMS42MjkzQzExOS44ODMgMTEuODczNyAxMTkuOTIgMTIuMTMzMSAxMTkuOTA0IDEyLjM5MTlWMTIuOTk1N0gxMTcuNzVDMTE1Ljk4MyAxMi45OTU3IDExNC42MzggMTMuMzQxMSAxMTMuNzEzIDE0LjAzMkMxMTMuMjcyIDE0LjM0NDcgMTEyLjkxNSAxNC43NjEzIDExMi42NzMgMTUuMjQ0OEMxMTIuNDMyIDE1LjcyODMgMTEyLjMxMyAxNi4yNjM5IDExMi4zMjggMTYuODA0MVYxNi45NzI1QzExMi4zMDMgMTcuNDY1MyAxMTIuMzgzIDE3Ljk1NzcgMTEyLjU2NCAxOC40MTY5QzExMi43NDQgMTguODc2IDExMy4wMjEgMTkuMjkxMSAxMTMuMzc2IDE5LjYzNDNDMTE0LjA3NCAyMC4yODQ1IDExNS4wNTggMjAuNjA5NiAxMTYuMzI4IDIwLjYwOTZMMTE2LjUyNSAyMC41OTIyWk0xMDUuMzkzIDEwLjQ3MDNDMTA2LjIyNyAxMC40NzAzIDEwNi44NjIgMTAuNzExMiAxMDcuMjk3IDExLjE5MzFDMTA3Ljc1MyAxMS43MzQ0IDEwOC4wMDggMTIuNDE2MSAxMDguMDIgMTMuMTIzNEgxMDIuNTQzQzEwMi41NTQgMTIuNDEzOSAxMDIuODE0IDExLjczMSAxMDMuMjc3IDExLjE5MzFDMTAzLjUyNiAxMC45NDMgMTAzLjgyNiAxMC43NDkxIDEwNC4xNTcgMTAuNjI0NEMxMDQuNDg3IDEwLjQ5OTcgMTA0Ljg0MSAxMC40NDcyIDEwNS4xOTMgMTAuNDcwM0gxMDUuMzkzWk0xMDcuODUyIDE2LjY0MTZDMTA3LjY1OCAxNy4wODY3IDEwNy4zMyAxNy40NjA0IDEwNi45MTQgMTcuNzEwNkMxMDYuNDk4IDE3Ljk2MDkgMTA2LjAxNCAxOC4wNzUyIDEwNS41MyAxOC4wMzc4SDEwNS4zMzVDMTA0LjQzNSAxOC4wMzc4IDEwMy43NTYgMTcuNzkxMSAxMDMuMzAzIDE3LjI5MThDMTAyLjgyNyAxNi43NDkzIDEwMi41NSAxNi4wNTk4IDEwMi41MiAxNS4zMzgySDExMC45NTVWMTQuMTEwNEMxMTAuOTU1IDEyLjIyOTQgMTEwLjQ1NyAxMC43MjY3IDEwOS40NiA5LjYwMjM4QzEwOC40NjMgOC40NzgwNSAxMDcuMTA5IDcuOTE1ODggMTA1LjM5NiA3LjkxNTg4SDEwNS4xODFDMTAzLjQ2MyA3LjkxNTg4IDEwMi4xMDIgOC40NzQxOSAxMDEuMDk3IDkuNTkwNzhDMTAwLjA5MyAxMC43MDc0IDk5LjU5MDYgMTIuMTUgOTkuNTkwNiAxMy45MTg4VjE0LjcxMTJDOTkuNTgyIDE1LjUyNDcgOTkuNzE2NiAxNi4zMzM0IDk5Ljk4ODMgMTcuMTAwMkMxMDAuMjM0IDE3Ljc5IDEwMC42MTkgMTguNDIxNSAxMDEuMTIgMTguOTU1MUMxMDEuNjMgMTkuNDgxIDEwMi4yNDcgMTkuODkxOCAxMDIuOTI5IDIwLjE1OTdDMTAzLjY5MiAyMC40NTggMTA0LjUwNyAyMC42MDQ5IDEwNS4zMjYgMjAuNTkyMkgxMDUuNTIxQzEwNi41MyAyMC42MTY5IDEwNy41MjkgMjAuMzg1MiAxMDguNDI0IDE5LjkxODhDMTA5LjI1OSAxOS40NTMxIDEwOS45NDUgMTguNzU5NCAxMTAuNDAxIDE3LjkxODhMMTA3Ljg1MiAxNi42NDE2Wk04NS40NjI5IDIwLjI3ODdMODguOTk4NSA4LjAwODc3TDkyLjM3MTUgMjAuMjk5SDk1LjA3MTFMOTkuOTE1OCAzLjY1NDY0VjMuNDM5ODRIOTcuMDEzTDkzLjgyIDE1LjM2NzNMOTAuNjg3OSAzLjQzOTg0SDg3LjQ5NDlMODQuMjAzMSAxNS41NTU5TDgxLjA4ODUgMy40MjgyMkg3OC4wMzE5VjMuNjU0NjRMODIuNjc2MyAyMC4zMDc4TDg1LjQ2MjkgMjAuMjc4N1pNNjYuNDIzNyA4LjIyOTM5VjE1Ljg2OTRDNjYuNDIzNyAxNy4yODQgNjYuODEwOCAxOC40MjQ4IDY3LjU4NDggMTkuMjkxOEM2Ny45NzUyIDE5LjcyNDIgNjguNDU2MyAyMC4wNjUgNjguOTkzOCAyMC4yODk4QzY5LjUzMTIgMjAuNTE0NiA3MC4xMTE3IDIwLjYxNzkgNzAuNjkzNyAyMC41OTIySDcxLjAwNzJDNzEuNjk1NCAyMC42MDAzIDcyLjM3NjIgMjAuNDQ4MiA3Mi45OTU2IDIwLjE0ODFDNzMuNTkzMSAxOS44NTQ4IDc0LjExMDEgMTkuNDIwNCA3NC41MDIxIDE4Ljg4MjVMNzUuMDA3MiAyMC4yNzg3SDc3LjIyNDlWOC4yNDY4SDc0LjI3MjhWMTUuMjgzMUM3NC4yODU0IDE2LjAwNjQgNzQuMDUyMSAxNi43MTI2IDczLjYxMSAxNy4yODZDNzMuMzk0NiAxNy41NSA3My4xMTkzIDE3Ljc1OTQgNzIuODA3MyAxNy44OTc2QzcyLjQ5NTIgMTguMDM1NyA3Mi4xNTUgMTguMDk4NyA3MS44MTQyIDE4LjA4MTNINzEuNTIzOUM3MS4yMzMyIDE4LjA5MjcgNzAuOTQzNSAxOC4wNDI3IDcwLjY3MzQgMTcuOTM0N0M3MC40MDM0IDE3LjgyNjcgNzAuMTU5IDE3LjY2MyA2OS45NTY0IDE3LjQ1NDNDNjkuNTU1OCAxNy4wMzY0IDY5LjM1NTUgMTYuMzQ1NSA2OS4zNTU1IDE1LjM4MThWOC4yNDY4TDY2LjQyMzcgOC4yMjkzOVpNNjMuMTkzIDguNzExMjVDNjIuMzg5OSA4LjE4Mjk0IDYxLjMzODEgNy45MTc4MiA2MC4wMzc3IDcuOTE1ODhINTkuODQzMkM1OC4xMjQ4IDcuOTE1ODggNTYuNzYzNCA4LjQyMTkzIDU1Ljc1OSA5LjQzNDAzQzU0Ljc1NDYgMTAuNDQ2MSA1NC4yNTI1IDExLjc5NTkgNTQuMjUyNSAxMy40ODM0VjE1LjAyNDdDNTQuMjUyNSAxNi43MTQxIDU0Ljc1NDYgMTguMDYzOSA1NS43NTkgMTkuMDc0MUM1Ni43NjM0IDIwLjA4NDIgNTguMTI0OCAyMC41OTAzIDU5Ljg0MzIgMjAuNTkyMkg2MC4wMzc3QzYxLjA2ODcgMjAuNjI5MSA2Mi4wOTExIDIwLjM5MjggNjMuMDAxNCAxOS45MDcyQzYzLjgxMjIgMTkuNDIxNSA2NC40NTg0IDE4LjcwMzUgNjQuODU2MiAxNy44NDYyTDYyLjQyMDggMTYuNDczMkM2Mi4yMzE1IDE2LjkxNTMgNjEuOTMxNiAxNy4zMDExIDYxLjU1IDE3LjU5MzdDNjEuMTA3OSAxNy44OTM4IDYwLjU4IDE4LjA0MTYgNjAuMDQ2NCAxOC4wMTQ2SDU5Ljg1MTlDNTguOTM3NSAxOC4wMTQ2IDU4LjI2MTIgMTcuNzI0MyA1Ny44MiAxNy4xNjk5QzU3LjM2MzEgMTYuNTQxOCA1Ny4xMzI5IDE1Ljc3NzQgNTcuMTY2OCAxNS4wMDE1VjEzLjUyNEM1Ny4xMjk3IDEyLjc0NDIgNTcuMzYwMiAxMS45NzUgNTcuODIgMTEuMzQ0QzU4LjI1MjUgMTAuNzg5NiA1OC45Mjg4IDEwLjUxMzkgNTkuODUxOSAxMC41MTM5SDYwLjA0NjRDNjAuNTkxOSAxMC40ODQ2IDYxLjEzMTEgMTAuNjQxMiA2MS41NzYxIDEwLjk1OEM2MS45NjMxIDExLjI3NTIgNjIuMjYyOCAxMS42ODU4IDYyLjQ0NyAxMi4xNTFMNjUuMDAxNCAxMC45NDY0QzY0LjYzNSAxMC4wNDIzIDY0LjAwNDggOS4yNjk0OSA2My4xOTMgOC43Mjg2Nk01MS4zMTIgOC43Mjg2NkM1MC41MDg5IDguMjAwMzYgNDkuNDU2MiA3LjkzNTIzIDQ4LjE1MzggNy45MzMzSDQ3Ljk2MjJDNDYuMjQzOCA3LjkzMzMgNDQuODgyNCA4LjQzOTM1IDQzLjg3OCA5LjQ1MTQ0QzQyLjg3MzcgMTAuNDYzNSA0Mi4zNzE1IDExLjgxMzMgNDIuMzcxNSAxMy41MDA4VjE1LjA0MjJDNDIuMzcxNSAxNi43MzE2IDQyLjg3MzcgMTguMDgxMyA0My44NzggMTkuMDkxNUM0NC44ODI0IDIwLjEwMTcgNDYuMjQzOCAyMC42MDc3IDQ3Ljk2MjIgMjAuNjA5Nkg0OC4xNTM4QzQ5LjE4NTggMjAuNjQ2OCA1MC4yMDkyIDIwLjQxMDQgNTEuMTIwNCAxOS45MjQ2QzUxLjkzMTIgMTkuNDM4OSA1Mi41Nzc0IDE4LjcyMDkgNTIuOTc1MyAxNy44NjM2TDUwLjUzOTkgMTYuNDkwNkM1MC4zNTA2IDE2LjkzMjcgNTAuMDUwNyAxNy4zMTg1IDQ5LjY2OSAxNy42MTExQzQ5LjIyNjEgMTcuOTExNyA0OC42OTcxIDE4LjA1OTQgNDguMTYyNSAxOC4wMzJINDcuOTcwOUM0Ny4wNTM2IDE4LjAzMiA0Ni4zODAyIDE3Ljc0MTcgNDUuOTM5IDE3LjE4NzNDNDUuNDg0NCAxNi41NTgzIDQ1LjI1NTMgMTUuNzk0MyA0NS4yODg4IDE1LjAxODlWMTMuNTI0QzQ1LjI1MjIgMTIuNzQ0NyA0NS40ODE1IDExLjk3NiA0NS45MzkgMTEuMzQ0QzQ2LjM3NDQgMTAuNzg5NiA0Ny4wNDc4IDEwLjUxMzkgNDcuOTcwOSAxMC41MTM5SDQ4LjE2MjVDNDguNzA4OSAxMC40ODQyIDQ5LjI0OTIgMTAuNjQwOCA0OS42OTUxIDEwLjk1OEM1MC4wODIxIDExLjI3NTIgNTAuMzgxOCAxMS42ODU4IDUwLjU2NiAxMi4xNTFMNTMuMTIwNCAxMC45NDY0QzUyLjc1NCAxMC4wNDIzIDUyLjEyMzggOS4yNjk0OSA1MS4zMTIgOC43Mjg2NlpNMzQuNDE3OSA3LjgzNzUxTDM2LjQxNzkgMTQuMDU1MkgzMi4zNTQxTDM0LjQxNzkgNy44Mzc1MVpNMzIuOTk1NiAzLjQyODIyTDI3LjM4NzUgMjAuMDgxM1YyMC4yOTYxSDMwLjMyNzlMMzEuNDg5IDE2Ljc3OEgzNy4zMjA3TDM4LjQ1NTcgMjAuMjk2MUg0MS41Mzg0VjIwLjA4MTNMMzUuOTI0NSAzLjQyODIySDMyLjk5NTZaIiBmaWxsPSJ3aGl0ZSI+PC9wYXRoPjxwYXRoIGQ9Ik0xMi4wMDMgMjAuMzI4QzEwLjM1NzQgMjAuMzI4NiA4Ljc0ODcxIDE5Ljg0MTIgNy4zODAyMiAxOC45Mjc0QzYuMDExNzQgMTguMDEzNiA0Ljk0NDk4IDE2LjcxNDUgNC4zMTQ4NyAxNS4xOTQ0QzMuNjg0NzUgMTMuNjc0MyAzLjUxOTU5IDEyLjAwMTUgMy44NDAyNyAxMC4zODc1QzQuMTYwOTQgOC43NzM1IDQuOTUzMDUgNy4yOTA4NiA2LjExNjQxIDYuMTI3MUM3LjI3OTc3IDQuOTYzMzMgOC43NjIxMiA0LjE3MDcgMTAuMzc2IDMuODQ5NDdDMTEuOTg5OSAzLjUyODIzIDEzLjY2MjcgMy42OTI4MSAxNS4xODMxIDQuMzIyNEMxNi43MDM0IDQuOTUxOTggMTguMDAyOSA2LjAxODI4IDE4LjkxNzEgNy4zODY0NUMxOS44MzE0IDguNzU0NjIgMjAuMzE5NCAxMC4zNjMyIDIwLjMxOTQgMTIuMDA4N0MyMC4zMTcxIDE0LjIxMzkgMTkuNDQwMiAxNi4zMjgyIDE3Ljg4MTIgMTcuODg3OEMxNi4zMjIxIDE5LjQ0NzQgMTQuMjA4MiAyMC4zMjQ5IDEyLjAwMyAyMC4zMjhaTTI0LjAwODggMTEuOTY4MUwyMS4yNzE1IDkuNDg5MTJMMjIuMzc0NSA1Ljk3Njc4TDE4Ljc2NjQgNS4xOTMwM0wxNy45NzEgMS41OTA3MUwxNC40NTg3IDIuNzE5ODhMMTEuOTY4MSAwTDkuNDg5MTggMi43MzQzOUw1Ljk3MTA0IDEuNjIyNjRMNS4xOTMxIDUuMjI0OTZMMS41OTA3OCA2LjAyMzIyTDIuNzE0MTUgOS41NDEzNkwtMC4wMDU3MzczIDEyLjAzMTlMMi43MjU3NiAxNC41MDhMMS42MTY5MSAxOC4wMjYxTDUuMjIyMTMgMTguODA0MUw2LjAyMDM5IDIyLjQwNjRMOS41MzI3MyAyMS4yODAxTDEyLjAyNjIgMjRMMTQuNTAyMiAyMS4yNjU2TDE4LjAyMDQgMjIuMzc3NEwxOC43OTgzIDE4Ljc3MjFMMjIuNDAwNyAxNy45NzFMMjEuMjkxOCAxNC40NTg2TDI0LjAwODggMTEuOTY4MVoiIGZpbGw9IiNGMDU1MTQiPjwvcGF0aD48cGF0aCBkPSJNNi43NTE4MSA2Ljc1NDc1QzcuNzg5NDggNS43MTcyMyA5LjExMTUgNS4wMTA3MSAxMC41NTA3IDQuNzI0NTJDMTEuOTg5OSA0LjQzODMzIDEzLjQ4MTcgNC41ODUzMiAxNC44MzczIDUuMTQ2OTFDMTYuMTkzIDUuNzA4NSAxNy4zNTE3IDYuNjU5NDggMTguMTY2OSA3Ljg3OTU3QzE4Ljk4MjEgOS4wOTk2NyAxOS40MTcyIDEwLjUzNDEgMTkuNDE3MiAxMi4wMDE1QzE5LjQxNzIgMTMuNDY4OSAxOC45ODIxIDE0LjkwMzMgMTguMTY2OSAxNi4xMjM0QzE3LjM1MTcgMTcuMzQzNSAxNi4xOTMgMTguMjk0NSAxNC44MzczIDE4Ljg1NjFDMTMuNDgxNyAxOS40MTc2IDExLjk4OTkgMTkuNTY0NiAxMC41NTA3IDE5LjI3ODVDOS4xMTE1IDE4Ljk5MjMgNy43ODk0OCAxOC4yODU3IDYuNzUxODEgMTcuMjQ4MkM1LjM2MjI4IDE1Ljg1NTYgNC41ODE5MSAxMy45Njg3IDQuNTgxOTEgMTIuMDAxNUM0LjU4MTkxIDEwLjAzNDIgNS4zNjIyOCA4LjE0NzMzIDYuNzUxODEgNi43NTQ3NVoiIGZpbGw9IiNGMDU1MTQiPjwvcGF0aD48cGF0aCBkPSJNMTY2LjMyOCAyMC4yOTYxQzE2Ni4xMzkgMjAuMzAwOCAxNjUuOTUyIDIwLjI0ODggMTY1Ljc5MiAyMC4xNDY5QzE2NS42MzMgMjAuMDQ1IDE2NS41MDcgMTkuODk3OCAxNjUuNDMyIDE5LjcyNDFDMTY1LjM1NiAxOS41NTA0IDE2NS4zMzQgMTkuMzU4MSAxNjUuMzY4IDE5LjE3MTlDMTY1LjQwMyAxOC45ODU2IDE2NS40OTIgMTguODEzOCAxNjUuNjI0IDE4LjY3ODRDMTY1Ljc1NyAxOC41NDMgMTY1LjkyNiAxOC40NTAyIDE2Ni4xMTIgMTguNDExOUMxNjYuMjk3IDE4LjM3MzYgMTY2LjQ5IDE4LjM5MTUgMTY2LjY2NSAxOC40NjMyQzE2Ni44NDEgMTguNTM1IDE2Ni45OTEgMTguNjU3NCAxNjcuMDk2IDE4LjgxNDhDMTY3LjIwMSAxOC45NzIzIDE2Ny4yNTcgMTkuMTU3NSAxNjcuMjU3IDE5LjM0NjlDMTY3LjI1OCAxOS40NzAzIDE2Ny4yMzUgMTkuNTkyOCAxNjcuMTkgMTkuNzA3M0MxNjcuMTQ0IDE5LjgyMTggMTY3LjA3NiAxOS45MjYyIDE2Ni45ODkgMjAuMDE0NEMxNjYuOTAzIDIwLjEwMjUgMTY2LjggMjAuMTcyOCAxNjYuNjg3IDIwLjIyMTJDMTY2LjU3MyAyMC4yNjk1IDE2Ni40NTEgMjAuMjk1IDE2Ni4zMjggMjAuMjk2MVpNMTY2LjMyOCAxOC41NTQ1QzE2Ni4xNzEgMTguNTQ4MSAxNjYuMDE2IDE4LjU4ODcgMTY1Ljg4MyAxOC42NzExQzE2NS43NDkgMTguNzUzNCAxNjUuNjQ0IDE4Ljg3MzggMTY1LjU3OSAxOS4wMTY3QzE2NS41MTUgMTkuMTU5NSAxNjUuNDk0IDE5LjMxODUgMTY1LjUyMSAxOS40NzNDMTY1LjU0NyAxOS42Mjc2IDE2NS42MTkgMTkuNzcwNyAxNjUuNzI3IDE5Ljg4NEMxNjUuODM2IDE5Ljk5NzQgMTY1Ljk3NSAyMC4wNzU4IDE2Ni4xMjkgMjAuMTA5MUMxNjYuMjgyIDIwLjE0MjUgMTY2LjQ0MSAyMC4xMjkzIDE2Ni41ODcgMjAuMDcxM0MxNjYuNzMzIDIwLjAxMzIgMTY2Ljg1OCAxOS45MTMgMTY2Ljk0NiAxOS43ODM0QzE2Ny4wMzQgMTkuNjUzOCAxNjcuMDgyIDE5LjUwMDggMTY3LjA4MyAxOS4zNDRDMTY3LjA4NSAxOS4yNDI3IDE2Ny4wNjggMTkuMTQxOCAxNjcuMDMxIDE5LjA0NzJDMTY2Ljk5NSAxOC45NTI2IDE2Ni45NCAxOC44NjYzIDE2Ni44NyAxOC43OTMxQzE2Ni43OTkgMTguNzE5OSAxNjYuNzE1IDE4LjY2MTQgMTY2LjYyMiAxOC42MjFDMTY2LjUyOSAxOC41ODA1IDE2Ni40MjkgMTguNTU4OSAxNjYuMzI4IDE4LjU1NzRWMTguNTU0NVpNMTY1Ljk2NSAxOC43ODY3SDE2Ni4zMUMxNjYuNTc4IDE4Ljc4NjcgMTY2LjcyOCAxOC44OTk5IDE2Ni43MjggMTkuMTIwNVYxOS4xNDY2QzE2Ni43MzMgMTkuMjEyIDE2Ni43MTcgMTkuMjc3MyAxNjYuNjgyIDE5LjMzMjdDMTY2LjY0NyAxOS4zODgxIDE2Ni41OTUgMTkuNDMwOCAxNjYuNTM0IDE5LjQ1NDNMMTY2Ljc1MiAxOS44NTQ5SDE2Ni41MTFMMTY2LjMxOSAxOS40ODYzSDE2Ni4xOTRWMTkuODU0OUgxNjUuOTY1VjE4Ljc4NjdaTTE2Ni4xOTQgMTkuMDAxNVYxOS4yNzQ0SDE2Ni4zMzRDMTY2LjQzOCAxOS4yNzQ0IDE2Ni40OTYgMTkuMjMwOCAxNjYuNDk2IDE5LjE0MDhWMTkuMTI2M0MxNjYuNDk2IDE5LjA0NSAxNjYuNDM4IDE5LjAwMTUgMTY2LjMzNCAxOS4wMDE1SDE2Ni4xOTRaIiBmaWxsPSJ3aGl0ZSI+PC9wYXRoPjwvZz48ZGVmcz48Y2xpcFBhdGggaWQ9ImNsaXAwXzE3ODVfOTI5NDEiPjxyZWN0IHdpZHRoPSIxNjcuODA4IiBoZWlnaHQ9IjI0IiBmaWxsPSJ3aGl0ZSI+PC9yZWN0PjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg==)AccuWeatherScalekit connectorAccuWeather's Core Weather API. Find AccuWeather location keys by city name, postal code, coordinates, IP address or partial text, then get current...Access token · Scalekit connectorAccess token30 tools](/agentkit/connectors/accuweather/) [![](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzA2IiBoZWlnaHQ9IjMwNiIgdmlld0JveD0iMCAwIDMwNiAzMDYiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0xNTMgMjYzQzIxMy43NTEgMjYzIDI2MyAyMTMuNzUxIDI2MyAxNTNDMjYzIDkyLjI0ODcgMjEzLjc1MSA0MyAxNTMgNDNDOTIuMjQ4NyA0MyA0MyA5Mi4yNDg3IDQzIDE1M0M0MyAyMTMuNzUxIDkyLjI0ODcgMjYzIDE1MyAyNjNaIiBmaWxsPSIjMDA0Q0ZGIi8+CjxwYXRoIGQ9Ik0xOTAuMzE5IDE1Mi43NTJMMTI1Ljk1MyAxOTUuNDk3QzEyMi45NzEgMTk3LjQ4NSAxMjEuNDc5IDIwMC43MTUgMTIxLjQ3OSAyMDMuOTQ2VjIxNC42MzJMMTk5LjUxNCAxNjMuNDM4QzIwMi45OTMgMTYwLjk1MyAyMDUuMjMgMTU2Ljk3NiAyMDUuMjMgMTUyLjc1MkMyMDUuMjMgMTQ4LjUyNyAyMDMuMjQyIDE0NC41NSAxOTkuNTE0IDE0Mi4wNjVMMTIxLjQ3OSA5MS4zNjc3VjEwMS4zMDhDMTIxLjQ3OSAxMDQuNzg4IDEyMy4yMTkgMTA4LjAxOCAxMjUuOTUzIDEwOS43NThMMTkwLjMxOSAxNTIuNzUyWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTE1MS41NSAxNTYuMjMxQzE1NS4wMjkgMTU4LjQ2NyAxNTkuNTAzIDE1OC40NjcgMTYyLjk4MiAxNTYuMjMxTDE2OC40NDkgMTUyLjUwM0wxMjcuNjkyIDEyNC45MThDMTI1LjIwNyAxMjMuMTc4IDEyMS40NzkgMTI0LjkxOCAxMjEuNDc5IDEyOC4xNDhWMTM2LjM0OUwxNDIuNjAzIDE1MC41MTVMMTUxLjU1IDE1Ni4yMzFaIiBmaWxsPSJ3aGl0ZSIvPgo8L3N2Zz4K)ActiveCampaignScalekit connectorConnect to ActiveCampaign to manage contacts, lists, tags, custom fields, automations, email campaigns, deals and pipelines, accounts, notes, and...API key · Scalekit connectorAPI key120 tools](/agentkit/connectors/activecampaign/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/activepieces.svg)Activepieces MCP serverVendor MCPConnect to Activepieces MCP to trigger and manage no-code automation flows directly from your AI workflows.Scalekit credentials availableOAuth · Vendor MCPOAuth47 tools](/agentkit/connectors/activepiecesmcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/adobe.svg)Adobe Marketing Agent MCP serverVendor MCPConnect to Adobe Marketing Cloud. Manage campaigns, analytics, and journeys using a natural-language AI assistant.Scalekit credentials availableOAuth · Vendor MCPOAuth12 tools](/agentkit/connectors/adobemarketingagentmcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/advancedmd.png)AdvancedMDScalekit connectorAdvancedMD is a cloud-based medical practice management and electronic health record (EHR) platform. This connector uses the SMART on FHIR authorization...SMART on FHIR · Scalekit connectorSMART on FHIR89 tools](/agentkit/connectors/advancedmd/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/adzviser.svg)Adzviser MCP serverVendor MCPConnect to Adzviser MCP to query real-time marketing analytics across 46+ platforms - Google Ads, Facebook Ads, GA4, TikTok, LinkedIn, and more - from a...Scalekit credentials availableOAuth · Vendor MCPOAuth63 tools](/agentkit/connectors/adzvisermcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/affinda.svg)Affinda MCP serverVendor MCPAI-powered document processing platform that extracts, validates, and integrates structured data from invoices, resumes, contracts, and custom document...Scalekit credentials availableOAuth · Vendor MCPOAuth85 tools](/agentkit/connectors/affindamcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/affinity.svg)AffinityScalekit connectorConnect to Affinity relationship intelligence CRM to manage deal flow, relationships, pipeline opportunities, and network connections for private capital...Access token · Scalekit connectorAccess token49 tools](/agentkit/connectors/affinity/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/agencyanalytics.svg)Agency Analytics MCP serverVendor MCPAgency Analytics is a marketing reporting platform that enables digital agencies to monitor SEO, PPC, social media, and other channel performance in...Scalekit credentials availableOAuth · Vendor MCPOAuth36 tools](/agentkit/connectors/agencyanalyticsmcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/agentmail.svg)Agentmail MCP serverVendor MCPConnect to Agentmail MCP. Manage inboxes, send and receive email, handle drafts, threads, and attachments from your AI workflows.Scalekit credentials availableOAuth · Vendor MCPOAuth53 tools](/agentkit/connectors/agentmailmcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/ahrefs.svg)Ahrefs MCP serverVendor MCPConnect to Ahrefs MCP to access SEO data including backlinks, keyword research, site audits, rank tracking, and web analytics directly from your AI...Scalekit credentials availableOAuth · Vendor MCPOAuth134 tools](/agentkit/connectors/ahrefsmcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/airbyte.svg)Airbyte MCP serverVendor MCPConnect to Airbyte's MCP server to manage data pipelines, sources, destinations, and connections for your data integration workflows.Scalekit credentials availableOAuth · Vendor MCPOAuth20 tools](/agentkit/connectors/airbytemcp/) [![](data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+QWlyY2FsbDwvdGl0bGU+PHBhdGggZD0iTTIzLjQ1MSA1LjkwNmE2Ljk3OCA2Ljk3OCAwIDAgMC01LjM3NS01LjM5QzE2LjcyNy4yMDQgMTQuNTA4IDAgMTIgMFM3LjI3My4yMDQgNS45MjQuNTE2YTYuOTc4IDYuOTc4IDAgMCAwLTUuMzc1IDUuMzlDLjIzNyA3LjI2LjAzNCA5LjQ4NS4wMzQgMTJzLjIwMyA0Ljc0LjUxNSA2LjA5NGE2Ljk3OCA2Ljk3OCAwIDAgMCA1LjM3NSA1LjM5QzcuMjczIDIzLjc5NiA5LjQ5MiAyNCAxMiAyNHM0LjcyNy0uMjA0IDYuMDc2LS41MTZhNi45NzggNi45NzggMCAwIDAgNS4zNzUtNS4zOWMuMzExLTEuMzU0LjUxNS0zLjU3OC41MTUtNi4wOTQgMC0yLjUxNS0uMjAzLTQuNzQtLjUxNS02LjA5NHptLTUuODczIDEyLjM5NmwtLjAwMy4wMDFjLS40MjguMTUyLTEuMTY1LjI4My0yLjEwMi4zNzdsLS4xNDcuMDE0YS40NDQuNDQ0IDAgMCAxLS40NS0uMjcxIDEuODE2IDEuODE2IDAgMCAwLTEuMjk2LTEuMDc0Yy0uMzUxLS4wODEtLjkyOC0uMTM0LTEuNTgtLjEzNHMtMS4yMjkuMDUzLTEuNTguMTM0YTEuODE3IDEuODE3IDAgMCAwLTEuMjkxIDEuMDYyLjQ2Ni40NjYgMCAwIDEtLjQ3MS4yODEgOCA4IDAgMCAwLS4xMjktLjAxMmMtLjkzOC0uMDk0LTEuNjc2LS4yMjQtMi4xMDUtLjM3N2wtLjAwMy0uMDAxYS43Ni43NiAwIDAgMS0uNDkyLS43MTNjMC0uMDMyLjAwMy0uMDY2LjAwNS0uMDk4LjA3My0uOTc5LjY2Ni0zLjI3MiAxLjU1Mi01Ljg5QzguNSA4LjYwOSA5LjU1OSA2LjE4NyAxMC4wMzcgNS43MTRhMS4wMjkgMS4wMjkgMCAwIDEgLjQwNC0uMjZsLjAwNC0uMDAyYy4zMTQtLjEwNi44OTItLjE3OCAxLjU1NC0uMTc4LjY2MyAwIDEuMjQxLjA3MSAxLjU1NC4xNzhsLjAwNS4wMDJhMS4wMjUgMS4wMjUgMCAwIDEgLjQwNS4yNmMuNDc4LjQ3MiAxLjUzNyAyLjg5NSAyLjU0OSA1Ljg4Ny44ODYgMi42MTcgMS40NzkgNC45MSAxLjU1MiA1Ljg5LjAwMi4wMzIuMDA1LjA2Ni4wMDUuMDk4YS43Ni43NiAwIDAgMS0uNDkxLjcxM3oiLz48L3N2Zz4=)AircallScalekit connectorConnect to Aircall. Read calls, transcripts and AI call insights, transfer, tag and comment on calls, start calls, send SMS and WhatsApp messages, and...OAuth · Scalekit connectorOAuth78 tools](/agentkit/connectors/aircall/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/airops.svg)Airops MCP serverVendor MCPConnect to AirOps MCP. Manage brand kits, run AI-powered analytics, track AEO citations, and automate content workflows from your AI agents.API key · Vendor MCPAPI key91 tools](/agentkit/connectors/airopsmcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/airparser.svg)Airparser MCP serverVendor MCPAI-powered document parser that extracts structured data from PDFs, emails, and other documents.Scalekit credentials availableOAuth · Vendor MCPOAuth17 tools](/agentkit/connectors/airparsermcp/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/airtable.svg)AirtableScalekit connectorBases, tables, records, fields, comments and webhooks.Scalekit credentials availableOAuth · Scalekit connectorOAuth26 tools](/agentkit/connectors/airtable/) Also:[MCP server](/agentkit/connectors/airtablemcp/) [![](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAAFACAYAAADNkKWqAAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAOdEVYdFNvZnR3YXJlAEZpZ21hnrGWYwAAIW1JREFUeAHtnXuMHdddx3+z6zR2Xrab2HED6u42BZGk7dqI0haF7joplSuhrP1PcfnHdlsQ6h+x3VL6D9Q2EtBKFNuIStB/vK6gtVAl2xTRhEK9+w9yWsF6SZpGbWGvQdA4hdqbkNQGutP5zsyZe+7d+5w7j/P4fqzjmXvv7H3M4zu/1zknEHOZjNrOdDkdtS3pY/UaIV4SRC0UI2lE7UbasL6cLq+kS+MIxBwmo7Y3ajNRm5VE8AjxDIPlbTQgigtRW0yXV8QA6hbAWUkE76DQqiPEJxpRuxC1s1KjGNYhgLDsDkZtThIBJITkwCFbEQJ4WhJBvCEVUqUAQvgOR+2I0L0lo+Ksp9jGsL/T7v3SkMQ9PiEVxQyrEEAKHyFeUYgKz0sFQjgu5QLhg1m7J2obhRBCBgMVH0iKQk0vS0mUZQHORu2kNMtWCCEkL42o7ZYSrMExKR4I3yWh+BFCimEyaitROyYFU6QFOBm180Lhy40vcf3h4F4hLTSkQGuwqBjgrCRW36QQQkgZJOYaEqmIDV6N2gsyIkUIIBId54RJDkJINUAE96frizICfQSwr4cMn/xTQoj1mNQrlAzIbLrMLYKjWIAQv+NCCCH1MZsuc4lgXgF0W/xoDNgBjxNJmE2XQ4kgTp88AkjLjxBiGrPpcigRHFYAKX6EEFOZjdqqDNFzZBgnAqnn80Lyw5I2QqoAdYILg2w4qABOCuv8iEfwXmU1GFJrlwxQLD2oAKIbyqQQQogdYIzBXf02GiQGiLjfXiG9YUaSkAIZ+YLaIUnB9NMywqdMSmL9EULIUBgSRugZD+wngPW4vgzAEEKKoacr3MsFRh/f/UIIIQVRQ6QIrnDX0phu32dS+mV9aaURQuwAWeEp6TDhUjcLEIOazgohhNgPRqq6JR1igZ0swElh4oMQUgI1Oo4drcBOFiDn8iCEuEZHK7DdAkTdzJKw6JkQd/E3fr/OCmyfFAkFz5NCCHEXf5OXMPAO6k+0C+BhIcRB2FGHpMzpD3QB3CmM/RFHYcUWSZkVrcJFF8CDQggh7jOrVnTPgCO+EGIh7JMwNA1JkiGZBQjXd1IIIdZB8RuaybRlAjgrhNgCMxpkdOIh/pQAzgghtkCTpwu8MwzBLP5Te+y6JDUyhBDiAyiG3goLcFIofoQQv4DmTUIAWftHSIHQEbWGncoCJIQUBEOU1mCJBchbKiGkeKYhgBNSF4MKG2+phJDiiZMg9SVAKGzWQCPcQKo+KO6dBNP1CiCxBt6rDKTqg+LgScBuhIQQbxkTQgjxFAogIQ7D2G1vKICElIApwsP4Vm8YAySEeAstQEKIt1QmgIxFkNLhSUaGhC6wq/DIEtIXXiaEEG9hDJAQ4i0UQOIFDA+STtAFJoR4Cy1AQoi3UACJv9Av9h66wIQQb6EFSAjxFgogIaQCzIw3UACJ2zDOZwhmRtoYAySEeAstQEKIEdRhrFMACSkBet7DU4cr6oAA8lQj5sG4kh0wBkgI8Ra6wIQYB72aqqAAGgJPedKETllV1wNdYEKIt9ACJIR4CwUwN3RaCbEdCmBuGDkoFd5fSAUwBkgI8RYPLEA3TQkaSISMTncBdOYKG8TADYZ8vn5othMyOnSBCSHewiQIIcRbKICEDAKDrk7ijgtMZ74jk5OTsmXLFtm5c2e8nJiYiJeq4XV9227cuHEjbqDRaGTPYX11dTVeouG5K1euCCE2YJlsUOW6oUQObXp6Ol4q8asDiKASw8XFxVgcKYzENKgolgJxm52dlZmZmXjZy3ozBV0QFxYW4kZInVAALQJCNzc3J3v37rVC8PoBQYQIXrx4MV4q15qQqqAAGo4SvYMHD9bmzlYFRPDs2bNy4cKFLN5ISNmEbGa1SOjCY8eOhdevXw99BL/7zJkzYWTlGnA8AgO+A1uJzaov63SLrL3w0qVLYdGshfaC/RFZv1YdR6tbYNF3LaZZ9WUrbUFFn9NJ+GwWrUEZ5jeurKxQCNnKaFZ9WadaWRZfudQrzUtLS4a4xmz1tULDElV9aTbVcAEjxkXyY06MkM3yZtWXtb4dPnzY2+RG0dAtZiugWfVlrW2wVuxzd+0A1iAy5zadD12bf0mIWhvrACsgsvrk+PHjztfx1QmKqHfv3s1iajIU+QSQsjkQELzz58/HxcykfFA8fejQobiQmoyGL5f4eNSOCykcDEYQubzxklTDxo0bZf/+/fE6+hsT0g8KYAkcOHBAzp07Jzt27BC3gE1Q08B4Q3y0srgpgqQfFMCOBD0f9uLYsWNy6tSp2BpxjxpHBR3yoymCZBAogAUC8UOyg5gBRdBwDBhlmwJYEBQ/M6EIkl5QAAuA4mc2EEEM23/58mUhRIcFLSNC8SueslIt+/btY4kMaYECOAIocEbCg7RTY7a4B6gT3LVrF4ulSQYFMCeo71taWhJiF5iTBD1GOOI0AQbHAM2diBXzcXzlK19h1zYLQW0mSpSefvppIYQWYA5g+bGHh93ACuSsdGRMSAv97E4kPSh+9nPmzBla8IRlMMMA15dZRDeA+N26dYtWoOd45gKP9nNXVlacmI+XNJmammJW2GM2iFfkFz+4vkaIX0EVJrjokRG9evVqvI6saC8hwG+H1YTl9PR0HAZwwYWEK4x4oPMw2t8R7pYBwEWPoa1stv4gbhcvXpTFyOW7FLUiykCwPyCEe/fulZmZGWv3DxMifmPkUNUmNVsnMMLcI6dOnYpnn6tiP0VCaOWw//jOJp1vbJU2q75sIW2Y+X4xl4dtQPiOHz9e2zwZ2Gfz8/OhTVR1k2Azrg2+cVUThZvUbLP+YPGZMkEQLELM3GYDtAK9bVZ92Upb0dZfmVOKQ2hGt2IKnXA6bhBjiLINODOzHNvAjXWAPTh58mShRc9lde5DAB+B/BdeeEFM4+bNm/LUU09JEASFTg6Fs7fo/cm6QD+xRq2rbLbE/mBd2bJPMYm5ySB2asu+ZCusWfVlK4tDmn6xAiQ6pKL9UVQ7cuRIWBeDhCCYDLG35dSG+r6wyTvM9OC9TZZfexssJlhmxLQ7Nu9XtuEbC6E7gFgVCp9NBUXN6MJlM9i/Jk4YjwLxrVu3CvEDjgbTAczrayoQPxe6bh06dMjIQUnRvc9EYSbl0FcAzR2WtDzQtctUTpw44UTnffyG06dPi4msO/4+XgQeYbyfXmVDENxUzp8/b9W+7NdQd4fMq2ksLS1ZtR+raK52gqAL3Ea57m8oo3D06FFxCbjAJlqBrox005UcFu1oZ665UADbKDf+k9+Xmp+fd3LcOsyqZ2Is0Ok4oKtqlgMKoAbu/KYO6YTYn4tA/M6ePSumwUSIHzghgEXFqE2d68NV609h4jQDGPSVuI8TAliURT83NycmYqKFVCQLBQ3QWiSc+MoP6AJrmHjSw/LzoYM+Rqs2CTX8P3EbiwWw2OIsnOwmnvC+jE5i4u+kFeg+FgtgsaksU092191fhYkCSAvQfTybFa47uADRv1Y/6dU63CFVF4bl5s2bW17vtu2o+OL+AjUzXfd9hxtetV0yRhFAdrK3AwpgCi6+flNDDks3AVXr+gU2MTGx7vXl5WXxCUzT2b38ZL34lS2Jowggxc8OKIAl4m7pSjn2DeYoHvZblAlLYXJikflrdRaYfdTropyzGxagSTjdHa5MLDJ/rRZAuhluYVotoM0CSONgMFgHSIzBxJCBrZlgGgeDQQEkxmDioAjEbSiAxBhMFMBCLUD6pcZRggCac5R5vhGjoF9qHCUIoDlHmecbGRX2BsmDPaYHXWBiBBx8wCXsMT1YCE0qB2KHvtdoKDZG7w+KH6kDCiApFTXNJARuZmbG6FG3iX9QACvE9Q7yFDtiGxTACnFJ/CBsEDgIHdYhfNZ3Hat+wBlSMxRA0hcnxa4TFD/voABaSzkOtRI4JCecFjtChAJoMaOLny52KitLsSOD4UZEmwLoCRQ70sqoAuZGRJsC6CBOiR0TEyXBflKAAmg5ekGxk5YdxY+UCAXQIlSdnaqxoxtLyGgUJ4CcBqtwdMHbu3evgUXF9E9dx/XLujgBpPgVAqy6ubm5WPi6z5BmCqn4UQedxfXLmi6wASgr7+DBg3Z2HTNW/KjMpDcUwJqAe3v48GF7Rc8KKH6kNxTAioG1d+zYMQvcW0LchwJYAcraO3LkCLO2hBgEBbBEKHyEmA0FsAQofISUSXHFORTAgoHwHT9+nMJHSGkUV5xDASwI1O+dPHmSyQ1CLIICWAAQPri766mjDo21b4QMCgVwBFC/d/78+dj660wdQkTxI2RQOC9wTg4cOCBLS0s9xI8Uw6DxHvbFJMNDAcwBCpnn5+drS3S4dqnfuHFDLly4IKdPn+7w6qAWLS1fMjxtLjCHdOnHmTNn4u5rdWLzpQ6xu3LliiwvL8vCwkK83mg0steRRSekKtoEkOLXDVh7iPcxyzs4uthhCcHTxY6QumESZEBg+VH8ukOxIzZCARwAlLlgQFKSQLEjrkAB7AMSHp1r/PwAYgdxW1xcjMVONUJcgALYA1h96NbmExA3ip171J3eNDW9SgHsAoqc4fq6jBI4lZGFpQeLj7hH3eJjanqVAtgFuL4ujdQMcYPIUeyGh8Vh7kIB7ADq/Oqu9RsViBuEDu4sioyLTlIMJQqWKwjFz10ogB2A9WcjqkfF2bNnY9e2TAtvKFEYcGPOjUKqhgLYho2TFMG6g+idOnXKA7eWo92Q4qAAtmGT9QfhO3HiRNwv2QUG61tN8fORsqIoG6r/SHOxxfqDlYeBA1wr0eEo2g5RsHyUpUQbqv/IAahJe22w/hDb27dvn5M9LyiADmGJ7WTmcFg17DyM62e69Qerb9euXc52O+PYiqRqOB5giunDMCHW53qXPGaBSdVQAFNMHunFxXhfJyYmJtK1Xi4Aq/JIcVAAxWz3F+6uL/2Rmy5wr0wvs8CkOCiAYrb1h4SHD13WIH5MgpCqsUcAS7zxz83NiYmgvs+X0Vg42CypA3sEsMTQj6nZR/Tu8IWZmRkhtmJvWMJ7F9hU10uN3uILHHHbZixLTGl67b0Ampr8uHjxovhCKSPvMFlMuqGdG7QADXV/fbL+SonBMllMBsB7ATQ18+jLJEOwwEd2f2ntkZzkEkCXbq7T09NSLYNdrb5kfwvpf01rj+QklwDyhjsK/a9Wn6w/lr/Yg4v3GSZB2P+0Ng4cOMD9bxFGGz451dlvAQw4BFNdQPh8m3KUlEhOdfZbAEMKYF3YOu/KqJjjRjJwCswWQE+PketuoQuz7uXFHDeSkXxgtgB6fIxcFUH8rlqtvwrPKZPu37fdeaeQ9XBSpFyUPzMZCrRdzAafP3++cnFvOVoVqlLV9+8NkcjdPTEl97/952VztI/v3LZN7t6xXcaj35y0UH702mvyw+9fi9pL8m+XvyEvPve8vHLtJfEVCmAuyr+KUB6COX5d4uTJk7X0vHE9knL7tu3yE3t+WR6YeUxuj0RwLBI6CN6G2L9rleHb7rpDNt09JeMPTsnUu94h49E2Lz77nHz77xbkW19dEN/wvgzGVCvL1CG68gK318Yh/U1OksHie9OvPyk/+0efiwTwCRm/I3Fzg/hfJH1hYvsmEhgmy1A9nz2QB976iDz+0Y/Ioc9/Vh5572yXT3PzNmKvADp+W3epSBjiZ2vJi6mx2Nc/+pi85Q8/J/dFS1wLsaiFiciFopaZ7KX/pxdNEGivN7ln+zbZ85sfkfd9/COyOXKdWynKoTfrwrVXAAs6HiZ3OUOhsO3YLH7ARAF84AMfkjd++EkZu6M9sREroSiRCYIgvU6wVHPNBhIqK7BdASX580d+aUZ+9TOflC07tknxmJXZ9N4FXl1dFVNBqYjNU0Ui5meV+HW4NqvvK96bBz74pNz73idEUjc3I7Xq4iUsQd3VxeOgw8+DQAbqXYLsdQjnPfdvkw/92afl/gcnxWW8F0DTBx2AiNgGrKZLly7ZF/Pr4J2ZFIrY8cHDsuXRx1XorunGxoKXurrRMrb8gkQilSgqS7ApgurHhtl7ZK5x+ua333WnHDj5Sdnx5klxFSZBDC81wcVnkwji+y4tLTk1yIEJoYj7fuXDcs8vPJYIX1O7UoLMmgtSC1B/WcUBRRPK5CFeCbK3kLa/AxDBgyd/R7aW4g7Xjz8CGHR+woZhp2BJmd51DNlS1PjB8nOteyFCEXUK+j2PPyGb34OqgPQkbitDzSzAqK1llqBuITZFrfln6ZuEba8oEVWbRGyMRPDQqU/GS9cYj9px8RhMOQmB2bhxo5iMugAXFxfFJCB2n/jEJ+TcuXNWxyv7gUFbb926JZcvX5Yq2XDvdrnvwx+X8dfdFglTGBt6Y0ES/8NyLDX+xtP1sbGg+Xy6zXgsaunfjrX+rd6S5xNHGfWBah3LO+6+U267/XXyna8vi0t4L4DgoYcesuLihQjiez7zzDO1zxWsC9+ePXuMv4GMCn4ffiesQVhIeIxjcPPmTSmTLe//Ndn44M8k4iSSCZ5aBtpz6jFEK94mey1MBS/I/laCsOU9YqEbkxaBRRsfC7L3nnjkzdJY/pZcf/H7kgcTK9dUbtxrcFKfOXNGbALZVUybWXUMEyKMIm3sM46k0xnM57J7924Zldt/+q2y7aO/H3dhUz07xtIubXr3tng5lqyPRZfzhjHRntO3D2NByx6nll7cRL2efo5I3KNkQ/w+zc+7uvy8fO7J3xVX8D4JAmzscgYBRLwNwl2m9QqRU4mYlZWVLLvbW/y8v6cWwqZ3RRlfCbNSlbWsrEVae3e0oRIbKpvbkvnVKqDDoPWvVLxwbS0ppdatI/XRb9r5sDy462FxBfYFliQOiLu2bZlLlGiooaXUPMKYThPreZI7EDW8JwQV9W9Y5ps32URnxy7Go9jfpnc+njyIS1Sgg2NJwmMs3cNBIlFZF7cgLX7O+n0EWf1zmPYWSeoB1ZPqeSyD2C1O3nZMlEqmJdXZ5+D/h3/x7fIvS8+LC1AAUyAcNpdu6GKogBCiQeC7xQzxd2gQORNdWljn5Uya3pZKNYzbfupt2XrTCguzoNUaYnZh0EwMa0KXRPSawpj5eZk4JkLYLIJON0y3DdNCQ1U8nX1uys+9793y5T8+Ky7AGGAKLn64eIxrmcXU1FRcV2jTcSkiBrj5N35bNr3tHWncL40BBul6FtvrEONLY39J/A6Jj7XoPdK4nyALnMb10ve6bSzQ4opJYmTDuthh8z3H07jgH7z/Sbn+vXzJEJNgDDAFFtLp06eFmMOJEycy1943xrYmgxHo9Xhr8f+BNOuaQ9Gjgc1HymALEzdYdY0LmgFB5SS3Wj+B/mJrIXWYvFe6Km/e9ZC4AAVQ49SpU7WXl5AECN/8/Hy8fvXqVfGNDT/5piwpkfXzTcmeb1vPXk97eOiCmG0cE6SxQlUkHaZ/0/aeoYr8aV3kJPk+b3CkexwFUINWoDko6w+4NjBsP8buvb9V8MJmrw1luSULtUz+C7SBDVq6f2QEqZi2domTUNbHA9X7qyyJejbdzpVeIRTANmgF1g8sP2X9AWS0fTom46/XxuJTSY14kAN1uYatfXdTP3lNGwpLiVVLocy6nE/Y9lwqmsg2ayLZFN0w63a36a47xAUogG3gQjt06JCQeoDVd/To0ZbncExs6LNdFGHbUo110Dq4afpC2LT6AmkrgVExQGn27VXxwObf61ZlkJXLBLqQaqnmILU0N9ECdBe4XD4G3usGQofsaSdrD2VKRWBDyUP4w1fXFSDr67GoraWxu7T2T6tvzur1pM0LXhdHFFXu3BxBJjMI26zFMH2uJRboABTALuzbt8/JWdlMRo/7tQOXuAg32NzKvybha69qyqcsNr0YWTLXuH2IqybaL+30svaemmRG7xW0WomBdBDDQH7wov0lMIAC2AW6wtUC8UP8tRs4Huj77ANrP7iWrTe7tSXrqvQlXO8ftwhXqBXEhOn2eumL3pJYYZBlfbPSGWlu1BoPDJ2oAQQUwB7ADW6PR5HigfgNMnS+T9ngH/035uoN9FxHTJLpDbqaspk73CKYzY0z0cueDZo5Fcm0tOVzMuHUgpPf++5VcQEKYB9gleACJeUwqPgB3JB8sQL/77vPiipubkmAhOmgCMria6nha2Z9VZ/gQGWCg6aAjQUq7ifpeyYWYtCseNHqAyVVysy0jB//x3caYhb5ghsUwAHABUoRLJ5hxE+B7asriSky2D/cBfr/33626X6ue6egGQPsmK1obplldMNs8PtIQFNB1coGJWi1NJOP0HuGqKxwKP+6lH9MwPLId6wogANCESwWhBbyzBiHJEl1x6HIlMlwF+j//vNlWXvt1daSl7bEhsrIaukSES2Wp22axfqSJ8JWoy5smywpWO8yZ/OHBGPyj08tiitQAIeAIjg6EDCUuvRKePQDf+t6mRJKYW4+8/fJ+rrEhiTDV4lkc/8m0tfMFAfx0KgizYo+aXOVm7E9tVWob7emfF/tc6Itbr7yqqyYPhTWEPctCuCQQARxAdtYIlOkQ5cHNUpKEeLlQ5nSzUtJ7WMQaFnYLFObsL62L13PymNa+/hma+uyu61LyQqiRXPDA/mHL/2Nge5vG0Oc6BTAHKgLuZDeCRWqUpEO3bDA5S3yxqGKpl0WQWSCX7v0V5IZYyLN8f7CZoCw9RRSsTppieupZIpa0cJ6HaxC7X3CbGTBSPhekiWH3F9AAcwJLrxdu3aN7hLXqUoVgJsF9tMoLm83lDvtsgi++tdfiITwWqp8rZ3dVNZXlaw0SwODthifyhqrx+lSf690mxZXWFRVYJIxXjz7JbnhSAG0IrcAOn7dDgxc4qmpKW/KMwYFFpqy+srsx6tE0NW+wmtRLHD186ebQ9rjybZBCsJQEzlpHdoq2aSZNVaWYBC0XsGhZg3qRdPKKV7+20W54pj1B3ILYN3xJJPARYih6NFzxPfucxA+WMW4KZRh9XWiMGvcUG59+1l5+ctfFDU8VXusrzVBrAubntFt2o66YKaK2lZs3fr5q9e+L1/9rJs3eLrABYL+qrjwdSH05UahC1+1tXpNlDXuYob45cgVXoUIanndpHg5WHeShaFmxYV6NzitrCV7rAUYRda91+q1/5K/+NgJufk/r4qLUABLQBfCRcfLNVRdXp3C1/594BIXlW02iRtf/oJcj0VQtPie6CG8hA4Wod6lriWDHGivq5b6wzeipMe5jx2X1brifhXE2Zp2MSkNzLoGcZiZmYnXbQcih365iHuaLjLY35jt78CBA5XN+lfUxOjd2PKeJ+T+/R9qmRA9mTgd1X9r2kRGrZMaZROhB62THukTL6lJj/7z2eflq5/5rLx8za2kRzsUwIrBRYh4oW1iqAYlhehB/GwcoRkzy2GeYxwD7P98cx53B/sEFij2Udnxz9vu2y6Tv/V7snHb9rjkeUO7sKXCCBcP62pGuQ2BPsObpDPEabPJRe/1T1/8knz9z/9SnCZVPgpgjeBCxEU4NzdX+MVYBLBklpeXY8FzdVh6NRm8mhtZ3ZQmJibWbbu6uprtAzXXshI9tayarY8+Lm/Yu182bduWTGmpiVk89WVq9WVTaiorL54OUxPBaJtrz31TFk/+ibziuNWnQwE0CIigatPT05WJorp4IXIQvHgZteucG8Ua7n30Mdn+7sdl80MPR6IWtLjGLfP76m5vdOmv/fA1Wfna1+Tfn/mGfO/Zb4pvUAANR7ltyjrBEtYJlkoc9fVOKMsES2XFYF1vxA1uj1ziu944JVseeYvc88ZJed1dd8rtd92RTn4u8QALr1xtyMuNFbmxsiIvPTe86LkkGhRAQoi3sAyGkLKpoJyD5IMWICHEW2gBEkK8hQJIiAa9Vb+gC0wI8RZagIQQb6lOAOlbEFIIvJSKgy6wwfDgEFIutbjAvIMNBsWPkHKhkUEI8RYmQYi50FUgJUMBJOZC36QyfL3XUACHgAYJcRVf7zWMARJCvMUaC9AN64s2JCkSnk+jQguQEOItjAGS4aDRQRyCAkiGo20ebUJshgJIhiekBnqJgwedMUBCiLfQAiSEeEsHAaRzQwjxA7rAhBBvqdcFprFZANyJhOSFFiDxHF4CPsOjTwjxFuuzwM44gPRkCSmBoO+rtAAJIV7COkBiDbYYyTTm7YEWICHEW2gBEkK8hQJICPEWCiAhxFsogIQQb3FAAJlzI4TkAwLY6PaiHdLCJDYh7lOKGjV6WoCUFgWtTELqpRQ1ugEBvCJVYa2O8FZADIf36BYG3B2xAK5KVVBHCCkHXlstDLg7GtVagISQDtB8q4nlnkkQUg483UkrNN9qghZgHfB0J8QIrihj5HrUtgghxBA4TknJ3IjaVlUGsyBFUrmPR6eybngEpOCd4K74GXKuLOK/Mf1BYVR+7HinrBseAeFOGBBDdtMC/lMCeEHIyNAKIqQgyr+YFto/ZiVqk0KIKzCMRjrTiNoUVvSucGfFEmhpDYrne4riRzpzUa3oArgglsDzelAc31O8E5J8zKuVdgFcEEI6YKTWGKHvlqqwvzePK6LVPrePBnNRrINmQBW4bnXnP4ss3TP+ulGn9Qftxx3F0CvComhCiHs0orZLkiLomHYLEC+cFkKGhYY4MZ8F0cQPdDptaQUSQlwEpS8N/YlOI0LTCiSEuMa8dBj5qpvjQiuQlAuLlK3AkcPUiNpu6SCA413+4GbUbkVtjxBC7MfvGO2JqD3V6YV+u2UpajtlFHinLwbuR0Ly0JC021sn+s0LfFRGhRdtMXA/EpKH3b1eHJfeNKK2NWrvFEIIWYfRvjVc354jXQ367Ud3hQkhHWF0oxQa0sP1VfRzgRX7pK2AkBBSDBS/wmlIH9dXMagANiQRQUIIqY1gsFeRu2jIAPSLAeo0JJlEfQ/7PRGSB143FYC4358OuvEwAgguS3IUZ4X0hac7Ib0o/AqB+B0f5g+GFUCwIMOIIFWAxPBEIKUytPiBPAIIFoSWICEVwZtHH3KJH8grgGBBKIIkD7yeSXHkFj8wigCCBaEIEkLqAdneT8kIjCqAYCFqy5L0Fql49BiaEoR4CGqS3xe1czIiRSrIZNQuCecWJsRc7O92ggmNUJPckAIYtBB6EBqSdD05IYQQM6lB/Aq0sjBQM+b0aEhBlOVDToqn1qD9N1jTfwF7zg6NG1Yf4n0LUjBFWoA6DUmswYG7pLiCledZy23Q9F9A8Rsae3cZYn3QEFh9Cx23GNGEKyIJ0gv0HMFcwxhSi6PJEEIGAcL36ah9QEqw+nSqTKNOSlKvMyNMlBBC1qMmZDslFY0+VUcdCUpl9kbtsHS1ChnnIcQjFiTxFOelcOHrrSV1F9JBAA9GbU5oFRLiE42onZVE/BakJkyqJIYYzkbfaCYS7FmpfUpOWqGlwN3qK7DsFqK2KMkw9Q0xAJO7UkxKIopYTqfLLWmbFEKIaTTSJcpWIHjL6XNXxNBqkB8DsLggj+QSIcwAAAAASUVORK5CYII=)Akta ProScalekit connectorConnect to Akta Pro to search and monitor company news signals, including sentiment, entities, and industry classification, for sales and market...API key · Scalekit connectorAPI key1 tool](/agentkit/connectors/aktapro/) Also:[MCP server](/agentkit/connectors/aktapromcp/) [![](data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTI0IDEyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNjIuNDk5IDBDMjguODkyIDAgMS40OTMgMjcuMDgyIDEuMDA1IDYwLjU1Ni41MSA5NC41NSAyOC4xMSAxMjIuNzIyIDYyLjEzIDEyMi45MTZjMTAuNTEuMDYzIDIwLjYyNi0yLjUwNiAyOS42MTQtNy4zODhhMS40MzcgMS40MzcgMCAwIDAgLjI2OC0yLjMzOGwtNS43NTYtNS4wOTdhNC4wNiA0LjA2IDAgMCAwLTQuMjcxLS43MThjLTYuMjcgMi42NjktMTMuMDkxIDQuMDI3LTIwLjExMiAzLjk0My0yNy40ODgtLjMzNi00OS43MDQtMjMuMTk3LTQ5LjI2My01MC42NjcuNDMtMjcuMTI1IDIyLjY0Ni00OS4wNTggNDkuODg4LTQ5LjA1OGg0OS44OTh2ODguNjNMODQuMDkgNzUuMDg2Yy0uOTE4LS44MTMtMi4zMi0uNjU2LTMuMDYuMzI1LTQuNTQzIDYuMDE0LTExLjk0NiA5Ljc1Mi0yMC4xNjggOS4xODYtMTEuNDA3LS43ODYtMjAuNjQ3LTkuOTYyLTIxLjUwMi0yMS4zNTYtMS4wMjMtMTMuNTkxIDkuNzU0LTI0Ljk3NSAyMy4xMzktMjQuOTc1IDEyLjEwNCAwIDIyLjA3NCA5LjMxMyAyMy4xMTggMjEuMTQyYTQuMTMgNC4xMyAwIDAgMCAxLjM1OSAyLjczN2w3LjM3MiA2LjUzM2MuODM0Ljc0IDIuMTYxLjI4OSAyLjM3MS0uODEzLjUzLTIuODQxLjcxOS01Ljc5OS41MS04LjgzNS0xLjE4Ny0xNy4zMDMtMTUuMjEyLTMxLjIyNC0zMi41MzctMzIuMjg5QzQ0LjgzMiAyNS41MiAyOC4yMyA0MS4wNDUgMjcuNyA2MC41MDRjLS41MTQgMTguOTY1IDE1LjAzOCAzNS4zMTQgMzQuMDE2IDM1LjczNEEzNC42NjQgMzQuNjY0IDAgMCAwIDgyLjkyIDg5LjYxbDM2Ljk5MSAzMi43NjZjMS41ODQgMS40MDUgNC4wODcuMjc4IDQuMDg3LTEuODM1VjIuMzMzQTIuMzMyIDIuMzMyIDAgMCAwIDEyMS42NjMgMEg2Mi40OTlaIiBmaWxsPSIjMDAzREZGIi8+PC9zdmc+)Algolia CrawlerScalekit connectorConnect to the Algolia Crawler to create, configure, run, pause, and monitor crawlers that extract website content into Algolia indices.Username and password · Scalekit connectorUsername and password20 tools](/agentkit/connectors/algoliacrawler/) [![](data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTI0IDEyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNNjIuNDk5IDBDMjguODkyIDAgMS40OTMgMjcuMDgyIDEuMDA1IDYwLjU1Ni41MSA5NC41NSAyOC4xMSAxMjIuNzIyIDYyLjEzIDEyMi45MTZjMTAuNTEuMDYzIDIwLjYyNi0yLjUwNiAyOS42MTQtNy4zODhhMS40MzcgMS40MzcgMCAwIDAgLjI2OC0yLjMzOGwtNS43NTYtNS4wOTdhNC4wNiA0LjA2IDAgMCAwLTQuMjcxLS43MThjLTYuMjcgMi42NjktMTMuMDkxIDQuMDI3LTIwLjExMiAzLjk0My0yNy40ODgtLjMzNi00OS43MDQtMjMuMTk3LTQ5LjI2My01MC42NjcuNDMtMjcuMTI1IDIyLjY0Ni00OS4wNTggNDkuODg4LTQ5LjA1OGg0OS44OTh2ODguNjNMODQuMDkgNzUuMDg2Yy0uOTE4LS44MTMtMi4zMi0uNjU2LTMuMDYuMzI1LTQuNTQzIDYuMDE0LTExLjk0NiA5Ljc1Mi0yMC4xNjggOS4xODYtMTEuNDA3LS43ODYtMjAuNjQ3LTkuOTYyLTIxLjUwMi0yMS4zNTYtMS4wMjMtMTMuNTkxIDkuNzU0LTI0Ljk3NSAyMy4xMzktMjQuOTc1IDEyLjEwNCAwIDIyLjA3NCA5LjMxMyAyMy4xMTggMjEuMTQyYTQuMTMgNC4xMyAwIDAgMCAxLjM1OSAyLjczN2w3LjM3MiA2LjUzM2MuODM0Ljc0IDIuMTYxLjI4OSAyLjM3MS0uODEzLjUzLTIuODQxLjcxOS01Ljc5OS41MS04LjgzNS0xLjE4Ny0xNy4zMDMtMTUuMjEyLTMxLjIyNC0zMi41MzctMzIuMjg5QzQ0LjgzMiAyNS41MiAyOC4yMyA0MS4wNDUgMjcuNyA2MC41MDRjLS41MTQgMTguOTY1IDE1LjAzOCAzNS4zMTQgMzQuMDE2IDM1LjczNEEzNC42NjQgMzQuNjY0IDAgMCAwIDgyLjkyIDg5LjYxbDM2Ljk5MSAzMi43NjZjMS41ODQgMS40MDUgNC4wODcuMjc4IDQuMDg3LTEuODM1VjIuMzMzQTIuMzMyIDIuMzMyIDAgMCAwIDEyMS42NjMgMEg2Mi40OTlaIiBmaWxsPSIjMDAzREZGIi8+PC9zdmc+)Algolia MonitoringScalekit connectorConnect to Algolia Monitoring to check service status, incidents, search latency, indexing time, reachability, and infrastructure metrics for your Algolia...API key · Scalekit connectorAPI key9 tools](/agentkit/connectors/algoliamonitoring/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/alphaxiv.svg)AlphaXiv MCP serverVendor MCPConnect to AlphaXiv MCP to search and retrieve arXiv research papers, abstracts, authors, and citations from your AI workflows.Scalekit credentials availableOAuth · Vendor MCPOAuth18 tools](/agentkit/connectors/alphaxivmcp/) [![](https://docs.amplemarket.com/mintlify-assets/_mintlify/favicons/amplemarket-50/mca-0KUOMtPMXsSB/_generated/favicon-dark/favicon-32x32.png)AmplemarketScalekit connectorConnect to Amplemarket, an AI-powered sales engagement platform, to search and enrich B2B leads, manage email/LinkedIn outreach sequences, and score...Access token · Scalekit connectorAccess token47 tools](/agentkit/connectors/amplemarket/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/amplitude.svg)Amplitude AnalyticsScalekit connectorConnect to Amplitude's analytics REST APIs: event segmentation, funnels, cohorts, taxonomy, chart annotations, session replay, export, releases, streaming...API key · Scalekit connectorAPI key74 tools](/agentkit/connectors/amplitudeanalytics/) [![](https://cdn.scalekit.com/sk-connect/assets/provider-icons/amplitude.svg)Amplitude Experiment ManagementScalekit connectorManage Amplitude Experiment feature flags, experiments, mutex groups, holdouts, and deployments. Separate connector from Experiment Evaluation (real-time...Access token · Scalekit connectorAccess token57 tools](/agentkit/connectors/amplitudeexperimentmanagement/) [Anakin MCP serverAnakin is an AI platform and marketplace that lets you build, deploy, and access a wide range of AI tools and automated workflows. This MCP connector...](/agentkit/connectors/anakinmcp/) [Anchor Browser MCP serverConnect to Anchor Browser MCP to run cloud browser automation, control live browser sessions, extract web data, and let AI agents browse and act on the...](/agentkit/connectors/anchorbrowsermcp/) [AnthropicConnect to the Claude API to send messages, count tokens, browse models, run message batches, and manage uploaded files with an Anthropic API key.](/agentkit/connectors/anthropic/) [API NinjasAPI Ninjas is a catalogue of small utility and reference-data APIs behind one API key. Get current weather and forecasts, geocode and reverse geocode...](/agentkit/connectors/apininjas/) [API-SportsAPI-Sports provides sports data across separate APIs per sport, all reached with one API key. This connector covers API-Football v3 in full (leagues...](/agentkit/connectors/apisports/) [Apify MCP serverConnect to Apify MCP to run web scraping, browser automation, and data extraction Actors directly from your AI workflows.](/agentkit/connectors/apifymcp/) [ApolloConnect to Apollo.io to search and enrich B2B contacts and accounts, manage CRM contacts, and automate outreach sequences.](/agentkit/connectors/apollo/) Also:[API key](/agentkit/connectors/apolloapikey/)[MCP server](/agentkit/connectors/apollomcp/) [AppcuesConnect to Appcues' Public API v2 to manage in-product experiences (Flows, Pins, Mobile, Launchpads, Banners, Flows 2.0, Embeds, NPS 2.0, Checklists)...](/agentkit/connectors/appcues/) [AppSignal MCP serverAppSignal is an application monitoring and performance management platform providing error tracking, performance monitoring, and alerting for Ruby...](/agentkit/connectors/appsignalmcp/) [Artificial AnalysisArtificial Analysis's free Data API of independent AI model benchmarks. Get LLM intelligence, coding and math index scores, per-benchmark results, token...](/agentkit/connectors/artificialanalysis/) [AsanaConnect to Asana. Manage tasks, projects, teams, and workflow automation](/agentkit/connectors/asana/) Also:[MCP server](/agentkit/connectors/asanamcp/) [AshbyConnect to Ashby, the all-in-one recruiting platform, to manage candidates, applications, jobs, job postings, interviews, offers, and hiring team feedback.](/agentkit/connectors/ashby/) [AssemblyAIAssemblyAI's speech-to-text and speech understanding platform. Transcribe audio and video files from a URL with speaker labels, language detection, PII...](/agentkit/connectors/assemblyai/) [athenahealthathenahealth (athenaOne) is a cloud-based EHR and practice management platform for ambulatory practices. This connector uses athenahealth's Certified FHIR...](/agentkit/connectors/athenahealth/) [Atlassian Rovo MCP serverConnect to Atlassian Rovo MCP server to manage Jira issues, Confluence pages, and Compass components directly from your AI workflows.](/agentkit/connectors/atlassianmcp/) [AttentionConnect to Attention for AI insights, conversations, teams, and workflows](/agentkit/connectors/attention/) [AttioConnect to Attio CRM to manage contacts, companies, deals, notes, tasks, and lists with a modern relationship management platform.](/agentkit/connectors/attio/) Also:[MCP server](/agentkit/connectors/attiomcp/) [AvailityConnect to Availity, the largest US health information network, to check patient eligibility and benefits (X12 270/271), check claim status (276/277)...](/agentkit/connectors/availity/) [AvomaAvoma's REST API for meeting intelligence and conversational revenue data. Search and retrieve meetings, recordings, transcriptions, and AI-generated...](/agentkit/connectors/avoma/) [AWS RedshiftRun SQL and browse databases, schemas and tables.](/agentkit/connectors/redshift/) [Axiom MCP serverAxiom is a cloud-native data analytics and observability platform for ingesting, storing, and querying logs, events, traces, and metrics at scale. The MCP...](/agentkit/connectors/axiommcp/) [BambooHRConnect to BambooHR. Manage employees, time off, applicant tracking, goals, training, benefits and HR reports.](/agentkit/connectors/bamboohr/) [BasecampConnect to Basecamp to manage projects, to-dos, card tables, message boards, comments, schedules, documents and Campfire chats, and to search across a...](/agentkit/connectors/basecamp/) [BaserowConnect to Baserow. List tables and fields, and read, create, update, move and delete rows in your databases using a database token.](/agentkit/connectors/baserow/) [BasinConnect to Basin, the form backend. Read and triage form submissions, and manage forms, projects, and notification mail templates.](/agentkit/connectors/basin/) [BeehiivConnect to beehiiv, the newsletter platform, to manage publications, posts, subscribers, segments, automations, tiers, polls and custom fields through the...](/agentkit/connectors/beehiiv/) [BetterContactConnect to BetterContact to find leads and companies, enrich contacts with verified work emails and phone numbers through waterfall enrichment, and look...](/agentkit/connectors/bettercontact/) [Betterstack MCP serverMonitor uptime, manage logs, and respond to incidents with Better Stack's observability platform.](/agentkit/connectors/betterstackmcp/) [Biomni MCP serverConnect to Biomni MCP by phylo.bio, an AI biomedical research assistant. Analyze life-sciences data, interpret genomic variants, query curated databases...](/agentkit/connectors/biomnimcp/) [BioRender MCP serverConnect to BioRender MCP. Search BioRender's scientific icon and figure template libraries to build publication-ready biological illustrations.](/agentkit/connectors/biorendermcp/) [BitbucketRepositories, pull requests, pipelines, issues, branches and commits.](/agentkit/connectors/bitbucket/) [Bitly MCP serverConnect with Bitly MCP for URL shortening, link analytics, and branded links.](/agentkit/connectors/bitlymcp/) [Bitquery MCP serverConnect to Bitquery MCP. Query on-chain DEX trading data, token prices, OHLCV series, trader profiles, and trending tokens across multiple blockchains...](/agentkit/connectors/bitquerymcp/) [Black Forest LabsBlack Forest Labs' FLUX API for AI image and video generation. Generate images from text with FLUX 3, FLUX.2 and FLUX1.1, edit images from instructions...](/agentkit/connectors/blackforestlabs/) [Bland AIConnect to Bland AI, an enterprise voice AI platform, to place and monitor AI phone calls, review transcripts and recordings, and manage agents, pathways...](/agentkit/connectors/bland/) [BoldSignConnect to BoldSign to send documents and templates for e-signature, track signing status, manage contacts, brands, custom fields and sender identities...](/agentkit/connectors/boldsign/) [Bonsai MCP serverConnect to Bonsai, the all-in-one business management platform for freelancers and agencies. Manage projects, tasks, CRM contacts, deals, invoices, and...](/agentkit/connectors/bonsaimcp/) [BoxBox is a cloud content management platform. Manage files, folders, users, groups, collaborations, tasks, comments, webhooks, search, and more using the...](/agentkit/connectors/box/) Also:[MCP server](/agentkit/connectors/boxmcp/) [Brandfetch MCP serverConnect to Brandfetch's MCP server to search, retrieve, and enrich brand data — logos, colors, fonts, and company details — directly from your AI...](/agentkit/connectors/brandfetchmcp/) [Brave SearchConnect to Brave Search to perform web, image, video, and news searches with privacy-focused results, plus AI-powered suggestions and spellcheck.](/agentkit/connectors/brave/) [BrazeConnect to Braze to create and update user profiles, send and schedule campaigns and Canvases, manage catalogs, email templates, Content Blocks and...](/agentkit/connectors/braze/) [Breathe HRConnect to Breathe HR to manage employees, leave and absence, sickness, change requests, training, expenses and expense claims, and to read company setup...](/agentkit/connectors/breathehr/) [Brevo MCP serverConnect to Brevo MCP. Manage email and SMS campaigns, transactional emails, contacts, lists, automations, and loyalty programs from your AI workflows.](/agentkit/connectors/brevomcp/) [BrexConnect to Brex to manage corporate cards, card and cash transactions, statements, expenses, budgets and spend limits, transfers and vendors, custom...](/agentkit/connectors/brex/) [Bright DataBright Data's web data platform. Fetch any public page past CAPTCHAs and anti-bot blocks with Web Unlocker, run Google and Bing searches through SERP API...](/agentkit/connectors/brightdata/) [Browser UseBrowser Use Cloud runs AI browser agents and hosted stealth browsers. Give an agent a task in plain language and get back its answer or structured JSON...](/agentkit/connectors/browseruse/) [BrowserbaseBrowserbase's headless browser platform for AI agents. Start cloud browser sessions with stealth, proxies, captcha solving and saved login contexts, get...](/agentkit/connectors/browserbase/) [BufferConnect to Buffer. Schedule and publish social media posts, manage channels, ideas, tags and snippets](/agentkit/connectors/buffer/) [Bugsnag MCP serverConnect to Bugsnag MCP. Monitor errors, releases, traces, and span groups across your projects from your AI workflows.](/agentkit/connectors/bugsnagmcp/) [Builder.ioConnect to Builder.io, the visual headless CMS. Read published content entries, create, update and delete entries through the Write API, and manage...](/agentkit/connectors/builderio/) [Buildkite MCP serverConnect to Buildkite MCP. Manage CI/CD pipelines, builds, agents, clusters, and test suites from your AI workflows.](/agentkit/connectors/buildkitemcp/) [BuiltWith MCP serverBuiltWith tracks the technology stacks of millions of websites. The BuiltWith MCP server lets AI agents look up a site's technology profile, track...](/agentkit/connectors/builtwithmcp/) [ButterbaseButterbase is an AI-optimized Backend-as-a-Service platform providing a full backend out of the box: managed PostgreSQL data API, JWT-based user...](/agentkit/connectors/butterbase/) [Cal MCP serverConnect to Cal MCP. Manage bookings, event types, schedules, and availability from your AI workflows.](/agentkit/connectors/calmcp/) [CalendlyEvent types, scheduled events, invitees, availability and webhooks.](/agentkit/connectors/calendly/) Also:[MCP server](/agentkit/connectors/calendlymcp/) [Candid MCP serverConnect to Candid MCP. Search nonprofit organizations, explore philanthropic data, and classify social sector activities using Candid's knowledge base.](/agentkit/connectors/candidmcp/) [CanvaConnect to Canva's Connect API to manage designs, assets, folders, brand templates, comments, autofills, exports, and analytics on the user's behalf via...](/agentkit/connectors/canva/) [Canvas MedicalCanvas Medical is a developer-first electronic health record (EHR) with a FHIR R4 API that supports reads and many writes. This connector signs in a...](/agentkit/connectors/canvasmedical/) [Carbone.io MCP serverConnect to Carbone.io MCP. Upload templates, render documents by merging templates with JSON data, convert between 100+ formats, and manage template...](/agentkit/connectors/carboneiomcp/) [Carta MCP serverConnect to Carta. Manage equity cap tables, fund administration, company accounts, and ownership data for venture-backed companies.](/agentkit/connectors/cartamcp/) [Catchr MCP serverCatchr is a data connector platform that syncs marketing and analytics data from ad platforms (Google Ads, Facebook Ads, etc.) to data warehouses and BI...](/agentkit/connectors/catchrmcp/) [ChiliPiper MCP serverConnect to ChiliPiper MCP. Schedule meetings, manage routing rules, track distributions, and automate handoffs from your AI agents.](/agentkit/connectors/chilipipermcp/) [ChorusConnect to Chorus.ai to sync calls, transcripts, conversation intelligence, and analytics.](/agentkit/connectors/chorus/) [Circleback MCP serverCircleback is an AI meeting notes and conversation intelligence platform. The Circleback MCP server provides a standardized interface that allows any...](/agentkit/connectors/circlebackmcp/) [Claap MCP serverVideo collaboration platform for recording, sharing, and discussing async video clips — used for meeting recordings, product demos, feedback, and team...](/agentkit/connectors/claapmcp/) [Clari CopilotConnect to Clari Copilot for sales call transcripts, analytics, call data, and insights.](/agentkit/connectors/claricopilot/) [Clarify MCP serverConnect to Clarify MCP to manage CRM records, leads, campaigns, lists, and analytics directly from your AI workflows.](/agentkit/connectors/clarifymcp/) [Clay MCP serverClay is a go-to-market (GTM) platform that unifies data sourcing from 150+ providers, AI-powered research agents, and workflow orchestration for sales and...](/agentkit/connectors/claymcp/) [Clickhouse MCP serverConnect to ClickHouse MCP to query, analyze, and manage your ClickHouse databases directly from your AI workflows.](/agentkit/connectors/clickhouse/) [ClickSendConnect to ClickSend to send SMS, MMS, text-to-speech voice calls, transactional email, fax, letters and postcards, manage contact lists and SMS...](/agentkit/connectors/clicksend/) [ClickUpConnect to ClickUp. Manage tasks, projects, workspaces, and team collaboration](/agentkit/connectors/clickup/) Also:[MCP server](/agentkit/connectors/clickupmcp/) [CloseLeads, contacts, opportunities, tasks, sequences and custom fields.](/agentkit/connectors/close/) Also:[MCP server](/agentkit/connectors/closemcp/) [CloudflareCloudflare is a cloud platform providing DNS management, CDN, security, and networking services. This connector enables automated management of zones, DNS...](/agentkit/connectors/cloudflare/) Also:[MCP server](/agentkit/connectors/cloudfaremcp/) [Cloudinary MCP serverConnects AI agents to Cloudinary's asset management platform, enabling upload, search, transformation, and organization of media assets through natural...](/agentkit/connectors/cloudinarymcp/) [Cloudpress MCP serverCloudpress is a managed WordPress hosting platform built for the AI era. Its MCP server lets AI agents manage sites, domains, DNS, security rules...](/agentkit/connectors/cloudpressmcp/) [CloverConnect to Clover point-of-sale to manage orders, line items, payments, refunds, inventory items, categories, modifiers, tags, tax rates, discounts...](/agentkit/connectors/clover/) [CodaConnect to Coda (now Superhuman Docs) to read and edit docs, pages, tables, rows, formulas and controls, manage folders, sharing and publishing, and...](/agentkit/connectors/coda/) [CogneeConnect to Cognee, an AI memory engine for agents. Remember data into a knowledge graph, recall it with semantic search, improve stored memory, and forget...](/agentkit/connectors/cognee/) [CoinGeckoCoinGecko's crypto market data API. Get live coin and token prices, ranked market data, historical price, market cap and volume charts, OHLC candles...](/agentkit/connectors/coingecko/) [CoinGecko ProCoinGecko's crypto market data API on a paid-plan key. Get live coin and token prices, ranked market data, top gainers and losers, new listings...](/agentkit/connectors/coingeckopro/) [CoinMarketCap MCP serverConnect to CoinMarketCap MCP. Access real-time crypto quotes, market metrics, technical analysis, trending narratives, and news from your AI workflows.](/agentkit/connectors/coinmarketcapmcp/) [Commonroom MCP serverConnect to Common Room MCP to manage community members, objects, and feedback data directly from your AI workflows.](/agentkit/connectors/commonroommcp/) [CompanyCamCompanyCam's Public API for contractor job-site documentation. Create, search and manage projects; list, upload, describe, tag and comment on project...](/agentkit/connectors/companycam/) [CompanyEnrichCompanyEnrich's B2B data API. Enrich companies by domain, name or social profile, search millions of companies by location, size, revenue, industry...](/agentkit/connectors/companyenrich/) [ConfluenceConnect to Confluence. Manage spaces, pages, content, and team collaboration](/agentkit/connectors/confluence/) [Constant ContactConnect to Constant Contact to manage contacts, lists, tags, custom fields and segments, build and schedule email campaigns, run bulk contact activities...](/agentkit/connectors/constantcontact/) [ContactOutConnect to ContactOut to find verified emails and phone numbers for professionals, enrich people and companies from LinkedIn URLs, emails, or domains...](/agentkit/connectors/contactout/) [Contentful MCP serverConnect to Contentful MCP. Manage spaces, entries, assets, content types, and taxonomies in your Contentful CMS from AI workflows.](/agentkit/connectors/contentfulmcp/) [Context.dev MCP serverConnect to Context.dev's hosted MCP server to scrape, crawl, and search the web, enrich brand data, and manage batches and monitors directly from your AI...](/agentkit/connectors/contextdevmcp/) [Context7 MCP serverConnect to Context7 MCP to fetch up-to-date, version-specific library documentation and code examples directly from the source.](/agentkit/connectors/context7mcp/) [Conversion Tools MCP serverConnect to Conversion Tools MCP. Convert files between 140+ formats including documents, images, audio, video, and data files from your AI workflows.](/agentkit/connectors/conversiontoolsmcp/) [ConvertAPI MCP serverConnect to ConvertAPI MCP. Convert, merge, split, and transform files across 200+ formats including PDF, Word, Excel, images, and more.](/agentkit/connectors/convertapimcp/) [CopperConnect to Copper, the CRM built for Google Workspace. Manage people, companies, leads, opportunities, tasks, projects and activities, and look up...](/agentkit/connectors/copper/) [CrispConnect to Crisp. Manage customer conversations, messages, contacts, operators, inboxes and helpdesk content](/agentkit/connectors/crisp/) [Crossbeam MCP serverConnect to Crossbeam MCP. Discover partner account overlaps, surface ecosystem activity, get partner and account context, and access co-selling...](/agentkit/connectors/crossbeammcp/) [Crustdata MCP serverPeople and company intelligence platform for candidate sourcing, sales prospecting, and talent intelligence. Provides real-time data on professionals...](/agentkit/connectors/crustdatamcp/) [CursorConnect to Cursor to launch and manage Cloud Agents that work on your GitHub, GitLab, Azure DevOps, or Bitbucket repositories: start agents and follow-up...](/agentkit/connectors/cursor/) [Customer.io MCP serverConnect to Customer.io MCP to manage customers, campaigns, and events](/agentkit/connectors/customeriomcp/) [Dart AI MCP serverAI-native project management tool for task and document management with deep AI integration.](/agentkit/connectors/dartaimcp/) [Databox MCP serverConnect to Databox MCP. Query metrics, manage dashboards, and push custom data to your Databox analytics and reporting platform.](/agentkit/connectors/databoxmcp/) [Databricks WorkspaceConnect to Databricks Workspace APIs using a Service Principal with OAuth 2.0 client credentials to manage clusters, jobs, notebooks, SQL, and more.](/agentkit/connectors/databricksworkspace/) [Datacircle MCP serverConnect to Datacircle's hosted MCP server to look up LinkedIn profiles and manage your Datacircle balance and exported files.](/agentkit/connectors/datacirclemcp/) [DatadogMonitors, metrics, dashboards, SLOs, incidents and synthetic tests.](/agentkit/connectors/datadog/) [Dataforseo MCP serverConnect to DataForSEO. Access real-time SEO data including SERP results, keyword analytics, backlinks analysis, domain technologies, and AI visibility...](/agentkit/connectors/dataforseomcp/) [DaytonaDaytona runs secure, elastic sandboxes for AI-generated code. Create, start, stop, archive and delete sandboxes; manage snapshots, volumes, registries and...](/agentkit/connectors/daytona/) [Deel MCP serverGlobal HR and payroll platform for hiring, paying, and managing international employees and contractors with built-in compliance.](/agentkit/connectors/deelmcp/) [Deepgram MCP serverConnect to Deepgram MCP. Transcribe audio, generate speech, and manage transcription projects using Deepgram's AI-powered speech recognition API.](/agentkit/connectors/deepgrammcp/) [DeepL MCP serverConnect to DeepL's MCP server to translate and rephrase text, correct grammar, translate documents, and manage glossaries and writing styles.](/agentkit/connectors/deeplmcp/) [DeepSeekDeepSeek's OpenAI-compatible API. Generate chat completions with the deepseek-flash and deepseek-v4-pro models, including thinking (reasoning) mode, JSON...](/agentkit/connectors/deepseek/) [DeepWiki MCP serverConnect to DeepWiki MCP to ask questions about and read AI-generated documentation for public GitHub repositories directly from your AI workflows.](/agentkit/connectors/deepwikimcp/) [DefiLlamaDefiLlama is an open DeFi analytics dashboard. Read total value locked (TVL) for protocols and chains, current and historical token prices, stablecoin...](/agentkit/connectors/defillama/) [Descript MCP serverConnect to Descript MCP. Import media, export transcripts, publish projects, run AI editing agents, and manage jobs from your AI workflows.](/agentkit/connectors/descriptmcp/) [Dev Rev MCP serverConnect to DevRev MCP. Manage issues, work items, conversations, and customer data in the DevRev product development platform.](/agentkit/connectors/devrevmcp/) [Devin MCP serverConnect to Devin MCP. Create and manage AI coding sessions, interact with Devin agents, manage playbooks and schedules, and browse repository wikis from...](/agentkit/connectors/devinmcp/) [DialpadConnect to Dialpad. Place and manage calls, send SMS, read call details, transcripts and AI recaps, manage contacts, and work with users, offices...](/agentkit/connectors/dialpad/) [DiarizeConnect to Diarize to transcribe and diarize audio and video content from YouTube, X, Instagram, and TikTok. Submit transcription jobs and retrieve...](/agentkit/connectors/diarize/) [DigitalOceanConnect to DigitalOcean to manage Droplets, App Platform apps, Kubernetes clusters, managed databases, domains and DNS records, volumes, load balancers...](/agentkit/connectors/digitalocean/) [Digits MCP serverDigits is an AI-powered business finance platform. This MCP connector gives AI agents read-only access to your Digits data — transactions, financial...](/agentkit/connectors/digitsmcp/) [DiscordServers, channels, roles and members, plus application commands.](/agentkit/connectors/discord/) Also:[Bot](/agentkit/connectors/discordbot/) [Docsautomator MCP serverConnect to DocsAutomator MCP. Generate documents and PDFs from templates using your data, automating document creation workflows.](/agentkit/connectors/docsautomatormcp/) [Dovetail MCP serverConnect to Dovetail, the AI-native UX research platform. Access projects, insights, and data from your AI workflows.](/agentkit/connectors/dovetailmcp/) [DrChronoConnect to DrChrono, the cloud EHR and practice management system. Read and update patients, appointments, availability, clinical notes, problems...](/agentkit/connectors/drchrono/) [DropboxFiles and folders, shared links, file requests and team members.](/agentkit/connectors/dropbox/) Also:[MCP server](/agentkit/connectors/dropboxmcp/) [Dropbox SignConnect to Dropbox Sign (formerly HelloSign) to send documents for e-signature, send from reusable templates, track and remind signers, download signed...](/agentkit/connectors/dropboxsign/) [Dropcontact MCP serverB2B contact enrichment and email verification platform that finds, verifies, and enriches professional email addresses and company data.](/agentkit/connectors/dropcontactmcp/) [Dynamic MockupsConnect to Dynamic Mockups to browse mockup templates, catalogs and collections, render product mockups from your designs, export print files, and...](/agentkit/connectors/dynamicmockups/) [Dynamo SoftwareConnect to Dynamo Software API to access investment management, CRM, and reporting data.](/agentkit/connectors/dynamo/) [E2BConnect to E2B to create and manage secure cloud sandboxes for AI agents: start, pause, resume, fork, snapshot and kill sandboxes, read their logs and...](/agentkit/connectors/e2b/) [Echtpost MCP serverConnect to Echtpost MCP. Send physical postcards and letters programmatically via the Echtpost API.](/agentkit/connectors/echtpostmcp/) [eClinicalWorkseClinicalWorks (eCW) is a cloud and on-premise electronic health record (EHR) and practice management system used by ambulatory practices. This connector...](/agentkit/connectors/eclinicalworks/) [Eden MCP serverEden is an AI-powered content creation platform that discovers viral trends across 3M+ social media posts and helps creators generate content in their...](/agentkit/connectors/edenmcp/) [Elation HealthElation Health is a cloud-based electronic health record (EHR) for independent and value-based primary care practices. This connector uses SMART on FHIR...](/agentkit/connectors/elation/) [ElevenLabsConnect to ElevenLabs to generate lifelike speech, manage and design voices, transcribe and dub audio, and build conversational voice agents with...](/agentkit/connectors/elevenlabs/) [EODHD MCP serverEODHD (End of Day Historical Data) provides comprehensive financial market data including end-of-day stock prices, historical OHLCV data, fundamentals...](/agentkit/connectors/eodhdmcp/) [EpicEpic is the most widely used electronic health record (EHR) system in US hospitals and clinics. This connector uses SMART on FHIR with a signed-in...](/agentkit/connectors/epic/) [Epic GamesConnect to Epic Games accounts. Read a player's profile, friends list and block list, and query Epic Games Store ownership, entitlements and offers](/agentkit/connectors/epicgames/) [Era Context MCP serverConnect to Era Context MCP. Access personal finance data including transactions, accounts, spending insights, and AI-powered financial knowledge from Era.](/agentkit/connectors/eracontextmcp/) [Eraser MCP serverConnect to Eraser MCP. Create and edit diagrams, flowcharts, and technical documentation using Eraser's AI-powered diagramming tools.](/agentkit/connectors/erasermcp/) [EventbriteConnect to Eventbrite to create and publish events, manage ticket classes, venues, discounts, inventory and registration questions, and read orders...](/agentkit/connectors/eventbrite/) [Evertrace AIConnect to evertrace.ai to search and manage talent signals, saved searches, and lists. Access rich professional profiles with scoring, experiences, and...](/agentkit/connectors/evertrace/) [ExaConnect to Exa to perform AI-powered semantic web search, crawl websites for structured content, get natural language answers from the web, run in-depth...](/agentkit/connectors/exa/) Also:[MCP server](/agentkit/connectors/examcp/) [Excalidraw MCP serverExcalidraw+ is a collaborative whiteboard and diagramming platform. The Excalidraw MCP server lets AI agents manage scenes, collections, workspaces...](/agentkit/connectors/excalidrawmcp/) [ExistConnect to Exist, the personal analytics app. Read a user's tracked attributes and daily values (mood, sleep, activity, productivity and more), averages...](/agentkit/connectors/exist/) [Expo MCP serverExpo is a platform for building universal React Native apps; its MCP server exposes developer services including EAS builds, submissions, and project...](/agentkit/connectors/expomcp/) [FairingConnect to Fairing to read post-purchase survey questions and the customer responses collected for attribution and marketing analysis.](/agentkit/connectors/fairing/) [Fal AIConnect to fal.ai to run generative media models (image, video, audio, 3D) through the queue or synchronously, track request status and results, discover...](/agentkit/connectors/fal/) [FathomConnect to Fathom AI meeting assistant. Record, transcribe, and summarize meetings with AI-powered insights](/agentkit/connectors/fathom/) Also:[MCP server](/agentkit/connectors/fathommcp/) [FellowAI MCP serverConnect to Fellow.ai MCP to manage meeting notes, action items, agendas, and team collaboration workflows directly from your AI agent.](/agentkit/connectors/fellowaimcp/) [Felt MCP serverConnect to Felt, a cloud-native GIS and mapping platform, to manage maps, layers, and geospatial data, query connected spatial databases, and collaborate...](/agentkit/connectors/feltmcp/) [Fever MCP serverFever is a live entertainment discovery platform. This MCP connector gives AI assistants direct access to Fever's global event catalog — search events by...](/agentkit/connectors/fevermcp/) [Fibery MCP serverConnect to Fibery MCP. Query, create, and update entities across your Fibery workspace using the Fibery API and AI assistant.](/agentkit/connectors/fiberymcp/) [FigmaConnect to Figma to access user files, teams, projects, and design metadata via OAuth 2.0](/agentkit/connectors/figma/) [FilloutConnect to Fillout, the online form, survey and quiz builder. List your forms, read a form's questions and metadata, browse and fetch form submissions...](/agentkit/connectors/fillout/) [Financial Datasets MCP serverFinancial Datasets provides an MCP interface to financial data APIs covering stock prices, financial statements, earnings, insider trades, and...](/agentkit/connectors/financialdatasetsmcp/) [FindymailConnect to Findymail, a B2B data provider for sales prospecting. Find and verify work emails, look up phone numbers and company data, run Intellimatch...](/agentkit/connectors/findymail/) [FinnhubFinnhub provides stock market data and financial news. Get real-time US stock quotes, look up symbols, read company profiles, company and market news...](/agentkit/connectors/finnhub/) [Firecrawl MCP serverConnect to Firecrawl MCP. Scrape, crawl, search, extract structured data, and monitor websites using Firecrawl's AI-powered web scraping API.](/agentkit/connectors/firecrawlmcp/) [FirefliesFireflies' GraphQL API for AI meeting transcripts. List and retrieve meeting transcripts with speaker-attributed sentences, AI summaries, and recording...](/agentkit/connectors/fireflies/) [Fireflies MCP serverConnect to Fireflies MCP. Search meeting transcripts, fetch recordings, manage channels, create soundbites, and retrieve analytics from your AI workflows.](/agentkit/connectors/firefliesmcp/) [FiscalAI MCP serverConnect to FiscalAI MCP. Access financial data for public companies including SEC filings, earnings, stock prices, financial ratios, and company profiles.](/agentkit/connectors/fiscalaimcp/) [Fish AudioConnect to Fish Audio to search the public voice library and manage your own voice models, design new voices from a text description (with base64 audio...](/agentkit/connectors/fishaudio/) [FlashData MCP serverConnect to FlashData's hosted MCP server to run Google Search, Images, News, Shopping, Videos and Places queries, fetch YouTube search results, metadata...](/agentkit/connectors/flashdatamcp/) [Floot MCP serverConnect to Floot MCP to build, deploy, and manage full-stack AI-generated web apps directly from your AI workflows.](/agentkit/connectors/flootmcp/) [Flux MCP serverFlux by Black Forest Labs provides state-of-the-art AI image generation via the FLUX.1 family of models. Generate high-quality images from text prompts...](/agentkit/connectors/fluxmcp/) [Fly.ioConnect to Fly.io to manage apps, Machines, volumes, IP addresses, certificates and secrets on the Fly Machines API.](/agentkit/connectors/fly/) [Folk MCP serverFolk is a collaborative CRM that helps teams manage contacts, track relationships, and run outreach — all in one workspace.](/agentkit/connectors/folkmcp/) [Follow Up BossConnect to Follow Up Boss, the real estate CRM. Manage people (leads and contacts), send in leads and events, log notes, calls and appointments, work...](/agentkit/connectors/followupboss/) [FreeAgentConnect to FreeAgent. Manage contacts, invoices, estimates, credit notes, bills, expenses, bank accounts and bank transaction explanations, projects...](/agentkit/connectors/freeagent/) [FreshBooksConnect to FreshBooks to manage clients, invoices, estimates, payments, expenses, time entries, projects, and accounting reports for small business...](/agentkit/connectors/freshbooks/) [FreshdeskTickets, contacts, companies, agents, notes and canned responses.](/agentkit/connectors/freshdesk/) [FreshserviceConnect to Freshservice. Manage IT service desk tickets, problems, changes, releases, assets, requesters, and the service catalog](/agentkit/connectors/freshservice/) [FrontConnect to Front to manage conversations, messages, comments, drafts, contacts, tags, inboxes, teammates, and channels.](/agentkit/connectors/front/) Also:[MCP server](/agentkit/connectors/frontmcp/) [Frontitude MCP serverConnect to Frontitude MCP to search, read, and manage your product's UX copy, content library, and localization from your AI workflows.](/agentkit/connectors/frontitudemcp/) [Fullenrich MCP serverConnect to FullEnrich MCP. Enrich contacts with verified email addresses and phone numbers using waterfall enrichment across multiple data providers.](/agentkit/connectors/fullenrichmcp/) [GainsightConnect to Gainsight Customer Success to manage companies, contacts, calls to action, success plans, timeline activities, and custom objects. Power...](/agentkit/connectors/gainsight/) [GammaConnect to Gamma to generate presentations, documents, websites and social posts with AI, and to search, export and analyze your Gamma content.](/agentkit/connectors/gamma/) [GitHubRepositories, issues, pull requests, reviews, commits, workflows and releases.](/agentkit/connectors/github/) Also:[Personal access token](/agentkit/connectors/githubpat/)[MCP server](/agentkit/connectors/githubmcp/) [GitLabConnect to GitLab to manage repositories, issues, merge requests, pipelines, CI/CD, users, groups, and DevOps workflows.](/agentkit/connectors/gitlab/) [GleanConnect to Glean's enterprise search and AI platform — search across all company knowledge and apps, chat with Glean's AI assistant, run and manage AI...](/agentkit/connectors/glean/) [Globalping MCP serverGlobalping is a global network measurement platform for running ping, traceroute, DNS lookup, HTTP, and MTR tests from hundreds of probe locations...](/agentkit/connectors/globalpingmcp/) [GmailSend, read and organize email: messages, threads, drafts, labels and filters.](/agentkit/connectors/gmail/) [GoCardless MCP serverConnect to GoCardless MCP. Retrieve and list customers, mandates, payments, payouts, refunds, and subscriptions, and explore integration options from your...](/agentkit/connectors/gocardlessmcp/) [GongConnect with Gong to sync calls, transcripts, insights, coaching and CRM activity](/agentkit/connectors/gong/) Also:[MCP server](/agentkit/connectors/gongmcp/) [Google AdsConnect to Google Ads to manage advertising campaigns, analyze performance metrics, and optimize ad spending across Google's advertising platform](/agentkit/connectors/googleads/) [Google AnalyticsConnect to Google Analytics (GA4) to run reports, realtime reports, and pivot reports, check dimension/metric compatibility, and manage audience exports...](/agentkit/connectors/googleanalytics/) [Google BigQueryDatasets, tables and jobs: run queries, insert rows and manage access.](/agentkit/connectors/bigquery/) Also:[Service account](/agentkit/connectors/bigqueryserviceaccount/) [Google Business ProfileGoogle Business Profile lets businesses manage their presence across Google Search and Maps — business information, locations, performance/insights...](/agentkit/connectors/googlebusinessprofile/) [Google CalendarCreate and update events, manage calendars and sharing rules.](/agentkit/connectors/googlecalendar/) [Google ChatGoogle Chat is Google's messaging and collaboration platform that lets you create and manage spaces, send and search messages, react with emoji, and...](/agentkit/connectors/googlechat/) [Google ClassroomGoogle Classroom is Google's free web-based platform for schools that streamlines creating, distributing, and grading assignments, and simplifies...](/agentkit/connectors/googleclassroom/) [Google Cloud VisionGoogle Cloud Vision API lets you derive insights from images using pre-trained machine learning models — label detection, OCR/text extraction, face and...](/agentkit/connectors/googlecloudvision/) [Google ContactsGoogle Contacts (People API) lets you manage your contacts, contact groups, and directory information. Create, read, update, and delete contacts and...](/agentkit/connectors/googlecontacts/) [Google DocsConnect to Google Docs. Create, edit, and collaborate on documents](/agentkit/connectors/googledocs/) [Google DriveFiles, folders and shared drives, with permissions, comments and revisions.](/agentkit/connectors/googledrive/) [Google FormsCreate and publish forms, read responses and watch for new ones.](/agentkit/connectors/googleforms/) [Google GeminiConnect to the Google Gemini API (Google AI for Developers) with a Google AI Studio API key to generate text and structured output with Gemini models...](/agentkit/connectors/gemini/) [Google LookerConnect to Google Looker or self-hosted Looker Core. Browse dashboards, run Looks, query LookML models, and access BI data programmatically.](/agentkit/connectors/googlelooker/) [Google MapsConnect to the Google Maps Platform to search for places, look up place details and photos, geocode and reverse-geocode addresses, validate postal...](/agentkit/connectors/googlemaps/) [Google MeetConnect to Google Meet. Create and manage video meetings with powerful collaboration features](/agentkit/connectors/googlemeet/) [Google PhotosGoogle Photos is Google's photo and video storage service for uploading, organizing into albums, and managing media created by your app.](/agentkit/connectors/googlephotos/) [Google Search ConsoleGoogle Search Console (the Search Console API, formerly Webmaster Tools API) lets you monitor and manage a site's presence in Google Search results —...](/agentkit/connectors/googlesearchconsole/) [Google SheetsConnect to Google Sheets. Create, edit, and analyze spreadsheets with powerful data management capabilities](/agentkit/connectors/googlesheets/) [Google SlidesConnect to Google Slides to create, read, and modify presentations programmatically.](/agentkit/connectors/googleslides/) [Google TasksGoogle Tasks is Google's task management service for creating and organizing to-do items and task lists, synced across Gmail, Calendar, and mobile devices.](/agentkit/connectors/googletasks/) [Google TrendsProgrammatic, consistently-scaled access to Google Trends search-interest data (5-year window, daily-to-yearly aggregation) — alpha only.](/agentkit/connectors/googletrends/) [Google Workspace (DWD)Admin access to Google Workspace users, groups, Gmail, Drive, Calendar and Chat.](/agentkit/connectors/googledwd/) [Gorgias MCP serverCustomer support helpdesk for e-commerce brands. Centralizes conversations from email, chat, social media, and SMS with ticket management and automation.](/agentkit/connectors/gorgiasmcp/) [GrafanaConnect to a Grafana instance (Grafana Cloud or self-managed) to search, read and edit dashboards and folders, query data sources, record annotations...](/agentkit/connectors/grafana/) [GrainGrain's REST API for managing meeting recordings, transcripts, webhooks, and workspace data. Search and retrieve recordings and transcripts, upload and...](/agentkit/connectors/grain/) Also:[MCP server](/agentkit/connectors/grainmcp/) [Granola MCP serverConnect to Granola MCP using OAuth 2.1 with MCP discovery and dynamic client registration.](/agentkit/connectors/granolamcp/) [GreenhouseConnect to Greenhouse Recruiting through the Harvest v3 API to manage candidates, applications, jobs, job posts, openings, interviews, scorecards, offers...](/agentkit/connectors/greenhouse/) [Greptile MCP serverAI-powered code search and understanding API that indexes GitHub and GitLab repositories, enabling natural language queries over codebases.](/agentkit/connectors/greptilmcp/) [GristConnect to Grist, the relational spreadsheet and database platform. List orgs, workspaces, and documents; read, add, update, upsert, and delete table...](/agentkit/connectors/grist/) [GTmetrix MCP serverConnect to GTmetrix MCP to analyze web page performance, run speed tests, monitor Core Web Vitals, and get actionable optimization recommendations...](/agentkit/connectors/gtmetrixmcp/) [GumroadConnect to Gumroad. Manage products, variants, offer codes, sales, subscribers, licenses, payouts and audience emails](/agentkit/connectors/gumroad/) [Gusto MCP serverConnect to Gusto MCP. Manage employees, contractors, payroll, departments, and company data from your AI workflows.](/agentkit/connectors/gustomcp/) [HackerRank WorkHackerRank for Work REST API for technical hiring. Manage coding assessment tests and invite or review candidates and their reports, schedule and review...](/agentkit/connectors/hackerrankwork/) [HappyScribe MCP serverHappyScribe is an AI-powered transcription and translation service. Connect your HappyScribe account to search transcripts, generate meeting summaries...](/agentkit/connectors/happyscribemcp/) [Harvest MCP serverHarvest is a time tracking and invoicing tool that helps teams track time, manage projects, and create invoices.](/agentkit/connectors/harvestmcp/) [HarvestAPIConnect to HarvestAPI to scrape LinkedIn profiles, companies, and job listings, and search for people and jobs using LinkedIn data. Enables AI agents to...](/agentkit/connectors/harvestapi/) [HealthieConnect to Healthie, the API-first EHR and practice platform for virtual and digital health care. Search and manage clients, book and reschedule...](/agentkit/connectors/healthie/) [Help ScoutConnect to Help Scout to manage conversations, threads, replies and notes, customers and their contact details, organizations, inboxes, saved replies...](/agentkit/connectors/helpscout/) [Hex MCP serverConnect to Hex MCP. Create and continue data analysis threads, search projects, and query your data using natural language from your AI workflows.](/agentkit/connectors/hexmcp/) [HeyGenConnect to HeyGen to create AI avatar videos, translate and lip-sync videos, generate voices and speech, and manage avatars, templates, and brand assets.](/agentkit/connectors/heygen/) [HeyReachConnect to HeyReach to manage LinkedIn outreach campaigns, lead lists, and conversations. List campaigns, retrieve leads, monitor campaign progress, and...](/agentkit/connectors/heyreach/) [Higgsfield MCP serverConnect to Higgsfield MCP to generate AI images and videos, train consistent characters, and manage your creation history across 30+ models including...](/agentkit/connectors/higgsfieldmcp/) [HighLevelConnect to HighLevel (GoHighLevel), the CRM and marketing automation platform for agencies, to manage contacts, opportunities, calendars and appointments...](/agentkit/connectors/highlevel/) [HitPayConnect to HitPay to create payment requests and payment links, look up charges, issue refunds, manage customers, products, orders, invoices, subscription...](/agentkit/connectors/hitpay/) [HoldedHolded's REST API v2 for cloud accounting and business management. Work with contacts and leads, sales and purchase documents (invoices, credit notes...](/agentkit/connectors/holded/) [Honeycomb MCP serverConnect to Honeycomb MCP to let your AI agents query your observability data, investigate traces and anomalies, and manage Boards, Triggers, and SLOs....](/agentkit/connectors/honeycombmcp/) [HTML/CSS to ImageHTML/CSS to Image (HCTI) renders HTML and CSS, a public webpage, or a saved template into a PNG, JPG, WebP or PDF and returns a hosted image URL. Create...](/agentkit/connectors/htmlcsstoimage/) [HubSpotCRM records, marketing emails and events, blogs and HubDB tables.](/agentkit/connectors/hubspot/) Also:[MCP server](/agentkit/connectors/hubspotmcp/) [Hugging face MCP serverConnect to Hugging Face MCP. Search and manage models, datasets, spaces, and collections on the Hugging Face Hub.](/agentkit/connectors/huggingfacemcp/) [HunterHunter finds and verifies professional email addresses. Search every email address at a company domain, find one person's email from their name and...](/agentkit/connectors/hunter/) [IcePanel MCP serverConnect your IcePanel software architecture models to AI agents. Query and update your C4 model landscapes — systems, apps, components, connections, and...](/agentkit/connectors/icepanelmcp/) [IcypeasConnect to Icypeas. Find and verify professional email addresses, scan domains, look up profile URLs, scrape profile and company data, search lead and...](/agentkit/connectors/icypeas/) [IGPT MCP serverIGPT is an AI assistant platform that exposes its capabilities via an MCP server, enabling agents to interact with AI-powered tools and workflows.](/agentkit/connectors/igptmcp/) [incident.ioincident.io's REST API for incident response and on-call. Declare, edit and triage incidents; post incident updates and timeline items; manage follow-ups...](/agentkit/connectors/incidentio/) [InsightlyConnect to Insightly CRM. Manage contacts, organisations, leads, opportunities, projects, tasks, events, notes, and pipelines](/agentkit/connectors/insightly/) [InstagramInstagram Graph API for Business and Creator accounts — publish media (image/video/carousel/reels), read media and insights, manage comments, track...](/agentkit/connectors/instagram/) [InstantlyConnect to Instantly to run cold email outreach: create and manage campaigns and subsequences, add and organize leads and lead lists, read and reply to...](/agentkit/connectors/instantly/) [IntercomContacts, conversations, tickets, companies, tags and help center articles.](/agentkit/connectors/intercom/) [IPinfoIPinfo's IP address data API. Look up the geolocation (city, region, country, coordinates, time zone, postal code), ASN and network flags of any IPv4 or...](/agentkit/connectors/ipinfo/) [Jam MCP serverConnect to Jam MCP. Access bug reports, console logs, network requests, user events, and video transcripts from your AI workflows.](/agentkit/connectors/jammcp/) [Jentic MCP serverConnect to Jentic MCP. Search available API actions, load execution details, manage credentials, and execute API operations from your AI workflows.](/agentkit/connectors/jenticmcp/) [JiminnyConnect with Jiminny to access call recordings, transcripts, coaching insights, and conversation intelligence data.](/agentkit/connectors/jiminny/) [JiraIssues, projects, boards, fields, workflows and permissions.](/agentkit/connectors/jira/) [Jira Service ManagementConnect to Jira Service Management. Manage customer requests, service desks, organizations, knowledge base articles, SLAs, and queues](/agentkit/connectors/jiraservicemanagement/) [JobberConnect to Jobber, the field service management platform. Manage clients, properties, requests, quotes, jobs, visits, invoices, and products for home...](/agentkit/connectors/jobber/) [JobNimbusConnect to JobNimbus CRM and project management for contractors to manage contacts, jobs, tasks, activities, estimates, invoices, work orders, and...](/agentkit/connectors/jobnimbus/) [Jotform MCP serverConnect to Jotform MCP. Create and edit forms, retrieve submissions, assign forms, and search assets from your AI workflows.](/agentkit/connectors/jotformmcp/) [JumpCloudConnect to JumpCloud to manage directory users, devices, user and device groups, policies, commands, applications, RADIUS servers and security alerts for...](/agentkit/connectors/jumpcloud/) [Keenable MCP serverConnect to Keenable MCP to search the web through Keenable's independent, AI-native search index and fetch any page as clean markdown for your agents.](/agentkit/connectors/keenablemcp/) [KintoneKintone is a no-code business application platform where teams build database apps to track records, run approval workflows and collaborate. Read, search...](/agentkit/connectors/kintone/) [Kit MCP serverConnect to Kit MCP. Manage email subscribers, sequences, broadcasts, tags, and forms for your email marketing workflows.](/agentkit/connectors/kitmcp/) [Klaviyo MCP serverConnect to Klaviyo MCP. Report, strategize & create with real-time Klaviyo data](/agentkit/connectors/klaviyomcp/) [Kling AI MCP serverKling AI is a video and image generation platform. This MCP connector exposes Kling AI capabilities — including video generation and image generation —...](/agentkit/connectors/klingmcp/) [KlipfolioConnect to Klipfolio Klips, the KPI dashboard and business-reporting platform. Manage Klips, dashboards and their layout, data sources and refreshes...](/agentkit/connectors/klipfolio/) [La Growth MachineLa Growth Machine's REST API for multichannel B2B outreach. Manage campaigns and their steps, messages and stats; create audiences and leads, search and...](/agentkit/connectors/lagrowthmachine/) [Latch Bio MCP serverLatch Bio is a cloud bioinformatics platform for running computational biology workflows. Its MCP server lets AI agents list and retrieve files, manage...](/agentkit/connectors/latchbiomcp/) [LaunchDarkly MCP serverConnect to LaunchDarkly's hosted MCP server to manage feature flags, experiments, and release controls directly from your AI workflows.](/agentkit/connectors/launchdarklymcp/) [LeadBoxer MCP serverConnect to LeadBoxer MCP to identify anonymous website visitors and enrich them with firmographic data. LeadBoxer is a B2B lead generation and website...](/agentkit/connectors/leadboxermcp/) [Leadfeeder MCP serverConnect to Leadfeeder's MCP server to identify website visitors, track B2B leads, and surface company-level intent data directly from your AI workflows.](/agentkit/connectors/leadfeedermcp/) [LeadIQConnect to LeadIQ to search and enrich B2B contacts and companies with verified emails, direct dials, and mobile numbers. Build prospect lists and power...](/agentkit/connectors/leadiq/) Also:[MCP server](/agentkit/connectors/leadiqmcp/) [LeadMagicConnect to LeadMagic to find and validate work emails, find personal emails and mobile numbers, enrich people and company profiles, search people...](/agentkit/connectors/leadmagic/) [Legal Data Hunter MCP serverConnect to Legal Data Hunter MCP. Search and explore indexed legal data sources worldwide, tracking case law, courts, dockets, and legal data coverage...](/agentkit/connectors/legaldatahuntermcp/) [Lemlist MCP serverConnect to Lemlist MCP. Manage outbound sales campaigns, leads, email sequences, and LinkedIn outreach from your AI workflows.](/agentkit/connectors/lemlistmcp/) [LeverConnect to Lever, the recruiting platform, to manage candidates and opportunities, job postings, interviews and panels, feedback, notes, requisitions...](/agentkit/connectors/lever/) [Lightspeed RetailConnect to Lightspeed Retail (X-Series) to manage products, inventory, customers, sales, registers, outlets, suppliers, consignments, price books...](/agentkit/connectors/lightspeedretail/) [LILT MCP serverLILT is an enterprise translation platform that combines AI speed with human expertise to deliver accurate, domain-specific translations at scale. This...](/agentkit/connectors/liltmcp/) [LinearIssues, projects, cycles, labels, comments and workflow states.](/agentkit/connectors/linear/) Also:[MCP server](/agentkit/connectors/linearmcp/) [LinkedInConnect to LinkedIn to manage posts, ads, organizations, analytics, and professional profiles from your AI workflows.](/agentkit/connectors/linkedin/) [LinkHutConnect to LinkHut (ln.ht) to save, search and organize bookmarks and tags with a Pinboard-compatible API.](/agentkit/connectors/linkhut/) [LinklyHQ MCP serverLinklyHQ is a URL shortening and link management platform offering click analytics, custom domains, UTM tracking, QR codes, and webhook integrations for...](/agentkit/connectors/linklymcp/) [Listen NotesListen Notes is a podcast search engine and database. Search podcasts and episodes by keyword with filters for genre, language, region, length and publish...](/agentkit/connectors/listennotes/) [ListenLabs MCP serverListen Labs is a qualitative research platform for creating, launching, and analyzing studies with AI assistance. This MCP connector gives AI agents...](/agentkit/connectors/listenlabsmcp/) [LobConnect to Lob to send physical mail from your workflows. Create postcards, letters, checks and self mailers, verify US and international addresses, and...](/agentkit/connectors/lob/) [Logo.devLogo.dev's brand data API. Search companies by name to find their domain, then look up a brand by domain, stock ticker, ISIN, crypto symbol or name to get...](/agentkit/connectors/logodev/) [LogRocket MCP serverConnect to LogRocket to access session data, query analytics, investigate user-reported issues, and detect regressions directly from your AI workflows.](/agentkit/connectors/logrocketmcp/) [LokaliseConnect to Lokalise to manage software and content localization: projects, translation keys, translations, languages, comments, contributors, glossary...](/agentkit/connectors/lokalise/) [Loops MCP serverConnect to Loops MCP. Create and manage loops and tasks, set priorities, track work queue stats, and ship completed loops from your AI workflows.](/agentkit/connectors/loopsmcp/) [Lucid MCP serverConnect to Lucid. Create and edit Lucidchart diagrams, Lucidspark boards, and Lucidscale visualizations from your AI workflows.](/agentkit/connectors/lucidmcp/) [LumaConnect to Luma to manage events, guests, calendars, ticketing, and communities on the Luma (lu.ma) event platform.](/agentkit/connectors/luma/) [Luma AILuma AI's Dream Machine API (lumalabs.ai) for AI video and image generation. Generate videos from text or images with the Ray 2 models, extend and...](/agentkit/connectors/lumalabs/) [Lunarcrush MCP serverConnect to LunarCrush MCP. Access social intelligence, sentiment analytics, and market data for crypto assets from your AI workflows.](/agentkit/connectors/lunarcrushmcp/) [Lusha MCP serverConnect to Lusha MCP. Search and enrich B2B contacts and companies, find lookalikes, run prospecting searches, and access intent and activity signals from...](/agentkit/connectors/lushamcp/) [Magic Patterns MCP serverConnect to Magic Patterns, the AI-powered UI design tool. Generate, edit, and manage design components and artifacts from your AI workflows.](/agentkit/connectors/magicpatternsmcp/) [MagicSlidesMagicSlides generates presentations with AI. Create an editable deck from a topic or pasted text, turn a web page into slides, build a designed deck from...](/agentkit/connectors/magicslides/) [MailchimpCampaigns, audiences and members, segments, automations and reports.](/agentkit/connectors/mailchimp/) [Mailercloud MCP serverConnect to Mailer Cloud MCP. Manage email campaigns, subscriber lists, and automation workflows for your email marketing operations.](/agentkit/connectors/mailercloudmcp/) [Mailerlite MCP serverConnect to MailerLite MCP. Manage email campaigns, subscribers, groups, automations, and forms from your AI workflows.](/agentkit/connectors/mailerlitemcp/) [MailgunConnect to Mailgun to send transactional and marketing email, manage domains and DNS/DKIM security, mailing lists, suppressions (bounces, complaints...](/agentkit/connectors/mailgun/) [MailtrapMailtrap is an email delivery platform for developers that provides transactional and bulk email sending, email sandbox testing, and deliverability tools....](/agentkit/connectors/mailtrap/) [Make MCP serverConnect to Make (formerly Integromat). Build, run, and manage automation scenarios, data stores, webhooks, and connections across thousands of apps from...](/agentkit/connectors/makemcp/) [MalwarebytesConnect to the Malwarebytes (ThreatDown) Nebula API to manage endpoints, detections, quarantine, jobs, policies, exclusions, groups, vulnerabilities and...](/agentkit/connectors/malwarebytes/) [MathpixMathpix OCR for STEM content. Read printed and handwritten math, text, tables and chemistry diagrams from images, recognize handwriting from pen strokes...](/agentkit/connectors/mathpix/) [MaxioConnect to Maxio Advanced Billing (formerly Chargify). Manage customers, subscriptions, products, components, coupons, invoices and usage-based billing.](/agentkit/connectors/maxio/) [MEDITECHMEDITECH Expanse is an electronic health record used by hospitals and health systems. This connector signs in a clinician with SMART on FHIR through...](/agentkit/connectors/meditech/) [Mem MCP serverA hosted MCP server that gives AI tools secure access to your Mem notes and collections — enabling AI agents to read, create, search, and organize notes...](/agentkit/connectors/memmcp/) [Mem0 MCP serverConnect to Mem0 MCP. Store, search, and retrieve persistent memory for AI agents and applications using semantic search.](/agentkit/connectors/mem0mcp/) [Memberstack MCP serverConnect to Memberstack MCP. Manage members, plans, form submissions, and permissions for your membership-based application.](/agentkit/connectors/memberstackmcp/) [Mercury MCP serverConnect to Mercury. Access accounts, transactions, recipients, invoices, treasury, webhooks, and approval requests for startup banking workflows.](/agentkit/connectors/mercurymcp/) [Meta AdsMeta's Marketing API for managing Facebook and Instagram ad campaigns, ad sets, ads, creatives, insights, custom audiences, and product catalogs for...](/agentkit/connectors/metaads/) [MetabaseConnect to Metabase, the open-source business intelligence and analytics tool. Search content, browse databases, tables, and fields, run saved questions...](/agentkit/connectors/metabase/) [Metaview MCP serverMetaview is an agentic recruiting platform that automates end-to-end hiring workflows — from candidate sourcing and outreach to interview note-taking and...](/agentkit/connectors/metaviewmcp/) [Metricool MCP serverMetricool is a social media analytics and scheduling platform for managing, analyzing, and scheduling content across Instagram, Twitter/X, Facebook...](/agentkit/connectors/metricoolmcp/) [Microsoft 365Outlook mail and calendar, Teams, SharePoint, OneDrive, Excel and To Do.](/agentkit/connectors/microsoft365/) [Microsoft ClarityConnect to Microsoft Clarity. Export project analytics like traffic, engagement, scroll depth, and click behavior.](/agentkit/connectors/microsoftclarity/) [Microsoft Entra IDConnect to Microsoft Entra ID through Microsoft Graph. Manage users, groups, applications, service principals and directory roles, and read sign-in logs...](/agentkit/connectors/microsoftentraid/) [Microsoft ExcelWorkbooks, worksheets, tables, ranges and charts.](/agentkit/connectors/microsoftexcel/) [Microsoft Power BIConnect to Microsoft Power BI. Work with workspaces, reports, dashboards, semantic models, dataflows, and deployment pipelines, and run DAX queries.](/agentkit/connectors/microsoftpowerbi/) [Microsoft PowerPointConnect to Microsoft PowerPoint. Authenticate with your Microsoft account to find, export, organize, and share PowerPoint presentations stored in OneDrive...](/agentkit/connectors/microsoftpowerpoint/) [Microsoft WordRead, copy and move Word documents and their versions.](/agentkit/connectors/microsoftword/) [Mini Course Generator MCP serverMini Course Generator is a platform for creating and publishing short, focused online mini-courses. It enables creators to build bite-sized educational...](/agentkit/connectors/minicoursegeneratormcp/) [MiniMaxMiniMax's international API platform for generative AI. Chat with the MiniMax M3 language models, turn text into speech with system, cloned or designed...](/agentkit/connectors/minimax/) [Mintlify MCP serverConnect to Mintlify MCP. Read and edit documentation pages, manage navigation nodes, search content, and publish changes via pull requests from your AI...](/agentkit/connectors/mintlifymcp/) [MireyeMireye gives AI agents cited data about US locations. Fetch free public fields such as elevation, flood zones, wetlands, terrain and nearby infrastructure...](/agentkit/connectors/mireye/) [Mireye MCP serverConnect to Mireye's hosted MCP server to fetch cited location data, geocode US addresses, search and enrich parcels, compute drive-time proximity, ask...](/agentkit/connectors/mireyemcp/) [MiroMiro is a visual collaboration platform for teams. Manage boards, sticky notes, shapes, cards, frames, connectors, images, and tags using the Miro REST...](/agentkit/connectors/miro/) Also:[MCP server](/agentkit/connectors/miromcp/) [MissiveMissive's REST API for the collaborative team inbox. List and update conversations and read their messages, comments, drafts and posts; create and send...](/agentkit/connectors/missive/) [Mistral AIMistral AI's La Plateforme API. Generate chat completions with Mistral models (with function calling and JSON output), extract text, tables and images...](/agentkit/connectors/mistral/) [Mixmax MCP serverConnect to Mixmax MCP. Manage email sequences, templates, contacts, and engagement analytics from your AI workflows.](/agentkit/connectors/mixmaxmcp/) [MixpanelConnect to Mixpanel's Query API, Lexicon Schemas, and Warehouse Connectors to run segmentation, funnel, retention, and Insights reports, execute custom...](/agentkit/connectors/mixpanelanalytics/) Also:[Ingestion](/agentkit/connectors/mixpanelingestion/)[Compliance](/agentkit/connectors/mixpanelcompliance/) [Mixpanel MCP serverConnect to Mixpanel MCP to enable your AI agents and tools to directly query Mixpanel analytics data. Select the data region matching your Mixpanel...](/agentkit/connectors/mixpanelmcp/) [Mobbin MCP serverConnect to Mobbin's MCP server to search real-world UI and UX design references from mobile apps, web apps, and websites using natural language. Returns...](/agentkit/connectors/mobbinmcp/) [MomentumConnect to Momentum, the AI-powered revenue intelligence platform. Retrieve meetings with attendee and transcript details, ingest and remap meetings, run...](/agentkit/connectors/momentum/) [Monday.comConnect to Monday.com. Manage boards, tasks, workflows, teams, and project collaboration](/agentkit/connectors/monday/) Also:[MCP server](/agentkit/connectors/mondaymcp/) [MoneybirdConnect to Moneybird. Manage contacts, sales invoices, estimates, purchase documents, bank mutations, time entries, products, and financial reports for...](/agentkit/connectors/moneybird/) [MongoDB AtlasMongoDB Atlas Administration API for managing your Atlas cloud deployments. List organizations and projects; create, update, pause and delete dedicated...](/agentkit/connectors/mongodb/) [MotherDuck MCP serverConnect to MotherDuck MCP. Query and analyze DuckDB databases, explore schemas, create visualizations, and automate data workflows from your AI workflows.](/agentkit/connectors/motherduckmcp/) [Motion MCP serverConnect to Motion MCP. Manage tasks, projects, workspaces, and schedules in the Motion AI-powered project management platform.](/agentkit/connectors/motionmcp/) [MrScraperMrScraper's web scraping platform. Fetch any page as raw HTML, Markdown, a screenshot or structured JSON, crawl multi-page listings and sitemaps through...](/agentkit/connectors/mrscraper/) [MrScraper MCP serverConnect to MrScraper's MCP server to retrieve web pages as HTML, Markdown, or screenshots, search Google, and check your account's subscription and...](/agentkit/connectors/mrscrapermcp/) [MT Newswires MCP serverConnect to the MT Newswires MCP server on viaNexus to search and retrieve real-time, low-latency financial news across equities, fixed income...](/agentkit/connectors/mtnewswiresmcp/) [MuralMural is a digital whiteboard for visual collaboration. Manage murals, widgets (sticky notes, shapes, text, arrows, images), rooms, folders, templates...](/agentkit/connectors/mural/) [Mux MCP serverMux is a video infrastructure platform for developers, providing APIs for video hosting, on-demand streaming, live streaming, and playback with analytics...](/agentkit/connectors/muxmcp/) [Nasdaq Data LinkNasdaq Data Link (formerly Quandl) serves financial, economic and alternative datasets. Query rows from a data table with column filters and column...](/agentkit/connectors/nasdaqdatalink/) [Neon MCP serverConnect to Neon MCP. Manage Neon serverless Postgres databases, projects, branches, and queries from your AI workflows.](/agentkit/connectors/neonmcp/) [Netlify MCP serverBuild, deploy, and manage Netlify projects — sites, functions, environment variables, forms, blobs, and edge functions — from AI agents via the Netlify...](/agentkit/connectors/netlifymcp/) [NextGen OfficeNextGen Office is a cloud-based EHR and practice management system for independent ambulatory practices. This connector uses NextGen Office's...](/agentkit/connectors/nextgenoffice/) [Nimble MCP serverConnect to Nimble MCP. Search the web across multiple engines, extract content from any URL, crawl websites at scale, discover all URLs on a site, and run...](/agentkit/connectors/nimblemcp/) [NocoDB MCP serverConnect to NocoDB MCP. Create and manage databases, tables, records, views, and fields from your AI workflows.](/agentkit/connectors/nocodbmcp/) [noCRMConnect to noCRM, a lead management tool for sales teams. Manage leads, pipeline steps, comments, client folders, prospecting lists, users and follow-up...](/agentkit/connectors/nocrmio/) [NooksConnect to Nooks, the AI sales engagement platform, to work with sequences, prospects, accounts, calls, emails, and tasks.](/agentkit/connectors/nooks/) [Nooks (API Key)Connect to Nooks, the AI sales engagement platform, to work with sequences, prospects, accounts, calls, emails, and tasks. Uses a long-lived...](/agentkit/connectors/nooksapikey/) [NotionPages, databases and data sources, including queries, comments and file uploads.](/agentkit/connectors/notion/) Also:[MCP server](/agentkit/connectors/notionmcp/) [Ocean.ioConnect to Ocean.io to search lookalike companies and people, enrich and look up company and contact data, manage exclusion lists and segmentations, and...](/agentkit/connectors/ocean/) [OdooConnect to Odoo ERP. Search, read, create, update and delete records in any Odoo model (contacts, CRM leads, sales orders, invoices, products, projects...](/agentkit/connectors/odoo/) [OmnisendConnect to Omnisend, the email and SMS marketing platform for e-commerce. Manage contacts and tags, segments, campaigns, automations, products and...](/agentkit/connectors/omnisend/) [OneDriveFiles and folders, with sharing links, permissions and versions.](/agentkit/connectors/onedrive/) [OneNoteNotebooks, sections and pages, including page content.](/agentkit/connectors/onenote/) [Onepage MCP serverOnepage is a website builder platform. The MCP connector lets Claude create, edit, and manage Onepage websites and pages on behalf of the user.](/agentkit/connectors/onepagemcp/) [OpenAIConnect to the OpenAI API to manage the resources in an OpenAI project: files, batch jobs, vector stores and file search, fine-tuning jobs, evals...](/agentkit/connectors/openai/) [OpenAlexOpenAlex is the open catalog of the global research system. Search, filter, group and fetch scholarly works, authors, sources (journals and repositories)...](/agentkit/connectors/openalex/) [OpenRouter MCP serverConnect to OpenRouter's MCP server to access unified LLM routing, model discovery, and generation tools directly from your AI workflows.](/agentkit/connectors/openroutermcp/) [OpenRush MCP serverConnect to OpenRush MCP to access marketing and competitive intelligence data — competitor discovery, keyword research, SERP snapshots, backlink analysis...](/agentkit/connectors/openrushmcp/) [OpenSEO MCP serverConnect to OpenSEO MCP. Research keywords, analyze live SERP results, review domain and competitor intelligence, track rankings, check backlinks, and read...](/agentkit/connectors/openseomcp/) [OpusClip MCP serverConnect to OpusClip's MCP server to turn long-form video into short, ready-to-post social clips directly from your AI workflows.](/agentkit/connectors/opusclipmcp/) [Oracle Health (Cerner)Oracle Health (formerly Cerner) Millennium is an electronic health record used by hospitals and health systems. This connector signs in a clinician with...](/agentkit/connectors/oraclehealth/) [OrdinalConnect to Ordinal, a social media scheduling and collaboration platform, to draft, schedule and review posts across LinkedIn, X, Instagram and TikTok...](/agentkit/connectors/ordinal/) [OtterAI MCP serverConnect to OtterAI MCP. Search meeting recordings, fetch full transcripts, and retrieve user account info from your AI workflows.](/agentkit/connectors/otteraimcp/) [OutlineConnect to Outline, the team knowledge base and wiki. Search, read, create and update documents, organize them in collections, comment, share, and manage...](/agentkit/connectors/outline/) [OutlookEmail, calendar events, contacts and tasks in Outlook.](/agentkit/connectors/outlook/) [OutreachConnect with Outreach to manage prospects, accounts, sequences, emails, calls, and sales engagement workflows.](/agentkit/connectors/outreach/) [PagerDutyConnect to PagerDuty to manage incidents, services, users, teams, escalation policies, schedules, and on-call rotations.](/agentkit/connectors/pagerduty/) [Pandadoc MCP serverConnect to PandaDoc MCP. Create, send, and manage documents, templates, and e-signatures directly from your AI workflows.](/agentkit/connectors/pandadocmcp/) [Parallel AI Task MCP serverConnect to Parallel AI Task MCP to run deep research tasks and task groups directly from your AI workflows.](/agentkit/connectors/parallelaitaskmcp/) [PayPalConnect to PayPal to create and capture orders, refund payments, send invoices, manage catalog products, subscription plans and subscriptions, send...](/agentkit/connectors/paypal/) [PDF.coPDF.co processes PDFs and documents from file URLs. Convert PDFs to text, JSON, CSV, HTML or Excel; create PDFs from HTML, web pages, Office documents and...](/agentkit/connectors/pdfco/) [Pendo MCP serverConnect to Pendo MCP to access product analytics, user guidance, and engagement data directly from your AI workflows.](/agentkit/connectors/pendomcp/) [People Data LabsConnect to People Data Labs to enrich and search person, company, IP and job posting data, resolve people from partial identifiers, and normalize company...](/agentkit/connectors/peopledatalabs/) [PerplexityPerplexity's REST API for web-grounded AI. Run ranked web search, Sonar chat completions with citations (sync and async), and Agent API responses with...](/agentkit/connectors/perplexity/) [PersonioConnect to Personio, the HR platform for small and mid-sized companies, to manage employees and employments, absences, attendances and projects...](/agentkit/connectors/personio/) [Perspective AI MCP serverConnect to Perspective AI's MCP server to build, manage, and query adaptive AI conversational forms that replace static forms, extract structured data...](/agentkit/connectors/perspectiveaimcp/) [PhantomBusterConnect to PhantomBuster to automate web scraping and data extraction workflows. Launch, monitor, and manage automation agents that extract data from...](/agentkit/connectors/phantombuster/) Also:[MCP server](/agentkit/connectors/phantombustermcp/) [PinterestConnect to Pinterest to manage boards, pins, and media, sync product catalogs, and access your Pinterest business account data.](/agentkit/connectors/pinterest/) [PipedriveConnect to Pipedrive CRM. Manage deals, contacts, organizations, activities, leads, and notes to streamline your sales pipeline.](/agentkit/connectors/pipedrive/) Also:[MCP server](/agentkit/connectors/pipedrivemcp/) [Pipeline CRMConnect to Pipeline CRM (formerly PipelineDeals), the sales CRM for small and mid-sized teams. Manage deals, people (leads and contacts), companies...](/agentkit/connectors/pipelinecrm/) [Pixelbin MCP serverImage and video transformation, optimization, and management platform.](/agentkit/connectors/pixelbinmcp/) [PlaidConnect to Plaid to read the bank data your users share through Plaid Link: accounts, real-time balances, account and routing numbers, account holder...](/agentkit/connectors/plaid/) [Plaid SandboxConnect to Plaid's Sandbox environment to build and test Plaid integrations with test banks and test data: create test Items without Link, fire test...](/agentkit/connectors/plaidsandbox/) [Plain MCP serverConnect to Plain MCP. Manage customer support threads, labels, tenants, Help Center articles, and thread field schemas directly from your AI workflows.](/agentkit/connectors/plainmcp/) [Plane MCP serverConnect to Plane MCP. Manage projects, work items, cycles, modules, epics, and initiatives in your Plane workspace from AI workflows.](/agentkit/connectors/planemcp/) [Planet Scale MCP serverConnect to PlanetScale MCP. Run SQL queries, inspect database branches and schemas, get query performance insights, and manage organizations and invoices...](/agentkit/connectors/planetscalemcp/) [Planning Center MCP serverPlanning Center is a church management platform with modules for people (contact database), giving, check-ins, services planning, groups, registrations...](/agentkit/connectors/planningcentermcp/) [Plaud MCP serverConnect to Plaud MCP. Browse your Plaud recordings, read AI-generated notes, and pull timestamped transcripts with speaker labels into your AI workflows.](/agentkit/connectors/plaudmcp/) [PostHog MCP serverConnect to Posthog MCP to enable your AI agents and tools to directly interact with PostHog's products.](/agentkit/connectors/posthogmcp/) [Postman MCP serverConnect to the Postman MCP server to manage collections, workspaces, environments, and APIs directly from your AI workflows.](/agentkit/connectors/postmanmcp/) [PostmarkSend and track transactional and broadcast email with Postmark. Manage templates, message streams, bounces, suppressions, webhooks, and delivery...](/agentkit/connectors/postmark/) [PredictLeadsConnect to PredictLeads for company signals: look up a company by domain and track its job openings, news events, technology detections, financing events...](/agentkit/connectors/predictleads/) [Prefect MCP serverConnect to Prefect Cloud's MCP server to manage workflows, deployments, flow runs, and work pools directly from your AI workflows.](/agentkit/connectors/prefectmcp/) [Prisma MCP serverConnect to Prisma MCP. Manage Prisma Postgres databases, run SQL queries, handle backups, and manage connection strings from your AI workflows.](/agentkit/connectors/prismamcp/) [Privacy MCP serverConnect to Privacy MCP. Create and manage virtual cards, set spend limits, pause or close cards, and review transactions from your AI workflows.](/agentkit/connectors/privacymcp/) [ProductboardConnect to Productboard. Capture customer feedback as notes, manage features, initiatives, objectives, releases, companies and users, and look up...](/agentkit/connectors/productboard/) [Profound MCP serverProfound is an AI search visibility and marketing analytics platform that helps brands understand and optimize their presence across AI-powered answer...](/agentkit/connectors/profoundmcp/) [ProhostAI MCP serverProhostAI is an AI-powered property management platform for Airbnb hosts and property managers, offering AI guest messaging, task/cleaning automation...](/agentkit/connectors/prohostaimcp/) [ProShort AIConnect to ProShort AI to search and retrieve AI-analyzed sales call recordings, transcripts, and summaries.](/agentkit/connectors/proshortai/) [ProspeoConnect to Prospeo to enrich people and companies with verified work emails, mobile numbers and firmographic data, search for prospects and companies with...](/agentkit/connectors/prospeo/) [PylonConnect to Pylon, the modern customer support platform, to manage tickets, contacts, accounts, tasks, projects, and knowledge base content.](/agentkit/connectors/pylon/) Also:[MCP server](/agentkit/connectors/pylonmcp/) [QuickBooksInvoices, bills, payments, customers, vendors and financial reports.](/agentkit/connectors/quickbooks/) [Quicknode MCP serverConnect to QuickNode MCP. Create and manage blockchain RPC endpoints, configure security rules, set rate limits, and monitor usage and logs from your AI...](/agentkit/connectors/quicknodemcp/) [Quiz.Video MCP serverQuiz.Video is an AI-powered platform for creating short-form quiz and flashcard videos. Transform topics, URLs, or documents into shareable quiz and...](/agentkit/connectors/quizvideomcp/) [Quo MCP serverConnect to Quo MCP. Access call details, summaries, transcripts, and voicemails, send text messages, and create or update contacts from your AI workflows.](/agentkit/connectors/quomcp/) [RagieRagie's REST API for retrieval-augmented generation (RAG) over your own content. Ingest documents from files, raw text or URLs, search them with semantic...](/agentkit/connectors/ragie/) [RampConnect to Ramp to manage corporate cards, funds, transactions, bills, reimbursements, vendors, purchase orders, users and company structure, and to read...](/agentkit/connectors/ramp/) [RazorpayConnect to Razorpay to accept and manage payments: create orders, capture payments, issue refunds, manage customers, invoices, payment links...](/agentkit/connectors/razorpay/) [Read AI MCP serverConnect to Read AI to access your meeting intelligence — transcripts, summaries, action items, and insights from meetings, emails, and chats. Retrieve...](/agentkit/connectors/readaimcp/) [ReadwiseConnect to Readwise and Readwise Reader to save and manage highlights, books, tags and Reader documents using a Readwise access token.](/agentkit/connectors/readwise/) [Recall AIRecall.ai is a meeting bot API for Zoom, Google Meet, Microsoft Teams, Webex and more. Send bots to meetings, control recording and in-call chat, and...](/agentkit/connectors/recall/) [Recraft AI MCP serverConnect to Recraft AI MCP. Generate AI-powered images, vectors, icons, and mockups from your AI agents using Recraft's creative design tools.](/agentkit/connectors/recraftmcp/) [RecruiteeConnect to Recruitee, the applicant tracking system. Manage candidates, job offers, pipeline stages, notes, tasks, tags, departments, locations, talent...](/agentkit/connectors/recruitee/) [RedditConnect to Reddit to access user identity, subreddit data, posts, comments, and mod tools.](/agentkit/connectors/reddit/) [RefinerConnect to Refiner, an in-app survey and customer feedback platform, to read survey responses and NPS/CSAT reports, manage contacts and segments, track...](/agentkit/connectors/refiner/) [RenderRender is a unified cloud platform for building and running web services, static sites, background workers, cron jobs, and managed databases. Manage...](/agentkit/connectors/render/) [ReplicateConnect to Replicate to run open-source AI models, manage predictions, trainings, deployments and models through the Replicate HTTP API.](/agentkit/connectors/replicate/) [Replit MCP serverConnect to Replit MCP. Create, update, and inspect Replit apps from natural-language prompts, list your apps, and resolve apps by name from your AI...](/agentkit/connectors/replitmcp/) [Reply.ioConnect to Reply.io, a multichannel sales engagement platform, to manage contacts, accounts, sequences, inbox threads, tasks and outreach reporting.](/agentkit/connectors/reply/) [ResendResend is an email API platform for developers. Send transactional and marketing emails, manage sending domains, contacts, audiences, broadcasts...](/agentkit/connectors/resend/) Also:[MCP server](/agentkit/connectors/resendmcp/) [Retell AIRetell AI is a platform for building, testing and deploying AI voice and chat agents. Create and update voice and chat agents, Retell LLM response engines...](/agentkit/connectors/retell/) [Revealed AI MCP serverConnect to Revealed AI. Track account signals, buyer personas, and people changes to surface timely outreach actions and account intelligence for B2B...](/agentkit/connectors/revealedaimcp/) [Rize MCP serverConnect to Rize MCP using OAuth 2.1 with MCP discovery and dynamic client registration. Access and analyze your time tracking data, projects, clients...](/agentkit/connectors/rizemcp/) [Roam MCP serverConnect to Roam MCP. Access and manage meetings, transcripts, chat messages, calendar events, and people data across your Roam virtual office workspace.](/agentkit/connectors/roammcp/) [RocketlaneConnect to Rocketlane to manage customer onboarding and professional services work: projects, phases, tasks, time entries, time-offs, spaces and space...](/agentkit/connectors/rocketlane/) [RocketReachConnect to RocketReach to look up and verify professional emails and phone numbers, enrich people and companies, and search its database of contacts and...](/agentkit/connectors/rocketreach/) [RootlyRootly's REST API for incident management, on-call and alerting. Create, update and resolve incidents; manage action items, retrospectives and status page...](/agentkit/connectors/rootly/) [Runware MCP serverConnect to Runware's MCP server to generate and edit images, video, audio, and 3D assets using thousands of AI models through a single API.](/agentkit/connectors/runwaremcp/) [SalesforceLeads, opportunities, accounts, reports and SOQL queries.](/agentkit/connectors/salesforce/) [Salesforce Marketing CloudConnect to Salesforce Marketing Cloud to manage contacts, data extensions, automations, journeys, and email/SMS/push sends across your marketing programs.](/agentkit/connectors/salesforcemarketingcloud/) [SalesloftConnect with Salesloft to manage people, cadences, accounts, activities, emails, calls, and notes](/agentkit/connectors/salesloft/) [Sanity MCP serverConnect to Sanity. Manage structured content, documents, datasets, schemas, releases, and media assets for headless CMS workflows.](/agentkit/connectors/sanitymcp/) [Scholar Gateway MCP serverConnect to Scholar Gateway to search Wiley's peer-reviewed academic literature — 8M+ articles from 2,000+ journals spanning sciences, healthcare...](/agentkit/connectors/scholargateway/) [Scite MCP serverConnect to Scite's MCP server to search scientific literature, patents, clinical trials, grants, and FDA drug/device safety data, and manage citation...](/agentkit/connectors/scitemcp/) [Scrapfly MCP serverConnect to Scrapfly MCP. Scrape web pages, take screenshots, and control a cloud browser with anti-bot bypass, JS rendering, and proxy support.](/agentkit/connectors/scarpflymcp/) [SegmentManage a Twilio Segment workspace through the Segment Public API: sources, destinations, warehouses, tracking plans, destination filters, transformations...](/agentkit/connectors/segment/) [SeismicConnect to Seismic to manage sales enablement content — browse and manage teamsites and library content, work with personal workspace files, search...](/agentkit/connectors/seismic/) [Semaphore CI MCP serverSemaphore CI is a fast, cloud-native continuous integration and delivery platform that automates building, testing, and deploying software with flexible...](/agentkit/connectors/semaphorecimcp/) [SemrushConnect to Semrush with a Version 4 API key. Pull backlink analytics and keyword metrics, manage Projects folders, Local listings and reviews, Google...](/agentkit/connectors/semrush/) [Send MCP serverConnect to Send to create, edit, and share Claude-generated documents as polished web pages with engagement tracking, custom domains, and team asset...](/agentkit/connectors/sendmcp/) [SendGridConnect to Twilio SendGrid to send transactional and marketing email at scale, manage templates, contacts, lists, segments, and single sends, verify...](/agentkit/connectors/sendgrid/) [Sentry MCP serverConnect to Sentry MCP server to monitor errors, investigate issues, manage projects, and analyze performance directly from your AI workflows.](/agentkit/connectors/sentrymcp/) [SerperSerper's Google Search API (serper.dev). Get real-time Google results as structured JSON: web search with knowledge graph and People Also Ask, news...](/agentkit/connectors/serper/) [ServiceM8Connect to ServiceM8. Manage jobs, clients, contacts, scheduling, materials, notes, attachments, inbox messages and more for field service businesses.](/agentkit/connectors/servicem8/) [ServiceNowIncidents, changes, problems, catalog requests and CMDB records.](/agentkit/connectors/servicenow/) [SharePointSites, lists and items, document libraries, pages and permissions.](/agentkit/connectors/sharepoint/) [ShippoConnect to Shippo to compare multi-carrier shipping rates, buy and refund labels, track packages, validate addresses, manage orders, batches, customs...](/agentkit/connectors/shippo/) [ShortcutConnect to Shortcut to plan and track software work: stories, tasks, comments, epics, iterations, objectives, labels, teams, workflows, docs, and...](/agentkit/connectors/shortcut/) [SignWellSignWell is an e-signature platform for sending, signing, and managing documents. Connect to create and send documents for signature, manage templates...](/agentkit/connectors/signwell/) [SlackSend and read messages, manage channels, users, user groups and reminders.](/agentkit/connectors/slack/) Also:[MCP server](/agentkit/connectors/slackmcp/) [Sleekplan MCP serverSleekplan is a customer feedback, feature request, and roadmap management platform.](/agentkit/connectors/sleekplanmcp/) [Slite MCP serverConnect to Slite MCP. Create and manage notes, channels, collections, and comments in Slite from AI workflows.](/agentkit/connectors/slitemcp/) [SMART App on FHIRSMART App on FHIR is a healthcare interoperability provider that enables secure access to electronic health records and clinical data using the SMART on...](/agentkit/connectors/customsmartfhir/) [Smartling MCP serverConnect to Smartling MCP to manage translation projects, jobs, files, and glossaries, and run machine translation and localization workflows from your AI...](/agentkit/connectors/smartlingmcp/) [SMTP2GOSMTP2GO is a transactional email and SMS delivery platform for developers. Send transactional and bulk email and SMS, manage templates, sender domains...](/agentkit/connectors/smtp2go/) Also:[MCP server](/agentkit/connectors/smtp2gomcp/) [SnowflakeRun queries and manage databases, schemas, tables, warehouses and roles.](/agentkit/connectors/snowflake/) Also:[Key pair auth](/agentkit/connectors/snowflakekeyauth/) [SpecStream MCP serverConnect to SpecStream's MCP server to find lists of records from the live web with sources and evidence, import and enrich your own lists, organize work...](/agentkit/connectors/specstreammcp/) [Speko MCP serverConnect to Speko's voice AI gateway to route calls and conversations across speech and language models. Manage phone numbers and credentials, and access...](/agentkit/connectors/spekomcp/) [Splice MCP serverConnect to Splice MCP. Search the Splice sample catalog, create and update multi-track stacks, download audio assets, and generate arrangements from text...](/agentkit/connectors/splicemcp/) [SplitwiseConnect to Splitwise to track shared expenses, manage groups and friends, record comments, and review balances between people.](/agentkit/connectors/splitwise/) [Sportradar MCP serverConnect to Sportradar MCP. Browse and search sports data API specs, discover endpoints, check coverage, and access guide pages from your AI workflows.](/agentkit/connectors/sportradarmcp/) [SquareConnect to Square. Take and refund payments, manage orders, customers, catalog items, inventory, invoices, payment links, subscriptions, bookings, team...](/agentkit/connectors/square/) [Stack ExchangeConnect to Stack Exchange to search and read questions, answers, comments, tags, badges and users across Stack Overflow and every other Stack Exchange...](/agentkit/connectors/stackexchange/) [Stack.ai MCP serverConnect to Stack AI MCP. Build, run, and manage AI workflow projects, search knowledge bases, list integration providers, and inspect execution traces...](/agentkit/connectors/stackaimcp/) [StatsigConnect to Statsig to manage feature gates, dynamic configs, experiments, layers, segments, holdouts, and metrics, and to read experiment results, audit...](/agentkit/connectors/statsig/) [StatuspageConnect to Statuspage. Manage status pages, incidents, components, component groups, subscribers, metrics, and page access permissions.](/agentkit/connectors/statuspage/) [StediConnect to Stedi, the healthcare clearinghouse. Search payers, run real-time insurance eligibility and coordination of benefits checks, submit and...](/agentkit/connectors/stedi/) [StoreLeads MCP serverConnect to StoreLeads MCP to discover, search, and analyze e-commerce stores and their technology stack from your AI workflows.](/agentkit/connectors/storeleadsmcp/) [StravaConnect to Strava. Read athlete profiles, activities, segments, routes, clubs and gear, and create or update activities](/agentkit/connectors/strava/) [StripeConnect to Stripe to manage customers, payments, products, subscriptions, invoices, and financial data.](/agentkit/connectors/stripe/) Also:[MCP server](/agentkit/connectors/stripemcp/) [SupabaseConnect to the Supabase Management API to manage organizations, projects, database branches, API keys, secrets, custom domains, network restrictions...](/agentkit/connectors/supabase/) Also:[MCP server](/agentkit/connectors/supabasemcp/) [SupadataConnect with Supadata to extract transcripts, metadata, and structured content from YouTube, social media, and the web using AI.](/agentkit/connectors/supadata/) Also:[MCP server](/agentkit/connectors/supadatamcp/) [Supermetrics MCP serverConnect to Supermetrics MCP to query marketing data, discover data sources, manage campaigns, and run analytics across your connected ad and analytics...](/agentkit/connectors/supermetricsmcp/) [SureContactConnect to SureContact, a marketing CRM for WordPress and WooCommerce, to manage contacts, tags, lists, companies, deals, tasks, notes, campaigns...](/agentkit/connectors/surecontact/) [SurveyMonkey MCP serverConnect to SurveyMonkey to manage surveys, collect responses, and analyze results. Create and update surveys, manage collectors and contacts, and retrieve...](/agentkit/connectors/surveymonkeymcp/) [Swagger MCP serverConnect to Swagger MCP. Create and manage APIs, developer portals, and documentation in SwaggerHub from AI workflows.](/agentkit/connectors/swaggermcp/) [Sybill MCP serverConnect to Sybill. Access AI-generated summaries of sales calls, deals, accounts, and conversations to accelerate B2B revenue workflows.](/agentkit/connectors/sybilmcp/) [Synapse MCP serverConnect to the Synapse MCP server (Sage Bionetworks) to explore Synapse entities, annotations, provenance, and project structure, and to search...](/agentkit/connectors/synapsemcp/) [Synthesize Bio MCP serverConnect to Synthesize Bio MCP. Run differential gene expression analysis, resolve sample metadata, and retrieve results and raw counts data from your AI...](/agentkit/connectors/synthesizebiomcp/) [TableauConnect to Tableau Cloud or Tableau Server to browse workbooks, views, and data sources, export visualizations, and query underlying data.](/agentkit/connectors/tableau/) [Tactiq MCP serverTactiq captures and transcribes meetings in real time, turning conversations into AI-generated notes, summaries, and action items.](/agentkit/connectors/tactiqmcp/) [TailscaleTailscale's REST API for managing a tailnet: list, inspect, authorize, tag, rename and remove devices; control subnet routes and key expiry; manage auth...](/agentkit/connectors/tailscale/) [Tally MCP serverConnect to Tally MCP. Create and edit forms, manage submissions, and update styling and logic in your Tally workspace from AI workflows.](/agentkit/connectors/tallymcp/) [Tango MCP serverConnect to Tango MCP by makegov to search federal contracts, opportunities, vehicles, organizations, and protests, and pull competitive...](/agentkit/connectors/tangomcp/) [Tavily MCP serverConnect to Tavily MCP. Search the web, crawl websites, extract content, map site structure, and run deep research using Tavily's AI-powered search API.](/agentkit/connectors/tavilymcp/) [TeamsTeams, channels and chats: messages, members, meetings and shifts.](/agentkit/connectors/microsoftteams/) [TeamworkConnect to Teamwork. Manage projects, tasks, task lists, milestones, time tracking, messages, companies and people](/agentkit/connectors/teamwork/) [Telnyx MCP serverTelnyx is a communications platform for voice, messaging, and AI. This MCP connector lets AI agents manage phone numbers, send SMS and MMS, place and...](/agentkit/connectors/telnyxmcp/) [Testdino MCP serverTestDino is a Playwright test reporting and analytics platform that centralizes test data, detects flaky tests, and provides AI-powered debugging via MCP...](/agentkit/connectors/testidinomcp/) [The Companies APIThe Companies API's company enrichment and search platform. Enrich a company from its domain, a work email or a social profile URL; search more than 50...](/agentkit/connectors/thecompaniesapi/) [TheirStackConnect to TheirStack to search job postings and companies, look up the technologies and buying-intent topics a company shows in its hiring, and manage...](/agentkit/connectors/theirstack/) [TickTick MCP serverConnect to TickTick MCP. Manage tasks, projects, habits, and focus sessions in your TickTick account from AI workflows.](/agentkit/connectors/ticktickmcp/) [TimelyConnect to Timely to manage time entries, projects, clients, tags, teams, users, planned tasks, and time reports.](/agentkit/connectors/timely/) [Tinyfish MCP serverConnect to Tinyfish MCP. Run browser-based web automations, fetch page content, and search the web using a real cloud Chrome browser.](/agentkit/connectors/tinyfishmcp/) [tl;dvConnect to tl;dv to list meeting recordings, read transcripts and AI notes, and import recordings from a URL for meeting intelligence and follow-up...](/agentkit/connectors/tldv/) [Todoist MCP serverConnect to Todoist MCP. Manage tasks, projects, sections, labels, filters, goals, and reminders from your AI workflows.](/agentkit/connectors/todoistmcp/) [Toggl 2.0Connect to Toggl 2.0 (Toggl Focus), a work planning and time tracking platform, to manage tasks, projects, planned and tracked time, clients, tags...](/agentkit/connectors/togglfocus/) [Toggl TrackConnect to Toggl Track to log, start, and stop time entries, manage projects, clients, tasks, and tags, and pull detailed, summary, and weekly time...](/agentkit/connectors/toggl/) [TombaConnect to Tomba, the B2B email finder. Find and verify professional email addresses, find and validate phone numbers, enrich people and companies, look...](/agentkit/connectors/tomba/) [Topaz LabsTopaz Labs' AI video and image enhancement API. Create video upscaling, denoising, frame interpolation and restoration jobs from a source video URL, track...](/agentkit/connectors/topazlabs/) [TopCounsel MCP serverConnect to TopCounsel by The L Suite to search, shortlist, and compare peer-vetted outside counsel recommendations grounded in firsthand feedback from...](/agentkit/connectors/topcounselmcp/) [TrelloConnect to Trello. Manage boards, cards, lists, and team collaboration workflows](/agentkit/connectors/trello/) Also:[MCP server](/agentkit/connectors/trellomcp/) [Trigger.devConnect to Trigger.dev to trigger and monitor background task runs, manage schedules, queues, concurrency limits, environment variables, deployments...](/agentkit/connectors/triggerdev/) [Twelve Data MCP serverConnect to Twelve Data MCP for real-time and historical financial market data, including stock, forex, crypto, and ETF prices, technical indicators...](/agentkit/connectors/twelvedatamcp/) [Twenty CRMConnect to Twenty, the open-source CRM, to manage people, companies, opportunities, notes and tasks, and to inspect the workspace data model. Works with...](/agentkit/connectors/twenty/) [TwilioSMS and conversations, calls, phone numbers and verification.](/agentkit/connectors/twilio/) [Twitter / XPosts, users, lists, bookmarks, direct messages and media uploads.](/agentkit/connectors/twitteroauth/) Also:[Access token](/agentkit/connectors/twitterbearer/) [Typeform MCP serverConnect to Typeform MCP to create and manage forms, read responses, and manage workspaces, contacts, and webhooks directly from your AI workflows.](/agentkit/connectors/typeformmcp/) [TypefullyConnect to Typefully to draft, schedule and publish posts across X, LinkedIn, Threads, Bluesky, Mastodon and Substack, manage tags and the publishing...](/agentkit/connectors/typefully/) [UnipileConnect to Unipile to read and manage messaging, email, calendar and LinkedIn data across WhatsApp, LinkedIn, Instagram, Telegram, Gmail, Outlook and IMAP...](/agentkit/connectors/unipile/) [Upstream MCP serverConnect to Upstream MCP to access AI-assistant tools and workflows, including inbox management, directly from your AI workflows.](/agentkit/connectors/upstreammcp/) [v0 MCP serverConnect to v0 by Vercel to generate and iterate on web app UIs from natural language. Create chats, send follow-up messages, and inspect v0 Platform chats...](/agentkit/connectors/v0mcp/) [Vapi MCP serverVapi is an AI-powered voice platform for building, testing, and deploying voice AI agents. This MCP connector enables AI agents to manage Vapi assistants...](/agentkit/connectors/vapimcp/) [VaquillVaquill is a legal data API over US primary law: the US Code, the Code of Federal Regulations, statutory codes and administrative regulations for all 56...](/agentkit/connectors/vaquill/) [VercelConnect to Vercel. Access user profile, teams, projects, deployments, and environment settings.](/agentkit/connectors/vercel/) Also:[MCP server](/agentkit/connectors/vercelmcp/) [Vibe Prospecting MCP serverConnect to Vibe Prospecting by Explorium to build B2B lead lists, research companies and prospects, enrich contacts, and personalize outreach from your AI...](/agentkit/connectors/vibeprospectingmcp/) [VimeoConnect to Vimeo API v3.4. Upload and manage videos, organize content into showcases and folders, manage channels, handle comments, likes, and webhooks.](/agentkit/connectors/vimeo/) [Vivu MCP serverConnect to Vivu's hosted MCP server to search your video projects in plain language and find moments in footage you already have.](/agentkit/connectors/vivumcp/) [WakaTimeConnect to WakaTime to read coding activity, stats, summaries, goals, projects, commits, leaderboards and organization dashboards, and to log heartbeats...](/agentkit/connectors/wakatime/) [WebexConnect to Webex. Send and read messages in spaces, manage spaces, teams and memberships, look up people, and schedule and manage Webex meetings...](/agentkit/connectors/webex/) [Webflow MCP serverConnect to Webflow. Build and manage websites, pages, components, styles, assets, CMS collections, and site settings through the Webflow Designer and Data...](/agentkit/connectors/webflowmcp/) [WeflowConnect to Weflow's Public API to read call and meeting recordings and their conversation intelligence data — transcripts, comments, clips, interaction...](/agentkit/connectors/weflow/) [WhatsAppConnect to the WhatsApp Business Cloud API to send and receive messages, manage media, message templates, phone numbers, and business profiles for...](/agentkit/connectors/whatsapp/) [Whimsical MCP serverConnect to Whimsical MCP. Create and edit flowcharts, mind maps, wireframes, and docs, and manage boards, comments, and workspaces from your AI workflows.](/agentkit/connectors/whimsicalmcp/) [Whop MCP serverWhop is a platform for selling digital products, memberships, and communities.](/agentkit/connectors/whopmcp/) [Windsor.ai MCP serverConnect to Windsor.ai MCP to query unified marketing and business data pulled from 350+ connected sources, discover available fields and connectors...](/agentkit/connectors/windsoraimcp/) [WiseWise's REST API for international money transfers and multi-currency accounts. Look up live exchange rates, supported currencies and price comparisons...](/agentkit/connectors/wise/) [Wisprflow MCP serverConnect to Wisprflow MCP to search and read meetings captured by Wispr Flow's meeting recorder — notes, transcripts, participants, and calendar events —...](/agentkit/connectors/wisprflowmcp/) [Wix MCP serverConnect to Wix MCP. Build and manage Wix sites, call REST APIs, search documentation, upload media, and suggest domains from your AI workflows.](/agentkit/connectors/wixmcp/) [WordPress MCP serverConnect to WordPress.com's official MCP server to manage posts, pages, comments, media, and site settings directly from your AI workflows.](/agentkit/connectors/wordpressmcp/) [WorkableConnect to Workable, the recruiting and HR platform. Manage jobs, candidates and their hiring pipeline, comments, ratings, requisitions and offers, plus...](/agentkit/connectors/workable/) [WorkOSWorkOS's REST API for enterprise identity and user management. Manage organizations, AuthKit users, organization memberships and invitations; read...](/agentkit/connectors/workos/) [WrikeConnect to Wrike to manage tasks, folders, projects and spaces, collaborate through comments and approvals, track time with timelogs, and look up...](/agentkit/connectors/wrike/) [XeroInvoices, contacts, payments, bank transactions and financial reports.](/agentkit/connectors/xero/) [YandexConnect to Yandex to manage files and folders on Yandex Disk, read and configure Yandex Metrica analytics counters, goals, filters and reports, and look...](/agentkit/connectors/yandex/) [You.com MCP serverConnect to You.com MCP. Search the web, research topics with cited sources, and extract full page content using You.com's AI-powered search and research...](/agentkit/connectors/youmcp/) [YouTubeConnect to YouTube to access channel details, analytics, and upload or manage videos via OAuth 2.0](/agentkit/connectors/youtube/) [Zapier MCP serverConnect to Zapier MCP to automate workflows and integrate with thousands of apps directly from your AI agent.](/agentkit/connectors/zapiermcp/) [ZendeskTickets, users, organizations, macros and help center articles.](/agentkit/connectors/zendeskoauth/) Also:[API key](/agentkit/connectors/zendesk/) [ZenRows MCP serverConnect to ZenRows MCP. Scrape any webpage with anti-bot bypass, render JavaScript-heavy sites, and automate browsers through ZenRows' cloud...](/agentkit/connectors/zenrowsmcp/) [ZigpollConnect to Zigpoll to manage surveys and questions, read responses, participants and AI insights, analyze attribution and trends, and send survey...](/agentkit/connectors/zigpoll/) [Zoho BooksConnect to Zoho Books. Manage customers and vendors, invoices, estimates, sales and purchase orders, bills, expenses, payments, credit notes, items, chart...](/agentkit/connectors/zohobooks/) [Zoho CRMConnect to Zoho CRM. Manage leads, contacts, accounts, deals, tasks, and other sales activities.](/agentkit/connectors/zohocrm/) [Zoho MailConnect to Zoho Mail. Manage email messages, threads, folders, labels, signatures, tasks, notes, and bookmarks, plus organization-level administration...](/agentkit/connectors/zohomail/) [ZoomMeetings, webinars, registrants, recordings and team chat.](/agentkit/connectors/zoom/) [Zoom Revenue AcceleratorConnect to Zoom Revenue Accelerator (formerly Zoom IQ). Access sales conversation intelligence, deal insights, scorecards, CRM data imports, and team...](/agentkit/connectors/zoomrevenueaccelerator/) [ZoomInfoConnect to ZoomInfo to search and enrich B2B contact and company data, access intent signals, discover technographic insights, and manage GTM Studio...](/agentkit/connectors/zoominfo/) [Plaid Dashboard MCP serverConnect to Plaid's hosted Dashboard MCP server to debug Items, review Link conversion analytics, check API usage, and list your Plaid teams from your AI...](/agentkit/connectors/plaidmcp/) [ZoomInfo MCP serverConnect to ZoomInfo's hosted MCP server to find and enrich B2B company and contact data, surface buyer intent signals, and research accounts from your AI...](/agentkit/connectors/zoominfomcp/) ### No connectors match these filters Clear filters ## Works with [LangChain](/agentkit/examples/langchain/)[OpenAI](/agentkit/examples/openai/)[Anthropic](/agentkit/examples/anthropic/)[Google ADK](/agentkit/examples/google-adk/)[Mastra](/agentkit/examples/mastra/)[CrewAI](/agentkit/examples/crewai/)[Claude Managed Agents](/agentkit/examples/claude-managed-agents/)[Hermes](/agentkit/hermes/)[LangGraph](/agentkit/examples/langgraph/)[OpenAI Agents SDK](/agentkit/examples/openai-agents-sdk/)[Vercel AI SDK](/agentkit/examples/vercel-ai/)[Any MCP client](/agentkit/mcp/overview/) ## Don't see your app? You can still connect your agent to it, or tell us which app to add next. * [**Add your own connector**\ Any OAuth, API key or MCP service, in your environment](/agentkit/bring-your-own-connector/overview/) * [**Call any API with custom tools**\ Scalekit adds the user's credentials to each request](/agentkit/tools/custom-tools/) * [**Request a connector**\ Tell us the app and what your agent needs to do](https://scalekitsupport.portal.usepylon.com/forms/request-a-connector-tool)